pixellint

Blog · Adjust S2S

Adjust S2S Event Tokens Are Not AppsFlyer Event Names

https://s2s.adjust.com/event expects event_token and app_token query parameters tied to dashboard tokens, not a JSON eventName string. The multi-tenant worker most teams already run for AppsFlyer S2S still maps purchase rows to eventName Purchase.

That mapper looked reasonable when only one MMP was live. Adjust documents event_token as the Adjust event token from the dashboard, alongside app_token and s2s=1 on the same GET or POST URL. AppsFlyer api3.appsflyer.com/inappevent/{app_id} wants a JSON body with eventName and appsflyer_id. The names sound interchangeable in standups. On the wire they are different products.

When the shared worker reuses the AppsFlyer branch and writes Purchase into event_token, Adjust returns Invalid event token: the token does not exist, was set incorrectly, or does not match the app token. The renewal or off-app purchase never attributes. That failure is the news, not a new lint rule.

Two hosts, two identity models

AppsFlyer S2S joins on appsflyer_id generated at first app open. The Send Event reference lists eventName, eventTime as yyyy-mm-dd hh:mm:ss.sss UTC, and stringified eventValue JSON. Authentication is the S2S key in the header for that app id path.

Adjust S2S joins on advertising ids such as idfa or gps_adid, or backup ids like idfv, plus the app_token that identifies the Adjust app configuration. event_token is not a human-readable name. It is a short code created when you define an event in the Adjust dashboard, paired with that app_token.

A data warehouse column named event_name flowing from analytics into both vendors is where the bug starts. ETL copies Purchase into whatever field the worker calls generic event field. Adjust never sees a token that maps to a configured event.

Hybrid stacks that added Adjust after AppsFlyer often keep one protobuf or Avro schema with event_name as the canonical string. Without a translation table per vendor, the Adjust branch posts marketing language into a parameter that only accepts dashboard codes.

What Adjust counts as a valid S2S event

Adjust event submission documentation lists required parameters: an advertising id, event_token, app_token, and s2s set to 1. Example curl calls use event_token=f0ob4r and app_token=4w565xzmb54d on https://s2s.adjust.com/event.

Optional revenue, callback_params, and partner_params ride on the same request. created_at timestamps must stay after installed_at; Bad event state fires when clocks violate that ordering. Those checks are Adjust-specific and unrelated to AppsFlyer eventTime string rules.

Successfully recorded events return OK. Failures return HTTP 400 with JSON error text. Invalid event token is explicit: the token does not exist, was set incorrectly, or does not match the app token. That is clearer than AppsFlyer 200-without-record cases, but only if someone reads the body.

Teams grep logs for 200 and assume the MMP recorded the row. Adjust 400 responses may land in a generic error bucket while AppsFlyer continues to accept the same mislabeled payload on the other branch, which hides the split-brain configuration.

Where shared workers break in production

Web billing is the common fracture. A subscription renews in Stripe. The worker posts AppsFlyer S2S with eventName af_subscribe and a UTC eventTime string. The Adjust branch copies af_subscribe into event_token. Adjust has no token with that spelling; the event is rejected.

Agency templates make it worse. White-label scripts parameterize host and app id while leaving field names from the first MMP integration. Adjust app tokens differ per client, and each event_token is scoped to that app. A universal Purchase string cannot work even if Adjust allowed friendly names, which it does not.

Server-side GTM and CDP connectors sometimes expose a single conversion name dropdown. Mapping that label to both vendors without per-vendor token tables produces exactly one silent Adjust drop per fired tag.

Off-app events amplify the mistake because neither SDK sees the transaction. There is no client-side echo to compare against. Marketing discovers the gap weeks later when Adjust cohort reports miss renewals AppsFlyer shows.

Why dashboards disagree after a migration

AppsFlyer and Adjust both show event counts in polished UI, but they count different objects after a misfire. AppsFlyer may stamp arrival time on a malformed eventTime while still returning OK. Adjust returns 400 Invalid event token and records nothing. Side-by-side weekly reviews look like Adjust is broken when the worker never translated tokens.

