AAAttribution.attributionToken(),
POST it to Apple's endpoint, retry the early 404s, store the result on
your user record. If Apple Search Ads is your only paid channel, this
replaces an MMP outright.
First, the fair version of the MMP pitch: AppsFlyer, Adjust, and Branch do real work beyond collecting postbacks. They pull spend from every ad network's API into one cost report, dedupe attribution across networks, run fraud detection, handle deep linking, and model the gaps SKAdNetwork leaves. If you're running Meta, TikTok, and Google campaigns simultaneously, that's genuine infrastructure. But for an indie running Apple Search Ads only (or Apple Ads plus one social network with server-side events), an MMP is overkill: one app, some Search Ads spend, and a single question. Which campaigns and keywords produce subscribers rather than installs? Apple answers that directly, for free, because it owns both the ad and the store. What follows is the whole pipeline, including the parts the framework docs mention quietly.
Step 1: get the token
Import AdServices (iOS 14.3+) and call
AAAttribution.attributionToken(). It's synchronous, it
throws, and two properties matter:
- Tokens are valid for 24 hours. You don't have to resolve attribution in the first session — persist the token and finish the job on a retry.
- It fails on simulators. Expect the throw in development and don't let it crash anything; test the real path on a device.
- It's synchronous and not instant. The call does real work — generate the token off the main thread, or you add a visible hitch to the user's very first launch.
Do this once per install, on or shortly after first launch, and record that you did. Attribution is a fact about the install, not the session — re-resolving it weekly is wasted work and a source of inconsistent data.
Step 2: POST it to Apple
Send the raw token as the body of a POST to
https://api-adservices.apple.com/api/v1/ with
Content-Type: text/plain. Three responses matter:
- 200 — JSON payload, the good case, below.
- 404 — attribution not ready yet. Apple's docs recommend retrying at 5-second intervals, up to three attempts. In practice, also keep the token and retry on the next launch within the 24-hour window; fresh installs sometimes take longer than fifteen seconds to become resolvable.
- 400 — the token itself is invalid (stale, malformed, or simulator-made); regenerate rather than retry.
One architectural note: you can make this call from the device or ship the token to your backend and resolve it there. Server-side wins on every axis that matters — retries survive app kills, you never ship attribution logic updates through App Review, and the result lands next to your analytics data instead of needing another hop.
Step 3: read the payload
Installs Apple can't tie to an Apple Ads campaign return
{"attribution": false}. Be precise about what that means:
not attributed to Apple Ads — not "organic". That user may have
come from a Meta campaign, a Reddit post, or a friend's recommendation;
AdServices only ever confirms or denies Apple's own ads. Record it as
"no ASA attribution" rather than "organic", and rather than leaving a
null you'll misread later. Attributed installs return the IDs behind
the install:
| Field | What it is | Needs ATT? |
|---|---|---|
campaignId | The campaign that won the install | No |
adGroupId | Ad group within it | No |
keywordId | The keyword the user searched | No |
conversionType | Download or Redownload | No |
countryOrRegion | Storefront of the download | No |
clickDate | When the ad was tapped | Yes — consent-only |
Note what's deterministic here: the payload is Apple's answer for this install, joinable to your own user record — not a modeled estimate. Whether you also need the ATT prompt is a separate decision, and it's a short decision tree:
- Apple Search Ads is your only paid channel: skip
the prompt. Everything decision-relevant in that table arrives without
consent; only
clickDateis gated. - You run retargeting, personalized ads, or meaningful paid-social spend: ATT may still matter, because those networks' own targeting and optimization improve with consented users. Decide on their terms, not this table's.
This is the deterministic half of the no-ATT attribution story — the aggregate half (SKAdNetwork, AdAttributionKit, and what they can't tell you) is covered in iOS attribution without ATT.
Mind conversionType: redownloads are returning users, not new
acquisition. If you're computing cost per new subscriber, count
them separately or your best "keyword" may turn out to be your own churned
users coming back.
Step 4: make the IDs mean something
The payload is all numeric IDs — no names. Two ways to map them:
- The Apple Search Ads Campaign Management API, which can list campaigns, ad groups, and keywords with their IDs. Correct, and worth it once campaigns churn often.
- A lookup table you maintain by hand. At indie scale (a handful of campaigns, tens of keywords), five minutes in a text file covers it, and the IDs are stable once created.
Either way, store the raw IDs on the user record and resolve names at display time. Renaming a campaign shouldn't rewrite history.
Step 5: join it to revenue
Campaign IDs on a user record are only interesting next to what that user did and paid. That requires behavior and purchases on the same record — the one-ID pattern, with the attribution fields stored as user properties. Once the join exists, the questions that steer spend become plain queries: trial rate per keyword, revenue per campaign, whether brand-term installs subscribe better than generic-term ones. Without the join you know which keywords produce installs, which the App Store was already telling you.
The end state worth building toward is cost per subscriber, not cost per install. Spend lives in the Apple Search Ads console (or its reporting API) per campaign and keyword; your attribution data provides the denominator that matters (subscribers, not installs) per the same IDs. Divide monthly, at indie volume: weekly cost-per-subscriber numbers on a few hundred euros of spend are coin flips, and a keyword needs a batch of attributed installs before its conversion rate means anything. The pleasant surprise once this runs: keywords with expensive installs are often the cheapest subscribers, because search intent quality varies far more than tap prices do. That inversion is invisible in the ASA console alone — it only shows up after the revenue join.
The gotcha list
- One resolution per install, persisted. Not per launch.
- Attribution looks back at the ad interaction window — an install can attribute to a click from weeks earlier, so a campaign you paused last Tuesday still produces attributed installs for a while. Don't panic-unpause.
- TestFlight and simulator installs won't attribute. Fail soft and flag them internal — a handful of your own test devices can bend indie-scale numbers, attribution included.
- Log the failure cases distinctly. "Not attributed to Apple Ads", "token failed", and "gave up after retries" are three different facts; collapsing them into one bucket hides integration bugs.
FAQ
Does this work without the ATT prompt?
Yes — everything except clickDate arrives without consent.
Campaign, ad group, keyword, conversion type, and storefront are all in
the standard payload.
Can AdServices attribute Meta or TikTok installs?
No. It's Apple Search Ads only. Other networks (provided they've registered with Apple) go through SKAdNetwork or AdAttributionKit, which return up to three aggregate postbacks per install, gated by conversion activity and crowd anonymity, with no user attached.
Why do I get 404s for a valid token?
Data isn't ready yet. Retry per Apple's guidance (5-second intervals, three attempts), then again on a later launch inside the token's 24-hour validity. A 404 that never resolves usually means the install isn't attributable to Apple Ads — not necessarily organic; AdServices can't see other channels.
How do I get names instead of numeric IDs?
Campaign Management API, or a hand-kept lookup table at indie campaign counts. Store IDs, resolve names at display time.