Install attribution
On the web X3 attributes a session from the URL (UTM parameters and click ids) or through the
journey. Apps add a third source: the install itself. X3 determines
its source once per install and passes it on to every session of that install that starts within
30 days after the first open and carries no attribution of its own, just like a stored first
touch on the web (attr_from = stored).
There are four ways an install gets its source:
- an X3 link on your landing page or in your ad, which carries the campaign through the store;
- Apple Ads, which Apple confirms through the AdServices token;
- app campaigns of Meta, Google and TikTok: exact on Android through the Play referrer, aggregated on iOS through AdAttributionKit and SKAdNetwork postbacks;
- any **Play link with
referrer=** (newsletter, QR code, partner site).
A fifth path closes the gap later: a signup or login that links the install to an earlier web journey (identity).
Accuracy tiers
Every install carries exactly one tier. Stronger sources replace weaker ones, never the other way round, and the dashboard shows the tier as a chip next to every number (Quellen › Installationen, Sources › Installs).
| Tier | Dashboard (DE / EN) | Source | Confidence | Level |
|---|---|---|---|---|
referrer | Exakt (Play-Referrer) / Exact (Play referrer) | Play Install Referrer with the X3 click token, UTM parameters, gclid or the Meta block | 0.95 | per install |
apple_ads | Exakt (Apple Ads) / Exact (Apple Ads) | AdServices token confirmed by Apple | 0.95 | per install |
link | Über Link / Via link | Universal Link, App Link or link token on the first open or a re-engagement | 0.90 | per install or per session |
identity | Über Login / Via login | signup or login links the install to a web journey with a campaign up to 90 days before the first open | 0.90 | per install, retroactive |
aggregated | Aggregiert / Aggregated | AdAttributionKit and SKAdNetwork postbacks, platform reporting | – | per campaign only, never per person |
organic | Organisch / Organic | Play reports an organic install | – | per install |
unknown | Unbekannt / Unknown | no source | – | per install |
pending | Wird ermittelt / Resolving | the Apple Ads token is still waiting for Apple | – | transient |
Precedence: referrer = apple_ads > link > identity > organic > unknown; pending
resolves to one of them. identity may only replace organic and unknown. The confidence of
identity is deliberately below the journey confidence: the person is certain, but whether the
campaign of the web journey really caused the download is an assumption of the counting model
(lookback 90 days, as on the web).
What X3 never does: match IP addresses and time windows between web and app ("probabilistic matching"), read the IDFA or the GAID, fingerprint devices or read the clipboard. Whoever promises exact iOS install attribution without Apple Ads, a login or ATT uses one of these.
The tier needs the purpose attribution granted (consent). Without
it the SDK reads no referrer and no token, and the install is stored with tier unknown.
X3 links
Do not link "Get the app" straight to the store. Link to an X3 link on your own domain instead:
- through your first-party proxy:
https://nordlicht.app/n3/go/<code>(recommended: your domain, the same domain as your Universal Links), or - direct:
https://api.x3growth.io/v1/go/<token>(works without a proxy, for ads and QR codes).
Create links in Einstellungen › Apps › Nordlicht › Installationsquelle (Settings › Apps ›
Install source): a code (app, herbst-angebot), a name, the iOS and Android app, an optional
fallback URL for desktop browsers, default attribution (UTM values used when the link URL has none)
and, for iOS, an App Store campaign token (ct). The dashboard shows both URL forms to copy.
What happens on a click:
- X3 creates a click token
x3c(22 characters, valid 30 days, stored only as a hash) and records the click. - It finds the web session of the click: the query parameter
swhen it names a session of the project from the last 24 hours (your landing page can appendgetIds().sessionId), otherwise the most recent session of the same browser from the last 30 minutes (the same-day visitor hash X3 uses on the web anyway). It never uses IP addresses across web and app. - The attribution of the click is: the link URL's own UTM parameters and click ids, else the link's defaults, else the attribution of the web session.
- It redirects without setting a cookie (
Cache-Control: no-store,Referrer-Policy: no-referrer):- Android → Google Play with
referrer=utm_source=…&utm_campaign=…&x3c=<token>; - iPhone and iPad → the App Store page with
pt(your provider token, if set) andct; - other devices → the fallback URL, else your first domain.
- Android → Google Play with
On Android the token survives the store: the SDK reads it from the Play Install Referrer on the first
open, and X3 links the install to the web session and its campaign (tier referrer). On iOS the
App Store passes nothing into the app; pt and ct show up only in App Store Connect's own
reports. The install becomes exact when the person signs up or logs in with an identity the web
journey already has (tier identity).
When the app is already installed, an X3 link opens it as a Universal Link (iOS) or App Link
(Android) if you set those up (iOS,
Android). The SDK reads the code or token from the
URL in handleOpenURL, and X3 records the click retroactively: tier link for a first open,
otherwise a re-engagement that attributes the session.
<a href="/n3/go/app" id="get-app">Get the app</a>
<script>
// optional: name the web session explicitly instead of relying on the 30 minute fallback
window.x3 && document.getElementById("get-app").addEventListener("click", function (e) {
var ids = window.x3.getIds && window.x3.getIds();
if (ids && ids.sessionId) e.currentTarget.href = "/n3/go/app?s=" + encodeURIComponent(ids.sessionId);
});
</script>The proxy route needs @x3growth/sdk 1.3.0 or later; older proxies answer 404 for /n3/go/….
Links of the form /n3/go/<code> resolve only through a proxy with a proxy key (the key names the
project); the direct form /v1/go/<token> works everywhere. Test clicks: append x3_test=1.
Link tokens for magic links
The strongest setup on iOS is a web-first funnel: the person creates an account or leaves an e-mail on your website, and your welcome e-mail contains a Universal Link that opens the installed app. Mint a link token for the web session on your server and put it into the link:
const res = await fetch(`https://api.x3growth.io/v1/projects/${project}/app-link-tokens`, {
method: "POST",
headers: { authorization: `Bearer ${process.env.X3_SECRET_KEY}`, "content-type": "application/json" },
body: JSON.stringify({ sessionId, link: "app" }),
});
const { token } = await res.json(); // valid 30 days
const url = `https://nordlicht.app/welcome?x3c=${token}`;For the link to open the installed app, its path (/welcome here) must be listed in your
apple-app-site-association and covered by your Android App Links intent filter, next to
/n3/go/*.
The secret key needs the scope events:write. The app passes the URL to handleOpenURL, which
picks up x3c; apps that obtain the token another way call applyLinkToken(token). A first open
with a token gets tier link and the web session's attribution.
Play Install Referrer
On Android the SDK reads the Play Install Referrer once per install (Play keeps it 90 days), through Google's Install Referrer library 2.2, the only third-party dependency of the SDK, and sends the raw string with the first open. The server parses it with the same rules as URLs on the web:
| Referrer content | Result |
|---|---|
x3c=<token> | the X3 click: tier referrer, the click's web session, journey link |
the 13 attribution keys (utm_source … msclkid) | tier referrer with these values (e-mail or phone-like values are dropped, project redactions apply) |
gclid | tier referrer, ad platform Google Ads |
the Meta block in utm_content | tier referrer, Meta campaign, ad set and ad (below) |
utm_source=google-play&utm_medium=organic or (not set) | tier organic |
| empty | tier unknown |
Any Play link with referrer= works, with or without an X3 link: newsletters, QR codes, partner
sites. Build them like
https://play.google.com/store/apps/details?id=app.nordlicht&referrer=utm_source%3Dnewsletter%26utm_campaign%3Doctober.
Meta Install Referrer
For Android app campaigns Meta writes an encrypted block into the Play referrer's utm_content.
X3 decrypts it on the server (AES-256-GCM) and gets the campaign, ad set and ad ids and the campaign
name; utm_source becomes facebook or instagram, utm_medium paid_social. The encrypted
block itself is never stored.
You need Meta's decryption key: in the Meta App Dashboard of your app open App settings › Basic,
section Android, and copy the Install Referrer Decryption Key (64 hex characters). Paste it in
Einstellungen › Apps › Nordlicht (Android) › Installationsquelle › Meta Install Referrer. It is
stored encrypted and shown only as a hint afterwards. Without the key, Meta installs still get
utm_source=facebook|instagram and utm_medium=paid_social, but no campaign.
Apple Ads
On iOS 14.3 and later the SDK calls AAAttribution.attributionToken() from Apple's AdServices
framework once per install (skipped on the simulator) and sends the token with the first open. It
needs no ATT prompt and no IDFA. The X3 server, not the app, exchanges the token with Apple:
- Apple answers
attribution: true→ tierapple_ads,utm_source=apple_ads,utm_medium=cpc, campaign id asutm_campaignandutm_id, ad group asutm_content, keyword asutm_term, plus organization, ad and country; attribution: false→ the install was not an Apple Ads install (the tier stays what other sources say);- not available yet → X3 retries after 5 seconds, 30 seconds, 2 minutes, 10 minutes and 1 hour
(tier
pendingmeanwhile).
The token is kept encrypted for at most 24 hours and then deleted. Apple Ads costs are not imported in this version.
Postback copies
For iOS app campaigns of Meta, Google, TikTok and other networks, Apple's AdAttributionKit (iOS 17.4
and later, re-engagement from iOS 18) and SKAdNetwork decide on the device which ad wins and send a
signed, delayed, aggregated postback to the ad network. You can receive an exact copy of the
winning postback on your own domain, and X3 turns the copies into a campaign report. Copies are
never linked to persons or sessions; they only feed the tier aggregated.
1. Info.plist. Register your registrable domain (Apple ignores subdomains); the iOS guide has the full list:
<key>NSAdvertisingAttributionReportEndpoint</key>
<string>https://nordlicht.app</string>
<key>AdAttributionKit</key>
<dict>
<key>AttributionCopyEndpoint</key>
<string>https://nordlicht.app</string>
<key>OptInForReengagementPostbackCopies</key>
<true/>
</dict>React Native and Expo apps set both with the config plugin prop attributionEndpoint.
2. Routes on your domain. Apple posts to fixed well-known paths with a trailing slash:
https://nordlicht.app/.well-known/appattribution/report-attribution/ (AdAttributionKit) and
https://nordlicht.app/.well-known/skadnetwork/report-attribution/ (SKAdNetwork). The X3 proxy
handler (1.3.0 or later) maps them to the X3 API with your proxy key; X3 accepts postback copies
only through a proxy key, because only your domain proves that the app is yours. In Next.js add
two route files next to your /n3 route:
import { createX3ProxyHandler } from "@x3growth/sdk/next/proxy";
export const { POST } = createX3ProxyHandler({
apiOrigin: process.env.X3_API_ORIGIN!,
proxyKey: process.env.X3_PROXY_KEY,
});export { POST } from "../../appattribution/[...path]/route";Next.js redirects URLs with a trailing slash by default, and Apple does not follow redirects. Turn that off:
const nextConfig = {
skipTrailingSlashRedirect: true,
};
export default nextConfig;Other stacks: forward POST /.well-known/appattribution/* and POST /.well-known/skadnetwork/*
unchanged (no redirect, body as text, up to 16 KB) to the matching proxy endpoint of your
proxy recipe with the header x-3x-proxy-key; the handler of the Next.js recipe
shows the mapping. Store webhooks (App Store, Google Play, RevenueCat) never go through the proxy.
3. Verify. X3 checks Apple's signature of every copy and dedupes by the postback identifier;
failed signatures are stored with a flag and left out of reports. The app's App Store id
(Einstellungen › Apps, field App-Store-ID) must match the copy's app-id. Postbacks arrive at the
earliest 24 to 48 hours after the end of a conversion window (or after a lock), and how much they
reveal depends on the crowd size thresholds Apple applies. The report is under Quellen › Installationen › Apple-Postbacks.
Conversion value schema
Postbacks only say something when the advertised app raises its conversion value itself. X3 does
this for you on iOS (with the purpose attribution granted): you define a schema, and the SDK
updates SKAdNetwork and AdAttributionKit whenever a schema event happens.
{
"v": 1,
"bits": [
{ "bit": 0, "event": "account_created" },
{ "bit": 1, "event": "trial_started" }
],
"revenue": { "from": 2, "to": 5, "currency": "EUR", "tiers": [0, 1, 5, 10, 20, 30, 50, 75, 100, 150, 200, 300, 500, 750, 1000, 2000] },
"coarse": { "high": ["purchase"], "medium": ["account_created", "trial_started"], "low": ["app_open"] },
"lockOn": null
}- Fine value (0 to 63): the bits of every schema event seen so far, plus the index of the
revenue tier (cumulative purchases in the schema currency) in bits
fromtoto. It only ever rises. - Coarse value:
high,mediumorlowby the first matching class. - **
lockOn**: an event that locks the first window (for examplepurchase), ornull.
Edit the schema in Einstellungen › Apps › Nordlicht (iOS) › Installationsquelle › Conversion-Wert-Schema; a live preview decodes any value ("bit 0 account, bit 1 trial, revenue tier 20 to 30 EUR"). The schema must match what you configure in the ad networks: enter the same mapping in Meta Events Manager and Google Ads, so that their optimization and the X3 report read the values the same way. The same schema decodes the postback copies in the X3 report. Android ignores the schema.
iOS limits in short
| Question | Answer |
|---|---|
| Exact on iOS without Apple Ads? | Only when the person signs up or logs in with an identity your web journey already has, or opens the app through a link token. |
| Does ATT change that? | No. X3 never reads the IDFA; ATT only matters for sending user data to ad platforms (conversions). |
| What about Meta and Google app campaigns on iOS? | Aggregated per campaign through postback copies and the platforms' own reporting. |
| Can X3 guess with the IP address? | No, never. |