Device id requirements diverge as well. Adjust S2S documentation lists idfa, gps_adid, fire_adid, oaid, or web_uuid as preferred identifiers, with idfv and android_id as backup paths. AppsFlyer centers appsflyer_id. A worker that only forwards the MMP id from the AppsFlyer column leaves Adjust without a join key even when event_token is correct.

Authentication differs too. Adjust event submission uses query parameters on s2s.adjust.com with s2s=1, while AppsFlyer uses a bearer S2S key on api3.appsflyer.com. Reusing the AppsFlyer header on Adjust is another silent branch failure that shows up only in integration tests if those tests exist.

The operational fix is to treat each vendor as a separate micro-endpoint in observability: tag spans with mmp=adjust versus mmp=appsflyer, log response bodies, and alert on Adjust 400 Invalid event token separately from AppsFlyer 401. Without that split, on-call greps HTTP 200 on the wrong host and closes the ticket.

AppsFlyer JSON shape that must not map to event_token

eventName is a string label on the AppsFlyer path. Adjust expects event_token from the dashboard, not this field name.

POST https://api3.appsflyer.com/inappevent/id123456789
Authorization: Bearer <S2S_KEY>
{
  "appsflyer_id": "1234567890123-1234567",
  "eventName": "af_purchase",
  "eventTime": "2026-09-25 14:30:00.123",
  "eventValue": "{\"af_revenue\":\"29.99\",\"af_currency\":\"USD\"}"
}

If your Adjust integration JSON-stringifies this body onto s2s.adjust.com/event, Adjust parses query parameters, not AppsFlyer envelope fields. eventName is ignored or misinterpreted while event_token stays empty or wrong.

Even a field rename from eventName to event_token without looking up the Adjust dashboard code sends Purchase into a parameter that must be f0ob4r-style tokens tied to the app_token.

Revenue parameters differ as well. AppsFlyer nests revenue inside stringified eventValue. Adjust documents separate revenue fields on the S2S request. Copying JSON blobs across vendors without reading each spec leaves money on one platform only.

Adjust S2S request after translation

Illustrative query shaped to Adjust S2S event submission docs; replace tokens with your dashboard values.

GET https://s2s.adjust.com/event?s2s=1&app_token=4w565xzmb54d&event_token=f0ob4r&idfa=8A3CB124-5A79-4334-8802-F75FEC099C58&revenue=29.99&currency=USD&created_at=2026-09-25T14:30:00Z

Translation belongs in the worker contract

Maintain a table from internal event keys to AppsFlyer eventName strings and Adjust event_token codes per app_token. Version that table when marketing adds funnels in either dashboard.

Test Adjust with intentional Invalid event token by posting a bogus code and confirm alerting fires. Then post a known good token from the dashboard and verify OK in Adjust raw data.

Do not rely on MMP UI success messages from a connector that only validates AppsFlyer. Each vendor endpoint needs its own response classification.

When migrating vendors, freeze the shared worker name. capi-forwarder and mmp-unified are warnings that someone assumed one payload fits all hosts.

How this differs from the AppsFlyer clock post

AppsFlyer S2S eventTime as UTC string versus Meta epoch is a clock dialect problem on the same eventName field. This post is identifier dialect: event_token is not eventName even when both mean purchase in a spreadsheet.

Fixing clocks on the AppsFlyer branch does not repair Adjust. Fixing Adjust tokens does not help AppsFlyer if eventTime is still Unix seconds. Multi-MMP workers need two translation layers, not one.

Pixellint checks payload shape against documented contracts. It will not look up your Adjust dashboard for valid tokens. It will flag missing s2s=1, empty event_token, or JSON posted where query parameters belong, which is enough to catch the copy-paste before renewals silently stop.

Adjust versus AppsFlyer S2S checklist

Sources

Contract pages

The dated argument is above. These pages are the field lists.

Read the MMP S2S field guide Docs