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:

  1. an X3 link on your landing page or in your ad, which carries the campaign through the store;
  2. Apple Ads, which Apple confirms through the AdServices token;
  3. app campaigns of Meta, Google and TikTok: exact on Android through the Play referrer, aggregated on iOS through AdAttributionKit and SKAdNetwork postbacks;
  4. 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).

Install attribution through an X3 linkNine steps from a Meta or Google ad through the Nordlicht landing page, the X3 link, the store and the first open to the signup. Android is exact from step 6 through the Play Install Referrer; iOS becomes exact at step 7 when the signup carries an identity the web journey already has; otherwise only the aggregated postback copy of step 8 remains.AdMeta, GoogleLanding pageWeb SDKStoreApp Store, PlayNordlicht appX3 SDKX3 APIvia nordlicht.app/n3Click with utm_*, fbclid1Pageview: session and journey2Tap “Get the app” → /n3/go/app: click token x3c3302 · Android: referrer=utm_…&x3c=… · iOS: App Store with ct4Installand first open5POST /v1/m/installPlay referrer · AdServices token6Android: x3c → web session, tier referrer (exact)iOS: Apple Ads exact, otherwise unknown for nowno IP matching, no fingerprintingSignup: identify()user id hash · e-mail hash7Same e-mail hash as in the web journey→ the iOS install becomes tier identityAdAttributionKit postback copy, aggregated, after 24 to 48 h8Conversion “Subscription” to Meta with the fbclid of the web journey9
Install attribution through an X3 link

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).

TierDashboard (DE / EN)SourceConfidenceLevel
referrerExakt (Play-Referrer) / Exact (Play referrer)Play Install Referrer with the X3 click token, UTM parameters, gclid or the Meta block0.95per install
apple_adsExakt (Apple Ads) / Exact (Apple Ads)AdServices token confirmed by Apple0.95per install
linkÜber Link / Via linkUniversal Link, App Link or link token on the first open or a re-engagement0.90per install or per session
identityÜber Login / Via loginsignup or login links the install to a web journey with a campaign up to 90 days before the first open0.90per install, retroactive
aggregatedAggregiert / AggregatedAdAttributionKit and SKAdNetwork postbacks, platform reporting–per campaign only, never per person
organicOrganisch / OrganicPlay reports an organic install–per install
unknownUnbekannt / Unknownno source–per install
pendingWird ermittelt / Resolvingthe 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).

Accuracy tiers of install attribution by platformEight tiers: referrer is exact on Android through the Play Install Referrer, apple_ads is exact on iOS through the AdServices token, link is exact on both when an X3 link or Universal Link opens the app, identity applies on both after a login joins an earlier web journey, aggregated covers AdAttributionKit and SKAdNetwork postbacks on iOS and platform reports, organic is reported by Google Play, unknown means no source and pending means the Apple Ads lookup is still running.TieriOSAndroidLevelreferrerExact (Play referrer)–exactper installapple_adsExact (Apple Ads)exact–per installlinkVia linkexactexactinstall or re-engagementidentityVia loginafter loginafter loginper install, retroactiveaggregatedAggregatedpostbacksplatform reportsper campaign onlyorganicOrganic–Play: organicper installunknownUnknownno sourceno sourceper installpendingResolvingApple Ads wait–transient
Accuracy tiers of install attribution by platform

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.

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:

  1. X3 creates a click token x3c (22 characters, valid 30 days, stored only as a hash) and records the click.
  2. It finds the web session of the click: the query parameter s when it names a session of the project from the last 24 hours (your landing page can append getIds().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.
  3. 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.
  4. 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) and ct;
    • other devices → the fallback URL, else your first domain.

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.

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:

server/magic-link.ts
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 contentResult
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)
gclidtier referrer, ad platform Google Ads
the Meta block in utm_contenttier referrer, Meta campaign, ad set and ad (below)
utm_source=google-play&utm_medium=organic or (not set)tier organic
emptytier 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 → tier apple_ads, utm_source=apple_ads, utm_medium=cpc, campaign id as utm_campaign and utm_id, ad group as utm_content, keyword as utm_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 pending meanwhile).

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:

Info.plist
<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:

app/.well-known/appattribution/[...path]/route.ts
import { createX3ProxyHandler } from "@x3growth/sdk/next/proxy";

export const { POST } = createX3ProxyHandler({
  apiOrigin: process.env.X3_API_ORIGIN!,
  proxyKey: process.env.X3_PROXY_KEY,
});
app/.well-known/skadnetwork/[...path]/route.ts
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:

next.config.ts
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.

skan-schema.json
{
  "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 from to to. It only ever rises.
  • Coarse value: high, medium or low by the first matching class.
  • **lockOn**: an event that locks the first window (for example purchase), or null.

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

QuestionAnswer
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.