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
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
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`.
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
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"
}
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`.
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.
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.
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)