DEV Community

Aleksander Sekowski
Aleksander Sekowski

Posted on

AppsFlyer S2S Returns 200 When the iOS App ID Skips the id Prefix. The Event Never Lands.

A backend worker posts an in-app purchase to AppsFlyer server-to-server. The response is 200. Raw Data stays empty.

The dev key is fine. The JSON body has appsflyer_id and eventName. The bug is usually in the URL path.

POST https://api3.appsflyer.com/inappevent/123456789
Enter fullscreen mode Exit fullscreen mode

That path looks like an App Store ID. AppsFlyer documents iOS app IDs prefixed with id, such as id123456789. Digits alone are how Android package names look (com.example.app), not how iOS IDs travel on this endpoint.

The call still returns 200. The event is not recorded. There is no error body that tells you the path was wrong.

The fix is one character in the path:

POST https://api3.appsflyer.com/inappevent/id123456789
Enter fullscreen mode Exit fullscreen mode

The app_id is in the path, not the JSON

AppsFlyer S2S API 3 puts the dashboard app identifier after /inappevent/. Android apps use the package name: com.example.myapp. iOS apps use the App Store id with an id prefix.

Copying the numeric App Store id from App Store Connect without the prefix is the usual miss. Your Android staging URL works. Production iOS does not, and nothing in the HTTP status hints at why.

The JSON body still needs appsflyer_id, the device id the SDK minted on first launch, and eventName, such as af_purchase. Missing either is a hard validation error. A wrong path is a silent drop.

How I catch it before 200 lies

I maintain Pixellint, an open source linter for pixels and conversion API payloads. The AppsFlyer pack (vendor/appsflyer) contracts URLs on api3.appsflyer.com/inappevent/ and the JSON body against AppsFlyer's S2S docs. On the digits-only iOS path:

$ pixellint validate url 'https://api3.appsflyer.com/inappevent/123456789'
rulepack: vendor/appsflyer (vendor: appsflyer)
  warning vendor.appsflyer.ios_app_id_unprefixed
          This app ID is digits only. AppsFlyer documents that iOS IDs must be
          prefixed with `id`; without it the call still returns 200 and the
          event is not recorded.
    fix:  Prefix the iOS app ID with `id`, such as `id123456789`.
Enter fullscreen mode Exit fullscreen mode

Warnings do not fail the run. This one should fail yours, because a 200 with no row in Raw Data is worse than a 4xx you would have noticed.

Paste the URL or JSON into the browser playground if you do not want a local install. Nothing you paste leaves the browser.

cargo install pixellint
# or: npm install pixellint
pixellint validate url 'https://api3.appsflyer.com/inappevent/id123456789'
pixellint validate json @purchase.json --rulepack vendor/appsflyer
Enter fullscreen mode Exit fullscreen mode

The field table with citations lives on the AppsFlyer S2S pack page. For the mobile attribution stack (MMP vs pixel vs S2S), read Mobile measurement.

eventTime is not epoch and not ISO 8601

AppsFlyer documents eventTime as UTC with a space between date and time: yyyy-mm-dd hh:mm:ss.sss. Example: 2019-05-15 12:17:01.123. Not T. Not a Unix integer.

{
  "appsflyer_id": "1234567890123-1234567",
  "eventName": "af_purchase",
  "eventTime": "1770000000",
  "ip": "192.0.2.1"
}
Enter fullscreen mode Exit fullscreen mode

That string is ten digits. It is not the format AppsFlyer expects. The endpoint accepts the POST. The timestamp is treated as arrival time, not purchase time.

  error   vendor.appsflyer.body.eventTime.invalid
          `eventTime` must match `yyyy-mm-dd hh:mm:ss.sss` in UTC.
    fix:  Send UTC as `2019-05-15 12:17:01.123`, with a space, not `T`.
Enter fullscreen mode Exit fullscreen mode

Meta wants seconds. Reddit and LinkedIn want milliseconds. TikTok wants ISO 8601 with T. AppsFlyer wants a space-separated clock string. The event time units guide maps them side by side so you are not guessing from one vendor sample.

email_hashed and ip follow different rules

AppsFlyer names hashed fields explicitly: email_hashed, phone_number_hashed, first_name_hashed. Send a raw address anywhere in the body and you get two findings:

  error   vendor.appsflyer.body.email_hashed.invalid
  error   vendor.appsflyer.body.unhashed_email
          A field carries what looks like a raw email address.
Enter fullscreen mode Exit fullscreen mode

Trim, lowercase, SHA-256, send the hex digest under email_hashed.

ip is the opposite. It must be the device IP in the clear. A 64-character hex digest in ip is how geo matching dies:

  error   vendor.appsflyer.body.hashed_plaintext_field
          `ip` looks like a SHA-256 digest, but AppsFlyer documents it as the
          device IP address, unhashed.
Enter fullscreen mode Exit fullscreen mode

The Meta pattern (hash em, leave client_ip_address plain) is close, but the field names differ. The identity guide lists what each vendor hashes and what stays plaintext.

OneLink impressions are a different URL

Click and impression tracking on OneLink uses impression.appsflyer.com with a template id and pid in the query string, not the S2S JSON envelope. That contract is the AppsFlyer OneLink impression pack. Lint the long URL before you wire it into a banner; the S2S pack does not cover it.

If you are linting more than AppsFlyer in CI, the vendor index links every pack.

The short version

Prefix iOS app ids with id in the /inappevent/ path. Send eventTime as 2019-05-15 12:17:01.123 UTC. Hash email into email_hashed. Send ip raw. A 200 from AppsFlyer S2S is not proof the event landed.

Pixellint is independent of AppsFlyer. The rule ids above cite AppsFlyer's S2S docs because that is where the requirements live, not because this is an official tool.

Top comments (0)