Attribution · how-to

Apple Search Ads attribution: AdServices without an MMP

FolioKit · July 17, 2026 · from building it into the FolioKit SDK

Short answer: the AdServices framework gives you deterministic Apple Search Ads attribution (campaign, ad group, and keyword) with one token request and one API call, no ATT prompt, no AppsFlyer. Get the token via 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:

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:

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:

FieldWhat it isNeeds ATT?
campaignIdThe campaign that won the installNo
adGroupIdAd group within itNo
keywordIdThe keyword the user searchedNo
conversionTypeDownload or RedownloadNo
countryOrRegionStorefront of the downloadNo
clickDateWhen the ad was tappedYes — 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:

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:

  1. 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.
  2. 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

Where FolioKit fits: the SDK runs this whole pipeline by default (token, retries, payload, attribution stored on the same user record as behavior and RevenueCat revenue), so per-keyword subscriber math is a dashboard view, not a weekend project. No MMP, no ATT prompt, no fingerprinting. Specifics in the SDK docs.

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.

FolioKit is analytics for indie iOS developers — Apple Search Ads attribution built into the SDK and joined to RevenueCat revenue on one user record, so "which keyword pays for itself" is a query, not a spreadsheet.

Get early access