Webhook protocol

asakit forwards what it resolves to any HTTP endpoint you control. Three independent channels, configured per app in the console under Dashboard → Webhooks.

ChannelFiresCarries
Attribution eventsevery completed AdServices resolution, attributed or notwhich campaign / keyword an install came from, keyed by app_user_id
Costonce a daycampaign × country × day spend for the trailing window
Revenue eventsevery purchase, trial, renewal or refund asakit storesthe money, plus the campaign the paying user came from

The payloads below are the contract. asakit posts JSON and reads nothing about your receiver: any 2xx is success, anything else is retried.

Attribution events use the generic analytics-event shape (type / event / distinct_id / properties / context in a batch), so a collector that already speaks it needs no glue — point the URL at it and set the auth headers. Anyone else parses the same documented JSON.

Channel ①: attribution events

Fires as soon as Apple answers the AdServices lookup for a device.

POST <your URL>
<your configured headers>
Content-Type: application/json

{
  "sent_at": 1785730000000,              // send moment, epoch ms
  "events": [
    {                                    // only when attributed
      "type": "user_set",
      "distinct_id": "<your app_user_id>",
      "event_id": "asa-<app_user_id>-set",
      "time": 1785730000000,             // resolution moment, epoch ms
      "properties": {
        "channel": "asa",
        "asa_campaign_id": "67890",      // user traits are dimensions — ids as strings
        "asa_adgroup_id": "111",
        "asa_keyword_id": "222",
        "asa_conversion_type": "Download"  // Download | Redownload
        // + your static properties
      }
    },
    {                                    // always, attributed or not
      "type": "track",
      "event": "asa_attribution",
      "distinct_id": "<your app_user_id>",
      "event_id": "asa-<app_user_id>-track",
      "time": 1785730000000,
      "properties": {
        "attributed": true,
        "org_id": 12345,                 // properties keep Apple's original types
        "campaign_id": 67890,
        "ad_group_id": 111,
        "keyword_id": 222,
        "ad_id": null,
        "conversion_type": "Download",
        "click_date": "2026-07-31",
        "country_or_region": "US"
        // + your static properties
      },
      "context": {
        "platform": "server",
        "lib": "asakit",
        "utm_source": "apple_search_ads", // string copies of the ids, for UTM-keyed receivers
        "utm_campaign": "67890",          // = campaign_id
        "utm_term": "222",                // = keyword_id
        "utm_content": "111"              // = ad_group_id
      }
    }
  ]
}

Configuration

FieldMeaning
Enabledon / off
URLPOST target
Headersattached verbatim to every request — put the receiver’s auth credentials here; asakit never interprets them
Static propertieskey/value pairs merged into every event’s properties (e.g. env = release)

Channel ②: cost

Once per UTC day, after 08:00 UTC (by then the ad platform’s day boundary has settled). The whole window is re-sent every run, not a delta — ad platforms backfill corrections for days after the fact, and a full re-push lets your receiver converge by upserting.

POST <your URL>
<your configured headers>
Content-Type: application/json

{
  // ← your configured body fields are merged in at the top level (project, token, …)
  "rows": [
    {
      "day": "2026-08-02",
      "media_source": "apple_search_ads",
      "campaign": "67890",             // campaign_id as a string
      "country": "US",
      "cost": 38.5,
      "currency": "USD",
      "installs": 31,                  // Apple Search Ads installs
      "impressions": 4200,
      "clicks": 120                    // Apple Search Ads taps
    }
  ]
}

Configuration

FieldMeaning
Enabledon / off
URLPOST target
Headersattached verbatim to every request — put the receiver’s auth credentials here; asakit never interprets them
Body fieldskey/value pairs merged into the request body’s top level (auth token, project id, …)
Window (days)how many trailing days are re-sent each run — default 7

Channel ③: revenue events

Fires per stored revenue event, whatever the source — the RevenueCat webhook or the generic ingest (/v1/revenue/:appId). Same batch shape as channel ①.

POST <your URL>
<your configured headers>
Content-Type: application/json

{
  "sent_at": 1785730000000,
  "events": [
    {
      "type": "track",
      "event": "purchase",                 // see the event names below
      "distinct_id": "<your app_user_id>",
      "event_id": "asa-rev-<source event id>",
      "time": 1785730000000,               // the purchase time, not the send time
      "properties": {
        "revenue_usd": 9.99,               // gross, signed — refunds are negative
        "revenue_net_usd": 6.99,           // after the store cut
        "currency": "USD",
        "product_id": "annual_pro",
        "store": "APP_STORE",
        "environment": "PRODUCTION",
        "period_type": "NORMAL",
        "is_trial": false,
        "is_renewal": false,
        "is_refund": false,
        "source_type": "INITIAL_PURCHASE", // the source's own classifier, unmapped
        "channel": "asa",                  // attribution dimensions, when the user has them
        "campaign_id": "67890",
        "ad_group_id": "111",
        "keyword_id": "222"
        // + your static properties
      },
      "context": {
        "platform": "server",
        "lib": "asakit",
        "utm_source": "apple_search_ads",
        "utm_campaign": "67890",
        "utm_term": "222",
        "utm_content": "111"
      }
    }
  ]
}

Configuration

FieldMeaning
Enabledon / off
URLPOST target
Headersattached verbatim to every request — put the receiver’s auth credentials here; asakit never interprets them
Static propertieskey/value pairs merged into every event’s properties (e.g. env = release)

Delivery, retries, idempotency

Notes