> ## Documentation Index
> Fetch the complete documentation index at: https://docs.appeeky.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Ad Signals

> Send trials, purchases and subscriptions back to TikTok Ads, Meta Ads and Google Ads so campaigns optimize for paying users

Ad signals send your app's conversions back to the ad platform the install came from. When a user who installed through an ad smart link starts a trial or pays, Appeeky sends that event to TikTok, Meta or Google with the click id the link captured. The platform can then optimize the campaign for people who pay, not only for installs.

Set it up under **My Apps → your app → Attribution → Ad signals**, or with the [API](#api).

> [!NOTE]
>
> Apple Search Ads has no API for advertisers to upload conversions. Apple attributes installs itself through AdServices, and Appeeky shows that attribution in [ROAS](https://docs.appeeky.com/docs/analytics-roas.md).

***

## How It Works

1. You create a [smart link](https://docs.appeeky.com/docs/analytics-smart-links.md) for each ad campaign, with **Channel** set to the ad platform (`tiktok_ads`, `meta_ads` or `google_ads`) and **External id** set to the campaign id. Use the link as the ad's destination URL.
2. The ad platform adds its click id to the URL when someone taps the ad: `ttclid` (TikTok), `fbclid` (Meta), `gclid`, `gbraid` or `wbraid` (Google). The smart link keeps the click id, the click time, and the visitor's IP address and user agent. IP and user agent are only kept when one of these click ids is present.
3. When the install is matched to the click, the install keeps the link and the click.
4. RevenueCat or Superwall reports a purchase. Appeeky Analytics matches it to the install and sends Appeeky a conversion.
5. For each platform you turned on, Appeeky logs a delivery and sends the event. Every delivery shows in the delivery log with its status.

Installs that did not come through an ad smart link have no click id. Their conversions are logged as skipped for each platform, so nothing is sent for them.

> [!NOTE]
>
> On Android the click id always reaches the install, through the Play install referrer. On iOS it only reaches the install when the app is already installed and opens through the link: the App Store passes nothing to a fresh install, and Appeeky does not fingerprint devices. For new iOS installs from ads, TikTok and Meta measure through SKAdNetwork and AdAttributionKit, which the SDK already reports.

***

## Events

| Appeeky event | TikTok | Meta | Google Ads |
| - | - | - | - |
| Trial started | `StartTrial` | `StartTrial` | Conversion action |
| Purchase | `Purchase` | `Purchase` | Conversion action |
| Trial converted | `Subscribe` | `Subscribe` | Conversion action |
| Renewal | `Subscribe` | `Subscribe` | Conversion action |

Trial started, purchase and trial converted are on by default. Renewal is off by default: renewals usually happen long after the ad click, outside the platforms' attribution windows. Refunds are never sent.

Every event carries its value in USD, as the analytics service reports it, and the event id, which the platforms use to drop duplicates. Trials are sent with value `0`.

**Minimum value** skips paid events below the amount you set. Trials are always sent.

***

## TikTok Ads

Uses the TikTok Events API for app events with your connected TikTok Ads account.

**You need**

* TikTok Ads connected in **Integrations**.
* Your app set up in TikTok Events Manager, and its **TikTok App ID**.

**What Appeeky sends** (`POST /open_api/v1.3/event/track/`)

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "event_source": "app",
  "event_source_id": "7400000000000000001",
  "data": [
    {
      "event": "Purchase",
      "event_time": 1792497600,
      "event_id": "rc_evt_123",
      "user": {
        "ttclid": "E.C.P.ttclid123",
        "external_id": "573baabb…",
        "ip": "203.0.113.7",
        "user_agent": "Mozilla/5.0 (iPhone)"
      },
      "app": { "app_id": "6479581124" },
      "ad": { "callback": "E.C.P.ttclid123" },
      "properties": {
        "currency": "USD",
        "value": 9.99,
        "content_type": "product",
        "contents": [{ "content_id": "pro_monthly" }]
      }
    }
  ]
}
```

`app.app_id` is the App Store id or Play package. `external_id` is the SHA-256 of the RevenueCat app user id, when there is one. Events older than 7 days are skipped.

***

## Meta Ads

Uses the Meta Conversions API for app events with your connected Meta Ads account.

**You need**

* Meta Ads connected in **Integrations** with a direct Meta login. Connections made through a partner can read insights but can't send conversions.
* A dataset (pixel) in Meta Events Manager **linked to your app**, and its **Dataset ID**.
* On iOS, your app's bundle id (Android uses the Play package).

**What Appeeky sends** (`POST /{dataset_id}/events`)

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "data": [
    {
      "event_name": "Purchase",
      "event_time": 1792497600,
      "event_id": "rc_evt_123",
      "action_source": "app",
      "user_data": {
        "fbc": "fb.1.1792403880000.IwAR0fbclid",
        "client_ip_address": "203.0.113.7",
        "client_user_agent": "Mozilla/5.0 (iPhone)",
        "anon_id": "inst_1",
        "external_id": ["573baabb…"]
      },
      "custom_data": { "currency": "USD", "value": 9.99, "content_ids": ["pro_monthly"] },
      "app_data": {
        "advertiser_tracking_enabled": 0,
        "application_tracking_enabled": 1,
        "extinfo": ["i2", "com.example.app", "", "", "18.1", "iPhone16,2", "", "", "", "", "", "", "", "", "", ""]
      }
    }
  ]
}
```

`fbc` is built from the `fbclid` and the click time. `anon_id` is the Appeeky install id. The SDK does not read the iOS App Tracking Transparency choice, so iOS events are sent with `advertiser_tracking_enabled: 0`; Android events with `1`. Events older than 7 days are skipped.

***

## Google Ads

Uploads offline click conversions with the Google Ads API.

> [!WARNING]
>
> Google Ads uploads are implemented but **not yet verified against a live Google Ads account**. Appeeky has no Google Ads connection yet, so you add an OAuth refresh token by hand. Use **Send test event** first: it only validates the upload.

**You need**

* Google Ads available on the Appeeky server (shown as "Not available on this server yet" otherwise).
* An OAuth refresh token with the `https://www.googleapis.com/auth/adwords` scope for a Google user with access to the account. It is stored encrypted and never shown again.
* The **Customer ID**, and the **Manager account ID** when access goes through a manager account.
* A conversion action of type **Import → Clicks**, and its id.

**What Appeeky sends** (`POST /customers/{customer_id}:uploadClickConversions`)

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "conversions": [
    {
      "gclid": "Cj0KCQgclid",
      "conversionAction": "customers/1234567890/conversionActions/555",
      "conversionDateTime": "2026-10-20 12:00:00+00:00",
      "conversionValue": 9.99,
      "currencyCode": "USD",
      "orderId": "rc_evt_123"
    }
  ],
  "partialFailure": true,
  "validateOnly": false
}
```

Exactly one of `gclid`, `gbraid` or `wbraid` is sent, in that order of preference. `orderId` is the event id, so the same conversion is not counted twice. Conversions more than 90 days after the click are skipped.

***

## Test Events

**Send test event** sends a \$1 purchase from a made-up click:

* **TikTok and Meta** need the test event code from Events Manager → Test events. The event appears there and is not counted in reporting.
* **Google Ads** sends the upload with `validateOnly: true`. Google checks the credentials and conversion action and records nothing. A message about the made-up click id still means the credentials work.

Test sends show in the delivery log with a **Test** label and are never retried.

***

## Delivery Log

Each conversion gets one delivery per platform you turned on.

| Status | Meaning |
| - | - |
| `pending` | Waiting to be sent, or waiting for a retry |
| `sent` | The platform accepted the event |
| `skipped` | Not sent; the reason says why |
| `failed` | The platform rejected it, or all attempts failed |

| Skip reason | Meaning |
| - | - |
| `sandbox` | Sandbox or test-store purchase |
| `refund_not_sent` | Refunds are not sent |
| `event_not_enabled` | This event type, or the platform, is turned off |
| `below_min_value` | Below your minimum value |
| `no_click_id` | The install has no click id for this platform |
| `event_too_old` | Older than the platform accepts |
| `missing_settings` | TikTok App ID, Meta dataset id, or Google customer id / conversion action missing |
| `not_connected` | The ad account is not connected (Google: no refresh token) |
| `token_expired` | The ad account's token expired; reconnect it |
| `token_unsupported` | The connection can't send conversions |
| `google_ads_not_configured` | Google Ads is not available on the server |

**Retries.** Timeouts, rate limits and server errors are retried after 1 minute, 5 minutes, 30 minutes and 2 hours: 5 attempts in all, then the delivery is `failed`. Rejections such as an invalid id fail at once. **Retry** in the log sends a failed or skipped delivery again with a fresh set of attempts, using your current settings.

The analytics service may send the same conversion more than once. Appeeky keeps one delivery per conversion and platform, so a conversion is never sent twice.

***

## API

All endpoints use the analytics app id, like the other [Analytics API](https://docs.appeeky.com/docs/analytics-api.md) routes.

| Method | Endpoint | Description |
| - | - | - |
| `GET` | `/v1/analytics/apps/:appId/signals/config` | Settings and connection status per platform |
| `PATCH` | `/v1/analytics/apps/:appId/signals/config` | Update one platform |
| `GET` | `/v1/analytics/apps/:appId/signals/deliveries` | Delivery log, newest first. `status`, `platform`, `limit` (max 200), `cursor` |
| `POST` | `/v1/analytics/apps/:appId/signals/deliveries/:id/retry` | Send a failed or skipped delivery again |
| `POST` | `/v1/analytics/apps/:appId/signals/test` | `{ "platform": "tiktok" }`: send a test event |

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -X PATCH "https://api.appeeky.com/v1/analytics/apps/YOUR_APP_ID/signals/config" \
  -H "X-API-Key: YOUR_APPEEKY_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "platform": "meta",
    "enabled": true,
    "eventKinds": ["trial_started", "purchase", "trial_converted"],
    "minValueUsd": 0,
    "settings": { "datasetId": "1234567890", "bundleId": "com.example.app", "testEventCode": "TEST12345" }
  }'
```

Settings per platform:

| Platform | Settings |
| - | - |
| `tiktok` | `tiktokAppId`, `testEventCode` |
| `meta` | `datasetId`, `metaAppId`, `bundleId`, `testEventCode` |
| `google` | `customerId`, `loginCustomerId`, `conversionActionId`, `conversionActionIds` (per event, e.g. `{ "trial_started": "777" }`); the refresh token goes in `googleRefreshToken` and is never returned |

An empty string clears a setting. Unknown settings are rejected.

***

## Data and Privacy

* Click IP address and user agent are only stored for clicks that carry an ad click id, and only sent to the platform that click came from.
* The app user id is sent as a SHA-256 hash.
* Each delivery stores the conversion, including the click IP and user agent, so a retry can send the same event. The request shown in the log has IP and user agent removed.

See [Privacy](https://docs.appeeky.com/docs/analytics-privacy.md).

***

## Related Docs

* [Smart Links](https://docs.appeeky.com/docs/analytics-smart-links.md)
* [ROAS](https://docs.appeeky.com/docs/analytics-roas.md)
* [Revenue](https://docs.appeeky.com/docs/analytics-revenue.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.