Attribution
Attribution API endpoint to match an app install to the link click that drove it, using the referrer, device ID, probabilistic match, and raw-signals cascade.
Install attribution answers one question: which link click led to this app install. A user taps a WarpLink short URL on their phone, the destination is the app store because the app isn't installed yet, and the install that follows arrives as a fresh app launch with no click history attached to it. This endpoint is how that gap closes. The mobile SDK calls it once, shortly after first launch, with whatever signals the platform can give it, and the server answers with either the link the install is attributed to or a plain "no match."
This is the one endpoint in the API called by a device rather than a script or a dashboard integration, and its authentication reflects that: it's the only operation that requires an SDK key rather than being reachable by a user-scoped API key at all. There is also no organization-role check here, unlike every other endpoint in this reference. Every other resource checks the calling key's creator against the role-permission matrix; attribution match has no creator to check, because the caller is a device, not a team member acting on the dashboard's behalf. The scope check alone decides whether the request proceeds.
Authentication and Permissions
| Operation | Scope | Role required |
|---|---|---|
| Match an install | attribution:write | none; SDK-key-only, no role check |
attribution:write is granted only to SDK keys and cannot be requested on an API key at creation time, whatever scopes you ask for. An API key placed in a mobile app's configure() call authenticates fine and resolves deep links fine, since both only need links:read, but every attribution request it makes is rejected with 403 FORBIDDEN. See Authentication for how the two key types are enforced, and the Errors reference for this exact failure mode.
The Match Cascade
The server tries four strategies in a fixed order, stopping at the first one that finds an install: referrer, then device ID, then the probabilistic match, then raw signals. Each strategy answers to a different signal the SDK can send, from most certain to least:
- Referrer.
referreris a WarpLink link ID your SDK read out of the Play Install Referrer (sent asutm_content), not the raw referrer string. This is deterministic: the link ID names the exact click, so there's nothing to guess. Android only, since iOS has no install referrer mechanism. - Device ID.
device_idis the IDFV, which only the iOS SDK sends. It finds an install already recorded for this device and returns it with that install's own match type and confidence, so it never turns a probabilistic match into a guaranteed one. This covers the same install asking again, for example after an earlier check was cut off before its answer arrived. A request withis_reinstall: trueskips this step, so a reinstall is recorded as a new install by the steps below. On iOS nothing in the cascade is deterministic for a first install, because the App Store passes nothing through an install. - Probabilistic match. The
fingerprintfield is a pre-computed SHA-256 hex string (a hashed match key) the SDK built from device signals. The server looks it up against the bucket of clicks recorded under that key and, if it finds an unclaimed one, claims it. - Raw signals.
accept_languageandtimezone_offset(optionallytimezone, the IANA zone name) let the server compute the match key itself, using the request's own IP address. This is the path the SDKs actually use in practice; they never send a pre-computed match key or an IP.
A branch that finds nothing falls through to the next one rather than answering matched: false immediately, so a request that sends both a referrer and raw signals still gets a probabilistic answer if the referrer didn't resolve to a live link. The strategy that was actually attempted, not just the first field present in the request, is what's recorded for each attempt, which is why a miss on the referrer branch is distinguishable from a miss with no signals at all.
Why Raw Signals Exist
A phone cannot learn its own public IP address from inside an app; that's only visible from the server end of the request. The IP is part of the match key the click side (the WarpLink Worker) computes when it records the click, so a match key the SDK computed locally, without knowing its own IP, would never match the one the click stored. Raw signals close that gap: the SDK sends the ingredients it can supply from the device, accept_language and timezone_offset (or the IANA timezone), and the attribution endpoint computes the match key server-side using the IP visible on the incoming request itself, the same way the click side did. This is why raw signals are the path every SDK actually uses, rather than a pre-computed fingerprint field (a hashed match key), which exists mainly for testing and for non-SDK integrations that already have a server-side IP to work with.
timezone, the IANA zone name (for example America/Toronto), is preferred over timezone_offset whenever a platform can supply it, because a named zone carries far more entropy, roughly 340 possible zones against 38 possible offsets, and doesn't shift at a daylight-saving boundary the way a raw offset does. Sending timezone selects the enriched_tz match variant; an SDK that sends timezone_offset alone gets enriched; a request with neither of those but still enough to satisfy the endpoint's validation is scored as basic.
Confidence and the Match Window
A probabilistic or raw-signals match returns a match_confidence score that states how strong the match is. match_confidence starts from the match variant and how long ago the click happened: enriched_tz starts at 0.85 confidence within the first hour and decays to 0.5 by 6 hours and 0.3 by 24 hours; enriched starts at 0.8, decaying to 0.45 and 0.25 at the same marks; basic starts at 0.7, decaying to 0.35 and 0.2. Each link sets its match window, with a six-hour default and a 24-hour maximum. Two more factors reduce the score: if the match key's click bucket held more than one distinct link (the answer was picked from a set, not read off a single entry), confidence is multiplied by 0.6; and if the click's IP address was classified as CGNAT, shared by potentially hundreds of unrelated subscribers, confidence is multiplied by another 0.6, versus 0.9 for an ordinary home IPv4 address and 1.0 for IPv6, which is rarely shared at all. A deterministic match (referrer or device ID) skips all of this: it reads a stored install record rather than scoring a match key, so the match window and these factors don't apply to it.
match_guaranteed is true only for a deterministic match, never for a probabilistic one. Use it, like match_confidence, to pick the destination the attribution result routes to, and keep sign-in on your own authentication.
App identity
A key authenticates a request for an organization, and an untied key does not say which app inside it is asking, so which app is asking has to travel in the request body itself: app_bundle_id on iOS, app_package_name on Android. Both are optional, because SDK versions before 1.1.0 send neither, and the server falls back to matching across the whole organization when neither is present. That fallback is exact for an organization with one app. SDK 1.1.0 and later send the app identifier.
An SDK key can also be tied to one app when you create it. A tied key acts only as that app: the request must name that app (or name none, as SDK versions before 1.1.0 do, which works only when that app is registered for the reporting platform), and the server compares it with the key's own app and nothing else. A request that names a different app, or an app your organization does not have, is refused with 403 KEY_APP_MISMATCH and one message for both cases, so the response never reveals which apps exist. The attempt is recorded with fail_reason: "key_app_mismatch". It also skips a referrer link that belongs to another app, and a link of another app resolves for it as 404 NOT_FOUND. Keys that are not tied behave exactly as before.
is_reinstall
is_reinstall marks an install on a device that already completed WarpLink attribution once, for the same app, under an install that has since been deleted. It never gates the match: a reinstall is a real install, attributed again on every platform, and counted once in your install totals, same as a first install. What it changes is which branch answers. Sending true makes the server skip the device ID branch, so the reinstall is matched by the probabilistic tier instead of being answered with the deleted install's stale row, and it stops the referrer branch from replaying that same stale row. An SDK that sends nothing, which includes every version before the field existed, is read as false and keeps the original device ID lookup. This field only ever narrows which branch answers; it can't be used to force a match that the cascade wouldn't otherwise find.
Attribution Match
/attribution/matchMatch an app install to a link click
Request Body
| Field | Type | Description |
|---|---|---|
fingerprint | string | Pre-computed SHA-256 match key as a hex string, case-insensitive. Optional; SDKs use the raw-signals path instead. |
platform* | "ios" | "android" | |
fingerprint_version* | "basic" | "enriched" | "enriched_tz" | Which match variant the SDK's signals support. `enriched_tz` uses the IANA zone, `enriched` the minute offset, `basic` neither. |
sdk_version* | string | |
device_id | string | The IDFV, sent only by the iOS SDK. Not an install match on its own: it finds an install this device already has and returns it with that install's original match type, confidence, and match_guaranteed, so it never turns a probabilistic match into a guaranteed one. Checked before the probabilistic match, and skipped when is_reinstall is true. |
referrer | string | The WarpLink link ID carried in the Play install referrer as utm_content. Send the parsed link ID, not the raw referrer string. Deterministic match; takes precedence over the probabilistic match. |
accept_language | string | Raw device preferred language (e.g. "en-US"). Required for the probabilistic path. The server normalizes it to the primary subtag. |
timezone_offset | integer | Timezone offset in minutes, JS getTimezoneOffset() convention (UTC+2 is -120). Sent alongside `timezone`; used as the fallback when no IANA zone is available. |
timezone | string | IANA timezone name, e.g. `America/Toronto`. Preferred over `timezone_offset`: it carries far more entropy and does not shift at a daylight-saving boundary. Sent by SDK versions that collect a zone name; older SDKs send the offset only. |
app_bundle_id | string | iOS bundle identifier of the calling app. Optional. Without it an install is matched across the whole organization, which is ambiguous once the organization has more than one app. |
app_package_name | string | Android package name of the calling app. Optional. Without it an install is matched across the whole organization, which is ambiguous once the organization has more than one app. |
is_reinstall | boolean | True when this device already completed WarpLink attribution for this app under an earlier install. A reinstall counts as an install and is attributed again on every platform, and the flag is more than a label: `true` makes the server skip the device ID (IDFV) branch, so the install is matched by the probabilistic tier instead, and it stops the referrer branch from answering with the earlier install's row. Optional, and absent means false: SDKs older than the field never send it. |
Responses
200Attribution result
400Validation error
| Field | Type | Description |
|---|---|---|
error* | object |
401Unauthorized
| Field | Type | Description |
|---|---|---|
error* | object |
403Forbidden. The key is missing the attribution:write scope, or it is an SDK key tied to a different app (code `KEY_APP_MISMATCH`).
| Field | Type | Description |
|---|---|---|
error* | object |
500Internal error
| Field | Type | Description |
|---|---|---|
error* | object |
503Service temporarily unavailable. Retry after the delay in the Retry-After header
| Field | Type | Description |
|---|---|---|
error* | object |
Worked Example
The SDK sends raw signals on first launch, since it can't compute its own IP-inclusive match key locally:
curl -X POST https://api.warplink.app/v1/attribution/match \
-H "Authorization: Bearer wl_live_YOUR_SDK_KEY" \
-H "Content-Type: application/json" \
-d '{
"platform": "ios",
"fingerprint_version": "enriched_tz",
"sdk_version": "1.1.1",
"app_bundle_id": "app.warplink.demo",
"accept_language": "en-US",
"timezone_offset": -240,
"timezone": "America/Toronto"
}'
A hit on the probabilistic branch, roughly 40 minutes after the click, with a single candidate and an ordinary residential IPv4 address, looks like this:
{
"matched": true,
"match_type": "probabilistic",
"match_confidence": 0.77,
"match_guaranteed": false,
"link_id": "3f9a2c10-...",
"deep_link_url": "warplinkdemo://product/42",
"destination_url": "https://warplink.app/demo/product/42",
"custom_params": {},
"install_id": "8e6d5b40-...",
"app_id": "9d4f1a20-..."
}
A miss, whether from no candidate click, an expired window, or a match key that scored zero confidence, is always the same shape:
{ "matched": false }
Common Errors
| Status | Code | Cause |
|---|---|---|
| 400 | VALIDATION_ERROR | None of fingerprint, referrer, device_id, or the raw-signals pair (accept_language + timezone_offset) were provided, or a field failed its own format check |
| 401 | UNAUTHORIZED | Missing, invalid, or expired key on the request |
| 403 | FORBIDDEN | The key is an API key rather than an SDK key, so it lacks attribution:write |
| 403 | KEY_APP_MISMATCH | The SDK key is tied to a different app than the one making the request |
| 500 | INTERNAL_ERROR | An unexpected server error; telemetry recording never causes this, since it's wrapped separately from the match itself |
| 503 | SERVICE_UNAVAILABLE | A dependency is temporarily unreachable; retry after the delay in Retry-After |
FAQ
Why does my API key get 403 FORBIDDEN on this endpoint?
attribution:write is only ever granted to SDK keys, and it's not in the set of scopes you can request when creating an API key. Create an SDK key instead and pass it to WarpLink.configure().
Why do I get 403 KEY_APP_MISMATCH?
The SDK key you configured is tied to one app, and the request came from a different app. Create an SDK key for this app under Keys > SDK keys. A tied key cannot be moved to another app.
Why do I need to send accept_language and timezone_offset instead of just a precomputed match key?
A phone doesn't know its own public IP, and the click side's match key includes it. Sending raw signals lets the server compute the match key with the IP visible on the request itself, which is the only way the two sides can agree on the same key.
What does ambiguous_app mean in my attribution metrics?
It means a request arrived with no app_bundle_id or app_package_name, your organization has more than one app, and the device-signal lookup that ran couldn't be scoped to just one of them. It's most common on SDK versions before 1.1.0; upgrading removes it.
What does probabilistic_disabled mean in my attribution metrics?
The app is in routing-only mode and the request carried a match key or raw device signals. We did not use them, so the attempt had only the referrer and the device ID repeat check to match on, and none applied. It is left out of the match rate, and the dashboard lists it under "Why installs don't match".
Why did a match come back with low confidence even though the click was recent? Confidence also drops when the click's bucket held more than one candidate link, or when the click's IP was a shared carrier-grade address (CGNAT). Either factor can pull the score down independently of how little time elapsed.
Can I force match_guaranteed: true for a probabilistic match?
No. It's true only for a deterministic match, meaning the referrer branch matched the install, and it's computed server-side from install.matchType. A device ID answer carries the flag of the install it returns, so it is true only when that install was itself a referrer match. There's no request field that overrides it.
Invitations
Invitations API endpoints to invite team members by email, list pending invitations, and revoke access, with role assignment, seat limits, and a 7-day expiry.
Installs
Installs endpoints list every install record and every attribution attempt, matched or not, with stable IDs and match details, to reconcile or keep a copy.