Blog · AppsFlyer and Adjust
AppsFlyer vs Adjust is an event name on one host and a dashboard token on the other.
AppsFlyer vs Adjust is one renewal posted at two hosts that do not share a field. AppsFlyer is POST https://api3.appsflyer.com/inappevent/{app_id} with a JSON body: appsflyer_id, eventName, eventValue as a stringified JSON string, and eventTime as yyyy-mm-dd hh:mm:ss.sss in UTC. There is a space between the date and the time. There is no T. Authentication is the app S2S key. One event per request.
Adjust is https://s2s.adjust.com/event with s2s=1, app_token, and event_token on the query. event_token is the short code created when someone defines the event in the Adjust dashboard, paired with that app_token. It is not the string Purchase and it is not af_subscribe. An advertising id such as idfa or gps_adid, or a backup such as idfv, identifies the device. A successful call returns OK. An unknown token returns HTTP 400: the token does not exist, was set incorrectly, or does not match the app token.
The shared worker copies eventName into event_token. AppsFlyer may accept af_purchase. Adjust rejects it. Weekly reviews then say Adjust is broken. The worker never translated the token. Pixellint is not affiliated with either MMP. The failure is the payload, which you can see without either dashboard.
The clocks do not match either
AppsFlyer eventTime is that UTC string. A ten-digit Meta epoch is not a timestamp the endpoint can read as purchase time. If eventTime is omitted, or the value does not match the format, AppsFlyer stamps HTTP arrival. Help is specific about bulk jobs: events from the previous day or earlier that arrive after 02:00 UTC are stamped with the API call time. A nightly job of yesterday's subscriptions lands on the wrong calendar day even when the string is valid. Future times on the next calendar day also fall back to arrival.
Adjust created_at has to stay after installed_at. Bad event state is the ordering error. That check is unrelated to the AppsFlyer format string. A Unix integer that happened to be accepted as created_at on one side is still the wrong eventTime on the other. Keep one UTC instant in the warehouse. Format a string with a space for AppsFlyer. Format the Adjust timestamp the S2S docs show for created_at. Do not send the Meta seconds integer to either.
AppsFlyer can return the body OK on a 200 while the event is not fully recorded. A missing id prefix on an iOS app id in the path is the documented case: digits alone can 200 and record nothing. Android uses the Play package name. 401 is the wrong S2S key. 400 is a missing appsflyer_id, a bad parse, more than one event, or an app id that does not match the token. Grepping for 200 closes the ticket on both the silent AppsFlyer miss and the Adjust 400 you did not classify.
The join keys are different products
AppsFlyer joins on appsflyer_id, generated when the app first launches. If that id is not a real install, the S2S event has nothing to hang on. Adjust joins on the advertising id plus app_token. A worker that only forwards the AppsFlyer column leaves Adjust without a device id even when event_token is finally correct.
Revenue splits the same way. AppsFlyer wants eventValue as a stringified JSON string. An object is not processed as revenue. Adjust documents revenue and currency as their own parameters on the S2S request. Copying the AppsFlyer JSON blob onto the Adjust URL sends eventName into a parser that is looking at query parameters.
Authentication splits too. The AppsFlyer S2S key does not belong on s2s.adjust.com. Adjust does not want that bearer header as its app_token. Each client has its own Adjust app_token, and each event_token is scoped to that app. A universal Purchase string cannot work, which is why a translation table is per app, not per company.
The same renewal, two requests
af_subscribe in event_token is the 400. The AppsFlyer name stays on AppsFlyer.
AppsFlyer: POST /inappevent/id123456789
eventName af_subscribe
eventTime 2026-09-27 18:04:00.000
appsflyer_id from the install
Adjust: GET /event?s2s=1&app_token=<dashboard>&event_token=<dashboard>&idfa=<id>
S2S exists for events the SDK never saw. There is no client echo to compare. Marketing notices weeks later that Adjust cohorts miss renewals AppsFlyer shows. Alert on Adjust 400 Invalid event token as its own signal. Alert on AppsFlyer 200 separately from a recorded-event check, because OK is not the record.
This is also not a SKAN postback. AppsFlyer can record an S2S event and still omit it from SKAN if Conversion Studio is not set to take S2S events. Adjust's 400 does not mean Apple rejected a conversion value. Fix the token table before you debug attribution windows.
Keep the internal event key, then map it. AppsFlyer gets eventName. Adjust gets the dashboard event_token for that app_token. Version the table when someone adds a funnel in either UI. A connector with one conversion-name dropdown is the bug unless the mapping behind it is per vendor.
Revenue numbers and the IP field follow the same split
AppsFlyer revenue inside eventValue has to look like 123 or -123.45. Commas, currency symbols, and currency codes inside that number are illegal. The whole eventValue is a string, not an object. Adjust puts revenue and currency on the query as their own parameters. A stringified AppsFlyer blob will not fill them.
ip on AppsFlyer is the device IP at event time, plaintext. Send it and geo uses it. Omit it and geo falls back to the install, with the IP field empty. Do not hash it because a Meta worker hashed email. email_hashed is SHA-256 after trim and lowercase. phone_number_hashed is digits with a country code. Those two are hashes. ip and the user agent are not. Adjust's device id is the advertising id, also not a hash of the AppsFlyer id.
Payload limit on the AppsFlyer call is 1KB, one event, TLS 1.2 or higher. A batch array copied from a Meta data[] is the 400 for more than one event. Adjust is one event per URL as well. Fan out in your worker. Do not POST one array at either host.
Watch the two status lines as different products
Tag the span with the MMP. Alert when Adjust returns 400 Invalid event token. Alert when AppsFlyer returns 401 for the S2S key. Do not alert only on non-200, because the iOS path without the id prefix is a 200 that recorded nothing. The overview date range in AppsFlyer is install date, not event date. A recorded renewal can look missing if you chart the wrong range, which then gets blamed on Adjust.
Off-app renewals have no SDK echo. The only comparison is the raw response and the raw export. If AppsFlyer shows the subscription and Adjust does not, open the Adjust body before you open attribution settings. The token table is the first explanation. The device id is the second. The clock is the third.
The field posts on event tokens and on eventTime are the contracts. This page is the search: one worker, two MMPs, three fields that do not transfer. Keep the translation table in the repo next to the code that builds each URL.
What does not transfer
- eventName does not fill event_token. event_token is a dashboard code tied to app_token.
- eventTime is yyyy-mm-dd hh:mm:ss.sss UTC. A Meta epoch is arrival time if AppsFlyer cannot parse it.
- appsflyer_id does not fill idfa or gps_adid. Adjust still needs a device id.
- iOS AppsFlyer paths need the id prefix. Digits alone can 200 and record nothing.
- eventValue must be a stringified JSON string. Adjust revenue is its own parameters.
- Adjust 400 names a bad token. AppsFlyer 200 can still mean the row was not recorded.
- Neither call is a SKAN postback.
Sources
Contract pages
The dated argument is above. These pages are the field lists.