You write a landing page or a blog post, people tap "Download on the App Store", and some of them install. A month later you want to know which page brought the users who stayed and paid. On Android the app can ask Google Play's Install Referrer API where the install came from. iOS has no equivalent, and the obvious workaround, matching visitors by their device signals, is the thing Apple forbids. This post covers what each available method sees, then a method we use on our own landing pages that stays first-party, keeps almost nothing, and says how sure it is.
Why iOS makes this hard
- Nothing passes through the App Store. The tap on your page opens the App Store; the install and the first launch happen later, and the app receives no URL, page or campaign parameter from that trip.
- The app can't read Safari's storage. The visitor ID your page keeps in a cookie or localStorage stays in Safari. The app starts with its own empty container.
- No shared device ID. The advertising identifier exists only inside apps, and since iOS 14.5 it returns all zeros unless the user granted tracking permission through App Tracking Transparency (Apple, advertisingIdentifier). A web page has no IDFA at all, so even a consenting user gives you nothing to join on.
- Apple's rules draw two lines. Apple defines tracking as linking data from your app with data "from other companies' apps, websites, or offline properties" for targeted advertising or advertising measurement. Separately, its user privacy and data use page says you "may not derive data from a device for the purpose of uniquely identifying it", and names the user's network connection as one example of such data. Any method has to fit inside both.
The four approaches, side by side
| Approach | What you get | Per user? | The catch |
|---|---|---|---|
| App Store campaign links and custom product pages | Downloads, sales, subscriptions and retention per link or page, in App Store Connect | No, aggregate | Nothing shows below five; can't be joined to your in-app events |
| SKAdNetwork for Web Ads and AdAttributionKit | Signed install postbacks for ads | No, aggregate | Built for registered ad networks, not for a link on your own site |
| Deferred deep link vendors | The right screen after install, plus attribution | When it works | A paste prompt, or network matching; a vendor SDK and contract |
| Probabilistic IP and device matching | A guess per install | Yes, with error | Breaks on shared networks and Private Relay; more signals edge toward fingerprinting |
App Store campaign links and custom product pages
A campaign link is your App Store URL with two tokens added. Apple's campaign links help gives this format:
https://apps.apple.com/app/apple-store/id123456789?pt=123456&ct=test1234&mt=8
pt is your provider token and stays the same across all
campaigns; ct is the campaign token, up to 30 characters,
and you create one per campaign in App Store Connect under your app's
Analytics tab, in Acquisition, Campaigns. A first-time download within
24 hours of using the link counts for that campaign. Per campaign you
get impressions, product page views, downloads, usage, sales and
subscriptions, each shown once it reaches five in the selected date
range, and a campaign only appears after more than a day has passed
since launch. Smart App Banners on your site can carry the same
tokens.
For a landing page this is the first thing to set up, because it costs
no code: give every page, or every channel that links to a page, its own
ct. App Store Connect's
source types
add one more layer. Taps from websites in Safari show as Web Referrer
with the referring site, while taps from Chrome or another non-Safari
browser on iOS count as an App Referrer, attributed to the browser
app.
Custom product pages go further when different pages promise different things. You can publish up to 70 extra versions of your product page, each with its own URL (a product page ID parameter added to the default URL), and Analytics shows impressions, downloads, retention and average proceeds per paying user for each one, once a page has at least five first-time downloads.
The limits are the same for both. Everything is aggregate, the token never reaches your app, and a small page can sit under the threshold of five for weeks. "Did the people from this page finish onboarding" is out of reach, because App Store Connect never sees your in-app events; the gaps are covered in App Store Connect analytics in 2026.
SKAdNetwork for Web Ads and AdAttributionKit
Apple has two privacy-preserving attribution frameworks, and both are built around ads. With SKAdNetwork for Web Ads, available since iOS 16.1, ad networks get attribution for web ad clicks in Safari that lead to App Store installs. The network registers with Apple and serves a signed payload, and it works only in Safari, not in SFSafariViewController or WKWebView. AdAttributionKit covers ads shown inside other apps, for apps distributed through the App Store and alternative marketplaces. Its postbacks contain no user- or device-specific data, arrive at least 24 to 48 hours after the conversion (Apple), and carry more or less detail depending on how many installs share the same values.
Both are the right answer when you buy ads through a network that supports them. A "Download" button on your own blog isn't a signed ad, and a delayed aggregate postback wouldn't tell you which user it was anyway. For the ad side, see Do you need an MMP as an indie iOS developer in 2026?
Deferred deep link vendors
Branch, AppsFlyer OneLink, Adjust and similar services exist mainly to open the right screen after a fresh install, with attribution on the side. On iOS they have two ways to carry the link across the install:
- The pasteboard. Branch's NativeLink copies the destination URL to the clipboard when the user taps its button and checks the pasteboard on first open. Since iOS 16, programmatic pasting shows the user an alert asking for approval unless they tap a system UIPasteControl button, so this path costs a prompt or an extra tap.
- Network and device matching. Branch's own guide to iOS deferred deep linking notes that Private Relay can hide IP addresses for iCloud+ users in Safari, a limitation that only applies to matching by address.
Apple's privacy page adds one rule for these tools: if a third-party deep-linking service passes unique identifiers or creates a shared identity between apps from different companies for ad targeting or ad measurement, your app needs the ATT prompt. And if an old tutorial points you to Firebase Dynamic Links, that service shut down on August 25, 2025. A vendor is worth it when you need content deep links after install. For attribution from your own pages alone, it's an SDK and a contract for a question you can answer with less.
Probabilistic IP matching
The classic do-it-yourself version records the visitor's IP address plus as many browser and device traits as it can get (user agent, OS version, screen size, language) at the click, collects the same traits from the app at first launch, and scores how alike they look over a window of hours or days. It fails in two directions.
It's fragile because one address often covers many people, and one person often uses several addresses. Carriers put many subscribers behind one public IPv4 address with carrier-grade NAT (the IETF reserved address space for it in RFC 6598), and offices and campuses do the same with Wi-Fi. Private Relay swaps Safari's address for a relay address that Apple says "may be shared among more than one Private Relay user". IPv6 devices rotate temporary addresses (RFC 8981), and phones hop between cellular and Wi-Fi all day.
It's privacy-sensitive because every trait you add to fix the accuracy moves it closer to what Apple prohibits: combining device signals until they identify one device. A table of raw IP addresses with timestamps is also personal data you now have to protect and explain.
A privacy-respecting network match, step by step
The method below is still a network match. What keeps it narrow is what it leaves out: one network signal plus a country and iPhone-or-iPad, one comparison inside a short window, your own site and your own app only, and nothing linkable kept after three days.
1. Record the store tap on your server
When someone taps an apps.apple.com link on your page,
send an event with the page path, any campaign parameter and the time.
The browser leaves for the App Store right away, so send it with
navigator.sendBeacon or fetch with
keepalive, or it gets cancelled. Your server, not the
browser, reads the network address from the
connection, hashes it with a secret salt, and stores only the hash
next to the country and a coarse device class: iPhone, iPad or other.
Safari on iPad identifies itself as a Mac by default, so treat a Mac
user agent as iPad; a Mac user can't install the iPhone app from that
tap anyway. The raw address is never written down.
Keep the salt secret and on the server. There are only about 4.3 billion IPv4 addresses, so an unsalted hash can be reversed by hashing every one of them.
2. On the app's first launch, hash its address the same way
The first request your app makes to your own backend carries its network address (if you have no backend, one serverless function is enough). Hash it with the same salt and look for an unclaimed tap. The app doesn't send anything extra for this: no identifier and no device traits beyond the device model most analytics events already carry, which is where iPhone or iPad comes from.
3. Exact tier: same network, within 24 hours
A tap with the same address hash, a compatible device class, not yet claimed, in the last 24 hours, is an exact match. Don't also require the same country here: the app may report the phone's region setting while the tap carries network geography, and the two differ for anyone abroad or with a foreign region set.
-- exact tier: claim the newest open tap from the same network
update store_taps
set matched_user_id = $1, matched_at = now(), match_tier = 'exact'
where id = (
select id from store_taps
where app_id = $2
and ip_hash = $3
and matched_at is null
and clicked_at > now() - interval '24 hours'
and (device_class is null or device_class = $4)
order by clicked_at desc
limit 1)
and matched_at is null
returning page, campaign;
4. Probable tier: only where a coincidence is unlikely
With no address match, a tap from the same country and device class in the last 30 minutes can count as probable, but only if your normal organic installs make a coincidence unlikely. Divide the country's usual daily organic installs by 48 to get the expected number in any 30-minute window. Illustrative numbers: at 2 a day from Germany, that's about 0.04, so a German iPhone install ten minutes after a German iPhone tap is probably that person. At 200 a day from the US it's about 4, and a country match means nothing; leave the install unmatched. A cut-off around 0.15 expected installs keeps "probable" meaningful.
5. Claim each tap once
The update above only succeeds while matched_at is still
empty, so two installs can't take one tap. After a match, mark any other
open taps from the same address as used, so a second phone on the same
Wi-Fi can't claim them a few minutes later.
6. Erase the hashes after three days
An hourly job clears the hash column once a tap is three days old. The page, campaign, country and match tier stay, and from then on the tap can't be linked to anything:
update store_taps set ip_hash = null
where ip_hash is not null
and clicked_at < now() - interval '3 days';
What the labels mean
- Exact means the same network address, not the same person. On a shared network it can be someone else.
- Probable means the timing and coarse traits line up in a place and hour where a coincidence would be rare.
- Unmatched is most installs, and that's expected. It includes everyone the method can't see.
No install is ever labelled certain. When you report results, show the share of installs in each tier next to the numbers, so nobody reads a page's installs as a full count.
Where the fingerprinting line is
Apple's rule is about purpose: deriving data from a device "for the purpose of uniquely identifying it". This method uses the network connection, which is one of Apple's own examples of device data, so the question is what it does with it. It builds no identifier for the device, collects nothing about the browser or device beyond a country and iPhone or iPad, compares one tap with one first launch inside a day, keeps nothing linkable after three days, and never leaves your own website and your own app. That last point also keeps it outside Apple's definition of tracking, which is about data from other companies' apps and websites.
This is our reading; Apple hasn't published guidance on this exact pattern. If you want no judgment call at all, campaign links and custom product pages involve none. Either way, a salted hash can still be linked by whoever holds the salt, so treat it as personal data: delete it quickly, and say in your site's privacy notice that you measure store taps this way.
What it misses and what it gets wrong
| Situation | Effect |
|---|---|
| iCloud Private Relay (iCloud+ users, in Safari) | Apple's network guide says Private Relay covers Safari browsing, DNS and insecure HTTP app traffic. The tap arrives from a relay address and your app's HTTPS calls aren't in that list, so no exact match. At best probable. |
| VPN on the phone | The address depends on whether the VPN was on at the tap and at the first launch, and many users can share one exit address. |
| Shared networks: office Wi-Fi, campuses, carrier NAT | Two people behind one public address who both tap and install within the window can swap credit. Claim-once and device class reduce it; they don't remove it. |
| Tap on cellular, install later on Wi-Fi | Different addresses, so no exact match, and probable only inside 30 minutes. It was the first kind of miss we ran into when we started matching installs on our own apps. |
| Read on a laptop, installed on the phone | The tap's device class isn't iPhone, so there's no match even on the same home Wi-Fi. The device-class check gives up these installs in exchange for fewer false matches. |
| Time gaps | Someone who taps today and installs next week falls outside every window. Campaign links stop at 24 hours for the same reason. |
| Ad blockers | Brave, uBlock Origin and most content blockers stop the tap event from reaching your server, so those visitors never enter the data (web tracking docs). |
| IPv6 temporary addresses | They rotate over time; a short window keeps most taps and launches on the same one. |
Put together, the matched installs are a floor. That's fine for the decisions this is built for: which pages and posts to keep writing, which page variant sends people who stay, which campaign parameter to drop. It isn't fine for paying affiliates or creators per install, sending conversions to an ad network, or quoting an install count as exact.
Which method to use
- Downloads and revenue per page, a few installs a week or
more: campaign links with one
ctper page, plus custom product pages if the pages differ in what they promise. No code. - Paid ads: the network's SKAdNetwork or AdAttributionKit support, and Apple Ads through AdServices (see Apple Ads attribution without an MMP).
- Opening a specific screen after install: a deferred deep link vendor, or your own pasteboard flow with UIPasteControl. Accept the prompt or the extra tap.
- Whether people from a page finish onboarding, come back and pay: the network match above, joined to your own events and revenue on one user record (the one-ID pattern).
FAQ
Can an iOS app know which website sent the user?
Not directly. The App Store hands the app no referrer, URL or campaign token, and iOS has no install referrer API. Campaign links give aggregate counts per link; per-user answers need your own matching.
Do App Store campaign links work for a landing page?
Yes. Give each page or channel its own ct. First-time
downloads within 24 hours of using the link count for it, and each
metric appears in App Store Connect once it reaches five.
Is IP address matching allowed on iOS?
Apple forbids deriving data from a device to uniquely identify it and lists the network connection as an example. A first-party match of one tap to one first launch, with no stored device identifier and the hash erased after days, is our reading of what stays inside that rule; Apple hasn't published guidance on it.
Does iCloud Private Relay break web-to-app attribution?
For exact matching, yes. The Safari tap comes from a shared relay address, and your app's HTTPS requests aren't among the traffic Apple lists for the relay, so those installs can at best be probable.
How accurate is website-to-install matching?
It's a floor. It misses Private Relay, VPN and ad-block users and late installs, and shared networks can misassign an exact match. Report the tiers separately and use the result for decisions, not billing.
Sources
- Apple, Campaign links, Acquisition source types and Custom product pages in Analytics (App Store Connect Analytics help)
- Apple, Custom product pages
- Apple, User privacy and data use (tracking definition, fingerprinting, deep-linking tools)
- Apple, advertisingIdentifier and UIPasteControl
- Apple, SKAdNetwork for Web Ads, AdAttributionKit, Receiving ad attributions and postbacks
- Apple, Prepare your network or web server for iCloud Private Relay
- Google, Play Install Referrer; Firebase, Dynamic Links deprecation FAQ
- Branch, NativeLink deferred deep linking and Deferred deep linking on iOS
- IETF, RFC 6598 (shared address space for carrier-grade NAT) and RFC 8981 (IPv6 temporary addresses)