Blog · AppsFlyer S2S
AppsFlyer S2S eventTime is a UTC string. A Meta epoch is how the subscription never attributes.
api3.appsflyer.com/inappevent/{app_id} wants appsflyer_id, eventName, and eventTime as yyyy-mm-dd hh:mm:ss.sss UTC. The first S2S worker most stacks will ship is a Meta CAPI clock on that path.
That paste looks close enough to ship. There is an event name, a clock, a user id, a revenue object. Every name is different. The host is api3.appsflyer.com, not graph.facebook.com. The join key is appsflyer_id, not fbclid. The clock is a UTC string with a space, not event_time in seconds. iOS app ids in the path need the id prefix. Digits alone can still 200.
S2S is for events the SDK never saw: a web renewal, a call-center charge, a billing system that never opened the app. It is not a SKAN postback and not Meta CAPI. A Meta-shaped body on the AppsFlyer host is how the subscription never attributes. The request can still look live. That is the news, not a new validator feature.
The clock is a UTC string, not an epoch
AppsFlyer documents eventTime as the time the event occurred, in UTC, formatted yyyy-mm-dd hh:mm:ss.sss. The example on the API page is 2019-05-15 12:17:01.123. There is a space between date and time. There is no T. There is no Unix integer. Date.now() is milliseconds. Math.floor(Date.now() / 1000) is the Meta helper. Neither string is the format this endpoint wants.
If you omit eventTime, AppsFlyer sets the time to HTTP arrival. If you send a value that does not match the format, Help says the event is stamped with arrival time. A ten-digit Meta epoch in that field is not a timestamp AppsFlyer can read as purchase time. The POST can still return OK. The renewal then sits on the day the worker ran, not the day the user paid.
Arrival stamping is not a footnote. Help is specific about bulk S2S: events that took place the previous day or earlier, arriving after 02:00 UTC, are stamped with the time of the API call. A nightly job that posts yesterday's subscriptions after that cutoff will land on the wrong calendar day even when the string is valid. Future eventTime values on the next calendar day also fall back to arrival. The clock you send only sticks when the format is right and the arrival window AppsFlyer documents is right.
iOS ids need the id prefix. A 200 is not a record.
The path is POST https://api3.appsflyer.com/inappevent/{app_id}. Android uses the Play package name, com.example.myapp. iOS uses the App Store id with an id prefix: id123456789. The overview page says failure to prefix iOS with id results in a valid 200 OK without recording the event. The Send Event page repeats it: if you don't, the request returns a 200 OK status but doesn't record the event.
That is a different miss from a 401. 401 is the authentication header: the key is not the S2S key for this app. 400 is missing appsflyer_id, a bad parse, more than one event in the payload, or an app_id that does not match the token. 200 is minimum validation on receipt. The response example is the string OK. The docs say you can get that OK even though the event may not completely record in AppsFlyer.
People google AppsFlyer S2S 200 and treat it like Graph's events_received. It is not. A digits-only iOS path, an invalid eventTime, a missing install for that appsflyer_id, or a payload that never stringified eventValue can all look live in the worker logs. The dashboard date range for the overview is install date, not event date. Looking at the wrong range is how a recorded event also looks missing.
S2S is the event the SDK never saw
The Send Event page is explicit about the job: send mobile events that occur outside the app from your servers. The example is a user who renews a subscription on the web interface. AppsFlyer then populates the event with values you sent plus install attribution it already has, such as install time and media source. The join is appsflyer_id, generated when the app launches for the first time. If that id is not a real install, the event has nothing to hang on.
This is not Apple's SKAN postback. SKAN Conversion Studio has a separate setting to record in-app events sent by the S2S API. If that toggle is off, S2S events can exist in AppsFlyer and still never appear in SKAN measurement. The S2S endpoint does not become a SKAN hop because you posted a purchase. Apple still owns the postback. Posting S2S is not sending a conversion value to Apple.
This is not Meta CAPI either. Meta wants data[] with event_name, event_time in seconds, and user_data. AppsFlyer wants a flat body with appsflyer_id, eventName, eventValue as a JSON string, and eventTime as that UTC string. Auth is the authentication header with the app's S2S key, not a Graph access token. One event per request. Payload up to 1KB. TLS 1.2 or higher. A shared CAPI JSON pointed at a new host fails here the same way it fails on every other vendor.
The body people will send first
A Meta epoch, an object eventValue, a hashed IP, and an iOS path without the id prefix. AppsFlyer can still return 200.
POST /inappevent/123456789
{
"appsflyer_id": "1234567890123-1234567",
"eventName": "af_subscribe",
"eventTime": 1753142400,
"eventValue": { "af_revenue": "9.99" },
"ip": "64ec88ca..."
}
That object is a legal-looking JSON event. Posted to api3.appsflyer.com it is the wrong clock, the wrong eventValue type, a hashed IP, and on iOS the wrong path. The endpoint can still 200. Help is blunt about revenue: eventValue must be a stringified JSON string. If it is an object, the event value is not processed correctly and revenue is not recorded. Valid revenue looks like 123 or -123.45. Commas, currency signs, and codes in that number are illegal.
ip is the device IP at event time, plaintext. If you send it, AppsFlyer uses it for geo. If you omit it, geo falls back to the install event and the IP field is empty. Do not hash it because a Meta helper hashed em. email_hashed is SHA-256 after trim and lowercase. phone_number_hashed is digits with country code, no symbols. Those are hashed fields. ip and ua are not.
Names that do not translate
- Meta event_time in seconds is AppsFlyer eventTime as yyyy-mm-dd hh:mm:ss.sss UTC.
- Graph data[] is a single S2S object. One event per request.
- event_name Purchase is whatever eventName the marketer mapped, often af_subscribe or af_purchase.
- user_data.em is email_hashed. client_ip_address is ip, plaintext.
- iOS app ids in the path are idXXXXXXXX. Digits only can 200 and not record.
- The authentication header is the S2S key, not the AppsFlyer API token and not a Graph token.
- S2S is not a SKAN postback. It is not Meta CAPI, even when the partner is metaweb_int.
ua and ip are required when the partner is Meta
Help has a separate rule for S2S events that AppsFlyer will send on to Meta: when the destination is the metaweb_int endpoint, the payload must include ua and ip. Meta needs those values to optimize via CAPI. The SDK fills them automatically. S2S does not. Advertisers have to put them on the request. Hashing ip, or leaving ua off because Graph hashed the match keys, is how the Meta-side hop gets a body AppsFlyer cannot use.
Device identifiers are a different miss. advertising_id (GAID) or idfa, where you have them, matter for SRN postbacks and incrementality. The docs say incrementality measurement on S2S requires the advertising ID in the payload. ATT on iOS 14+ belongs in att. If you omit os on iOS, AppsFlyer treats the device as 14.5. That is not a default you want on a 2026 iOS build. sharing_filter can block postbacks to partners; it does not fix a bad clock.
Partner postbacks are a separate mapping. S2S records the event inside AppsFlyer. Whether a media source sees it depends on in-app event postback configuration: this partner only versus all media sources including organic. Fixing the S2S body does not turn on a partner mapping you never saved. The two screens disagree, and both look internally consistent.
iOS apps: Ensure to prefix with id, for example id123456789. If you don't, the request returns a 200 OK status but doesn't record the event.
AppsFlyer S2S Send Event
What to do
Keep one internal event: name, UTC instant, order id, appsflyer_id, raw IP, user-agent. At the edge, emit the AppsFlyer envelope. Format eventTime as yyyy-mm-dd hh:mm:ss.sss in UTC. Stringify eventValue. Prefix iOS app ids with id. Put the S2S key in the authentication header. Persist Meta event_time and AppsFlyer eventTime as different columns. Do not point a shared CAPI JSON at api3.appsflyer.com.
Check the path before you check the dashboard. Lint the body you actually POST, including the URL. A 200 is minimum validation, not a join. Use a real appsflyer_id from an install. If the renewal is a web event for People-Based Attribution, that is a different host (webs2s.appsflyer.com) with a different schema. This post is the mobile in-app S2S hop.
The contract page for MMP S2S is the docs. This post is the market fact: subscription renewals still leave the app, and the dialect is not Graph. Pixellint is not affiliated with AppsFlyer or Meta. Passing a linter means the artifact matches the published envelope. It does not mean AppsFlyer attributed the renewal, and it does not detect fraud.
Checklist
- POST to https://api3.appsflyer.com/inappevent/{app_id} with the S2S key in the authentication header.
- iOS app_id is idXXXXXXXX. Android is the Play package name.
- Send appsflyer_id, eventName, and eventValue as a JSON string. One event per request, under 1KB.
- eventTime is yyyy-mm-dd hh:mm:ss.sss UTC. Do not send a Unix integer. Arrive before the Help stamping cutoff if you care about event day.
- Send ip and ua plaintext. Required when AppsFlyer will forward to metaweb_int.
- A 200 OK is not proof the event recorded. Check raw data on the event date, not only the overview on the install date.
Sources
Contract pages
The dated argument is above. These pages are the field lists.