How attribution works
A six-rung waterfall that resolves on every event batch, not just at install — so a user who arrives with no signal can still resolve later.
Dashboard → Attribution. Every install gets one first-touch source, permanently — the first rung that matches wins, and nothing overwrites it afterward.
1. Play Install Referrer → source: install_referrer
2. Claimed click → source: click_claim
3. Link params on launch → source: tracking_link
4. Raw UTMs → source: utm
5. Probabilistic (24h) → source: probabilistic
6. View-through → source: view_through- No candidate at any rung → no row is written. Organic is the absence of an attribution row, not a value it can be set to.
- Ties within a rung go to the most recent touch.
- Up to 3 losing touches are kept as contributors on the winning row, for assisted-install reporting.
Claiming a click by hand
Rungs 1, 3, 4, 5, and 6 above all resolve from data the SDK captures on its own. Rung 2 — claimed click — is the one exception, and it's manual for a structural reason: on iOS, a tap on a universal link can open Safari and go through the App Store before your app ever runs, so nothing the SDK does automatically at launch can see that click happened. You have to hand it the click ID yourself, once, after the user is identified:
import { claimAttribution } from "@fictura/sdk";
const claimed = await claimAttribution(growthClickId); // booleanCall it in exactly two places: when your app receives a growth_click param from a universal link, or right after a signup that started on the hosted landing page (/w/<slug>) — both are cases where the click already happened outside the app and needs to be paired to the account that just signed in. Everywhere else, the waterfall resolves without you touching it.
What identifies an install
An install is a row in master_users — one per app, per resolved identity. Identity comes from whichever of these your app sends first: a device ID, a RevenueCat ID, or your own account ID. The attribution row is unique per (app, master_user), so the first touch is permanent; anything later that matches the same user is a no-op.
Deterministic vs. passive
The first four rungs are deterministic — a real click, a real referrer string, a real UTM. The last two are passive, IP-hash matches with real limits:
- The hash is
sha256(client_ip), truncated. The raw IP is never stored — only the hash, computed identically at click time and install time. - Probabilistic matches to a click within a hard 24-hour cap, regardless of the link's own configured window, and additionally requires a non-conflicting country and platform.
- View-through matches to an impression within the link's configured window, capped at 24 hours.
- Passive matching is skipped entirely for users older than 48 hours — it exists to resolve a fresh install, not to relabel someone who's been using the app for a week.
pending, not organic
A user with no identity yet reads back as pending, not organic — resolving requires an identity to exist first. And an organic answer for a user under 48 hours old carries may_still_resolve: true, since a passive match could still land. Cache accordingly: an "organic" you fetched at minute 3 is not the final word.
Re-engagement
A returning user isn't re-attributed — first touch stays first touch. But a link marked is_retargeting writes a separate re-engagement record instead, on deterministic signals only (never probabilistic), deduped per (app, user, link) within a 30-day window. It never overwrites is_primary_attribution, so revenue is never double-counted between a user's original source and a win-back campaign that reached them later.
When it runs
On every event batch the SDK sends — not only ones that carry attribution parameters. That's what lets a passive rung resolve after the fact: the first batch might carry nothing useful, and the fifth might contain the IP that finally matches a click. It fast-exits once a row exists, and a failure here never blocks event ingestion — attribution can't take your analytics down with it.