AppsFlyer
Adapty exchanges data with AppsFlyer in both directions. AppsFlyer tells Adapty which campaign brought each user. Adapty tells AppsFlyer what that user paid — purchases, renewals, trials, and refunds, with revenue and product details attached.
- View the whole subscription lifecycle, not just the first purchase. Renewals and trial conversions are store events, with no app session for AppsFlyer’s client-side SDK to report. Adapty receives subscription events server-side and forwards them to AppsFlyer, so campaign figures keep updating long after the initial install. Refunds travel the same way, and AppsFlyer deducts them from campaign revenue.
- Optimize your ad campaigns with subscription event data. AppsFlyer relays in-app events from Adapty onward to your ad networks as postbacks. The services that manage your ad budget can optimize on what users actually paid.
- Filter Adapty analytics by campaign. Adapty saves AppsFlyer’s attribution on each profile, down to the ad set and creative, and subscription charts can filter by it.
- Show a different paywall per campaign. Adapty segments filter on the same attribution fields — campaign, ad set, and creative. Use a segment as an audience to match the paywall to the ad that brought the user.
Your Adapty account already includes two tools for paid campaigns. Adapty Ads Manager runs your Apple Ads campaigns; Adapty Attribution covers Meta Ads and TikTok. Both report ROAS and LTV straight from your Adapty purchase data, and both are free to start — see pricing.
How the integration works
Adapty receives attribution data from AppsFlyer, and sends subscription events back to it. Both depend on one value: the AppsFlyer ID, a string AppsFlyer generates when your app first launches.
- When the user installs your app, the AppsFlyer SDK assigns them a unique ID.
- Your app passes that ID to Adapty, which stores it on the user’s profile as
appsflyer_id. - Your app also passes AppsFlyer’s attribution data to Adapty, which saves it on the same profile.
- Later, when the user triggers a subscription event — for example, starts a trial or buys a product, Adapty’s servers post the event to AppsFlyer’s S2S API with the same
appsflyer_id. - AppsFlyer matches the ID to the install it already attributed, so the purchase inherits that install’s campaign and media source.
Setup instructions
Before you start:
- Confirm your AppsFlyer plan accepts S2S in-app events. AppsFlyer’s entry-level Zero package doesn’t: its API rejects every event Adapty sends with
403 Forbidden. - Ship the AppsFlyer SDK in your app. The AppsFlyer ID that the integration depends on exists only after that SDK initializes. A server-side integration alone won’t produce the ID.
- Turn off every other attribution integration. Adapty accepts one attribution source per profile and can’t overwrite an existing value. On iOS, non-organic Apple Ads attribution always takes priority — see Select a single attribution source.
Create an S2S token in AppsFlyer
Adapty authenticates against AppsFlyer’s S2S API with a token you create. Only AppsFlyer admins can open the Tokens page, so ask one to create the token if your account isn’t an admin. Skip to Configure Adapty if you already have one.
-
Sign in to AppsFlyer.
-
Click your account name in the top-right corner, and open the Security center.
-
On the Manage your account security page, find the AppsFlyer API and S2S tokens card and click Manage your AppsFlyer tokens. The Tokens page opens.
-
Click New token.
-
Enter a Name for the token. The name is for your reference only, and you can change it later.
-
Select the S2S token type. Any other type breaks the integration.
-
Click Create new token.
NoteAppsFlyer allows two tokens per type. If your account already has two S2S tokens, reuse one of them.
-
Find your new token in the list. AppsFlyer masks the value, so click the copy icon in the Token column to retrieve it.
Configure Adapty
-
Open Integrations > AppsFlyer in the Adapty Dashboard.
-
Enable the AppsFlyer toggle.
-
If you already connected your app to the App Store, the iOS App ID field is automatically populated with your app’s numerical Apple ID. If it’s empty, connect your App Store account first. Android requires no equivalent field.
-
Paste the S2S token into the Production field of S2S key for iOS, S2S key for Android, or both.
-
Fill in the Sandbox fields to keep test purchases out of your production numbers — see Keep sandbox data out of production.
-
Under How the revenue data should be send, choose which revenue figure Adapty sends as
af_revenue. The three options match the Adapty Analytics revenue views, so your choice also names the view your AppsFlyer numbers should agree with.Option What Adapty sends Gross revenue The full amount the buyer paid, before commission and tax. The default. Proceeds after store commission The amount minus the store’s commission, with tax still included. Proceeds after store commission and taxes The amount minus both. -
Set the remaining options:
Toggle When on Default Report user’s currency Adapty reports each sale in the currency the buyer paid in, rather than USD. Off Send trial price Trial starts carry no revenue otherwise. Turn this on to give each one a placeholder price, and a Trial price percentage field appears — set it to the share of the subscription price a trial should report. At 60%, a $10 subscription sends $6.Off Exclude historical events Adapty skips events that happened before the user installed a build containing the Adapty SDK. On Delay events with a future datetime Apple reports renewals and trial conversions in advance, so these events carry a future date. AppsFlyer usually replaces that date with the day the event arrives. Turn this on to hold each event until its date — see Renewals land on the wrong day. Off -
Rename or disable individual events in the Events names section — see Event names.
-
Click Save.
Keep sandbox data out of production
To keep test purchases out of your real numbers, send them to a separate AppsFlyer app. Register a second app for your development builds, then paste its token into the Sandbox field of each platform section.
Adapty routes every transaction by environment — real purchases to the app whose token is in Production, test purchases to the app in Sandbox. To report everything in one app instead, paste the same token into both fields.
App Store review and TestFlight purchases are sandbox transactions, even though they run on a production build. Adapty sends these purchases with the Sandbox key.
Sandbox transactions are excluded from all analytics charts. They still appear on individual profile pages and in the event feed.
Configure your app code
- Register a conversion callback with the AppsFlyer SDK. On iOS, implement the
AppsFlyerLibDelegateprotocol; on Android, theAppsFlyerConversionListenerinterface; on Unity, theIAppsFlyerConversionDatainterface. On React Native and Flutter, pass a handler to theonInstallConversionDatamethod instead. - Wait for AppsFlyer to invoke that callback. AppsFlyer attributes each install on its own servers, so the result reaches your app asynchronously rather than at launch. The SDK invokes the callback again on every later session.
- Inside the callback, read the user’s AppsFlyer ID with
getAppsFlyerUIDand pass it to Adapty withsetIntegrationIdentifier(). Adapty’s events reach the right AppsFlyer user only with this value. - Inside the same callback, pass AppsFlyer’s attribution data to Adapty with
updateAttribution(). This tells Adapty which campaign produced the install. On iOS and Android SDK 4.1 and later, the method is namedupdateExternalAttribution(). - Await
Adapty.identify()instead of running it alongside steps 3 and 4. Adapty creates an anonymous profile at activation, then switches to the identified profile onceidentify()resolves. Anappsflyer_idset during that switch doesn’t always survive it.
For the full sequence, see call order in the iOS, Android, React Native, Flutter, Unity, Capacitor, and Kotlin Multiplatform SDKs.
Third-party SDKs generate user IDs asynchronously. The ID may not be ready when Adapty.activate() runs. If your Customer User ID comes from one of these SDKs, call Adapty.activate() without it. Once the ID arrives, call setIntegrationIdentifier(), then identify() with the CUID.
Verify the integration
- Trigger a sandbox purchase and open your app’s Event Feed. Every delivery attempt appears there. To read AppsFlyer’s response to a failed attempt, hover its row.
- In AppsFlyer, open Settings > SDK Integration Tests > Live Events and select your test device. Live Events lists S2S events as they arrive, well before they reach AppFlyer’s Activity dashboard.
- Open your app’s Activity dashboard and confirm the event, its revenue, and its media source. Allow about an hour — S2S events don’t reach this dashboard immediately.
Adapty’s events never appear in your app’s AppsFlyer SDK debug log. Adapty posts them from its own servers, so they never pass through your app. An empty local log says nothing about the integration.
AppsFlyer event structure
Adapty sends one POST request per event to https://api3.appsflyer.com/inappevent/{app_id}, with the S2S token in the authentication header. API 2 uses https://api2.appsflyer.com/inappevent/{app_id}.
{
"appsflyer_id": "1699887556000-6192770",
"eventName": "af_subscribe",
"eventTime": "2026-03-01 12:00:00",
"eventValue": "{\"af_content_id\":\"yearly.premium.6999\",\"af_order_id\":\"GPA.3383-4699-1373-07113\",\"store_country\":\"US\",\"profile_country\":\"US\",\"af_content_type\":\"in_app\",\"af_revenue\":\"9.9900\",\"af_currency\":\"USD\",\"af_quantity\":\"1\"}",
"os": "17.0.1",
"bundleIdentifier": "com.example.app",
"customer_user_id": "user_12345",
"eventCurrency": "USD",
"ip": "192.168.100.1",
"advertising_id": "00000000-0000-0000-0000-000000000000",
"idfa": "00000000-0000-0000-0000-000000000000",
"idfv": "00000000-0000-0000-0000-000000000000",
"att": "3"
}
| Parameter | Type | Description |
|---|---|---|
appsflyer_id | String | The AppsFlyer ID your app passed to setIntegrationIdentifier. AppsFlyer matches the event to an install by this value. |
eventName | String | The name from the Events names section — see Event names. |
eventTime | String | When the event occurred (UTC, YYYY-MM-DD HH:MM:SS). Adapty replaces it with the current time for events older than 26 hours — see Old events arrive with today’s date. |
eventValue | String | A JSON-encoded string of the fields in the table below. |
os | String | The OS version of the user’s device. |
bundleIdentifier | String | The app’s bundle ID on iOS, or package name on Android. |
customer_user_id | String | The user’s Customer User ID. |
eventCurrency | String | The ISO 4217 currency code, for example USD. |
ip | String | The user’s IP address. |
advertising_id | String | Android only. The Google Advertising ID. |
idfa | String | iOS only. The ID for Advertisers. |
idfv | String | iOS only. The ID for Vendors. |
att | String | iOS only. The App Tracking Transparency status, 0 to 3. Adapty sends 0 when it has no value. |
eventValue carries the purchase itself. The last four parameters appear only on events that carry revenue:
| Parameter | Type | Description |
|---|---|---|
af_content_id | String | The product ID from the store. |
af_order_id | String | The original transaction ID. |
store_country | String | The country of the user’s store account. |
profile_country | String | The country Adapty derived from the user’s IP address. |
af_content_type | String | Always in_app. |
af_revenue | String | The revenue amount, to 4 decimal places. Negative on refunds. |
af_currency | String | The currency of af_revenue. |
af_quantity | String | Always 1. |
Event names
Adapty maps its revenue-carrying events to AppsFlyer’s standard event names by default, rather than sending its own names as custom events. This matters if you forward events to ad networks: a network acts on the standard names it already recognizes, so you skip inventing a mapping for each one.
| Adapty event | Default AppsFlyer name |
|---|---|
| Subscription started | af_subscribe |
| Subscription renewed | af_subscribe |
| Trial converted | af_subscribe |
| Trial started | af_start_trial |
| Non-subscription purchase | af_purchase |
Every other Adapty event keeps its own name, for example subscription_refunded. In the Events names section of the AppsFlyer integration page, rename any event or turn off the ones you don’t need. For the full list of what Adapty can send, see Events.
Limitations
- A profile without
appsflyer_idproduces no event. AppsFlyer matches an event to the install that generated the ID, and reads the campaign from that install. Adapty sends nothing without it, and the Event Feed flags these profiles — see Events don’t reach AppsFlyer. - No historical backfill. Adapty forwards events from the moment you enable the integration. Past purchases never reach AppsFlyer.
- Device details stay empty in AppsFlyer raw data. An S2S event carries only what the request contains. Device Model, Device Category, Language, Operator, WIFI, App Version, and App Name have no S2S parameter, so nothing sent this way can populate them.
Troubleshooting
- Events don’t reach AppsFlyer
- Purchases show up as organic
- Old events arrive with today’s date
- Renewals land on the wrong day
- Revenue in AppsFlyer doesn’t match Adapty Analytics
Failed to authenticatein the Event Feedaccess_level_updatedshows as failed in the Event Feed
Events don’t reach AppsFlyer
Open the Event Feed first. A failed delivery shows the error AppsFlyer returned. These causes account for most of them:
- The profile has no
appsflyer_id. Confirm your app callsgetAppsFlyerUIDand passes the result tosetIntegrationIdentifieron every platform you ship on — see Configure your app code. - The app ID for that platform is missing. AppsFlyer addresses each app by ID, so Adapty sends nothing without one. iOS events need iOS App ID on the integration page; Android events need the package name from App settings > Android SDK — see Configure Adapty.
- The purchase is a sandbox transaction and the Sandbox key is empty. See Keep sandbox data out of production.
- The event is turned off in the Events names section.
A successful delivery doesn’t guarantee AppsFlyer keeps the event. AppsFlyer returns 200 OK for any well-formed request, then discards events whose appsflyer_id matches no real install.
Purchases show up as organic
Each appsflyer_id identifies one install, and every event carrying that ID inherits the install’s attribution — the campaign that produced it, or none if the install was organic. AppsFlyer needs 20–30 seconds or more to attribute a new install. An event that arrives before then has no attribution to inherit, so AppsFlyer marks it unattributed organic.
Expect first-launch purchases to report as organic — trials that start within seconds of app launch. To avoid it, delay your paywall long enough for AppsFlyer to finish processing the install.
If every event looks organic, not just the occasional first-launch purchase, the cause is attribution rather than timing: another source claimed the profile first. See Select a single attribution source.
Old events arrive with today’s date
Events may reach Adapty late for two reasons:
- When App Store Server Notifications show Delayed in App Store Connect, Apple queues its notifications. A renewal then reaches Adapty long after it happened — see App Store Server Notifications show “Delayed”.
- Back-dated events pass through whenever Exclude historical events is off.
AppsFlyer won’t accept old timestamps such as these. To ensure consistent event handling, Adapty replaces eventTime with the current time for any event older than 26 hours. You can’t disable the rewrite.
Renewals land on the wrong day
Apple notifies Adapty of renewals and trial conversions before they happen, and Adapty forwards them immediately with the future eventTime unchanged. AppsFlyer keeps a future timestamp only when it falls on the arrival day — a renewal dated tomorrow gets stamped with today’s arrival time instead.
Turn on Delay events with a future datetime in the integration settings. Adapty then holds each event until its time arrives, so AppsFlyer records the date Apple announced. See Event timestamps with future dates.
Revenue in AppsFlyer doesn’t match Adapty Analytics
Adapty and AppsFlyer count the same purchases differently. These differences account for almost every mismatch.
- AppsFlyer’s Overview dashboard groups revenue by install date; Adapty’s charts group it by event date. In AppsFlyer, a July renewal from a January install counts toward January. Compare that dashboard against Adapty’s cohort analysis, which also groups revenue by install month. AppsFlyer’s raw data reports group by event date, so compare those against Adapty’s charts instead.
- Purchases from profiles without
appsflyer_idnever reach AppsFlyer. They stay in Adapty Analytics. See Events don’t reach AppsFlyer. - AppsFlyer shows only the revenue figure you selected in Adapty. The How the revenue data should be send setting decides whether Adapty sends gross revenue, proceeds, or net revenue. Comparing that against another Adapty Analytics view produces a gap equal to the commission, the tax, or both.
- Adapty uses your app’s reporting timezone; AppsFlyer receives UTC. Integrations always get UTC timestamps, whatever you set in App Settings. A purchase at 23:30 UTC on Jul 1 lands on Jul 2 in Adapty if your reporting timezone is +02:00.
- AppsFlyer is missing historical events. There are two probable causes. With Exclude historical events on, a user’s AppsFlyer history starts at their first launch of an Adapty build. Adapty also never backfills events it processed before you enabled the integration.
- Sandbox purchases reach the AppsFlyer app named by the Sandbox key. When that’s the production app, test purchases inflate its revenue. See Keep sandbox data out of production.
- Events turned off in the integration never reach AppsFlyer. Adapty Analytics still counts them. Review the Events names section — turning off
subscription_renewedremoves most of the revenue for any established app.
Failed to authenticate in the Event Feed
AppsFlyer rejects the credentials when they don’t match the API version. API 2 expects a Dev key; API 3 expects an S2S token. Switching the version without replacing the key produces this error on every event.
Create a fresh token in the AppsFlyer Security Center and paste it into the S2S key fields, or follow Switch from AppsFlyer S2S API 2 to 3.
access_level_updated shows as failed in the Event Feed
access_level_updated is a webhook-only event. Adapty never sends it to this integration. But Adapty records an outcome for every enabled integration, and an unsupported event is displayed as a failure.