WarpLink
API Reference

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.

Two read endpoints give you the raw attribution records behind the dashboard's install counts. GET /v1/installs lists the install records: one row for each install we tied to a link. GET /v1/attribution/attempts lists every attribution attempt, including the ones that found no click, with the reason. Every record has a stable ID and the details of how it matched, so you can check a report against your own numbers, or keep a copy before you leave. The MCP server has the same two reads as the list_installs and list_attribution_attempts tools.

Neither list returns a device identifier. A device ID, a device fingerprint, an IP address and the stored routing payload are never in a response. Where one exists, a boolean such as has_device_id says so.

Authentication and Permissions

Both endpoints use an API key with the analytics:read or links:read scope. SDK keys are refused with 403 SDK_KEY_ROUTE_FORBIDDEN, the same as on every other route that is not an SDK endpoint. The rate limit is the same 60 requests a minute per key as the rest of the API.

Retention and the Free plan's look-back apply as they do to the click export. A Free organization reads the last 30 days: a from before that is refused with 403 LOOKBACK_EXCEEDED, and details.earliest_allowed is the first instant it may use. A from earlier than your plan's data retention is raised to the start of it, and window.from in the response shows the window that was read. A window that lies wholly before your retention returns 422 RETENTION_WINDOW_UNAVAILABLE. Like the click export, these lists keep working when a Free organization reaches its click limit.

List Installs

GET/installs

List install records

Lists the organization's install records, newest first, with stable ids and match details, so you can reconcile them against your own reports or keep a copy. Only matched installs have a record: an attempt that matched nothing is listed by GET /attribution/attempts instead. click_id is null when the match did not carry a click identifier (for example an Android referrer match or an older deferred payload). The device id, the device fingerprint and the stored routing payload are never returned; has_device_id, has_device_fingerprint and has_matched_payload say only whether each one is stored. Requires an API key with the analytics:read or links:read scope. SDK keys are refused with 403 SDK_KEY_ROUTE_FORBIDDEN. Records are filtered by installed_at: from is inclusive and to is exclusive. Retention and the Free plan's 30-day look-back apply as they do to the click export: a Free organization whose from is earlier than the last 30 days gets 403 LOOKBACK_EXCEEDED with details.earliest_allowed, and a from earlier than the plan's data retention is raised to it, which window.from reports. The list is not frozen when a Free organization reaches its click limit. Page with pagination.next_cursor and send the same filters with it. A parameter this endpoint does not accept is a 400 VALIDATION_ERROR.

Parameters

NameTypeInDescription
app_idoneOfqueryOmit to read installs of every app. A UUID reads one app. The literal "none" reads only installs with no app.
link_idstringqueryOnly installs attributed to this link.
fromstringqueryStart of the window, inclusive, as an ISO instant with an offset. Without it the list starts at the earliest instant your plan may read.
tostringqueryEnd of the window, exclusive, as an ISO instant with an offset. Defaults to now. Must be after from.
limitintegerqueryRecords per page, 1 to 500.
cursorstringqueryThe next_cursor of the previous page. Send it with the same filters. A cursor from the other list is refused with 400 CURSOR_INVALID.

Responses

200One page of install records
FieldTypeDescription
data*object[]
pagination*object
window*objectThe window that was read.
400Invalid query: an unknown or repeated parameter, a value of the wrong type, a from that is not before to (VALIDATION_ERROR), or a cursor that does not belong to this list (CURSOR_INVALID)
FieldTypeDescription
error*object
401Unauthorized
FieldTypeDescription
error*object
403Forbidden. An SDK key, a key without the analytics:read or links:read scope, or a Free organization whose from is earlier than its last 30 days (LOOKBACK_EXCEEDED, with details.earliest_allowed)
FieldTypeDescription
error*object
404Not found
FieldTypeDescription
error*object
422No part of the requested window is within your plan's data retention (RETENTION_WINDOW_UNAVAILABLE)
FieldTypeDescription
error*object

Records are filtered by installed_at and returned newest first. Only a matched install has a record. An attempt that matched nothing appears only in the attempts list. click_id is null when the match did not carry a click identifier (for example an Android referrer match or an older deferred payload).

id is the same value the attribution response returned as install_id, so you can join a record to the response your app received. click_id is the click the install was attributed to. match_guaranteed is true for a deterministic match. A reinstall is an install: it has its own record, and is_reinstall says whether the device had completed attribution before. The field is empty when the SDK did not say.

has_matched_payload says whether the routing the match answered with was stored when the match was made. It is false on records created before we stored it.

List Attribution Attempts

GET/attribution/attempts

List attribution attempts

Lists every attribution attempt of the organization, matched or not, newest first. An attempt is one request from an SDK asking which click led to an install. Use it to see why installs were not tied to a click: fail_reason names the reason (no_signals, no_match, no_candidates, candidates_filtered, candidates_claimed, scored_zero, key_app_mismatch), and it is also set to ambiguous_app on a matched attempt that looked up a device-wide signal across several apps. match_source is claim when the attempt created an install and replay when it answered from an existing one, so the claims equal the records of GET /installs. No IP address, fingerprint or device id is stored on an attempt or returned; ip_class says only whether the request came from a carrier-grade NAT, IPv4 or IPv6 address. Requires an API key with the analytics:read or links:read scope. SDK keys are refused with 403 SDK_KEY_ROUTE_FORBIDDEN. Attempts are filtered by created_at: from is inclusive and to is exclusive. Retention and the Free plan's 30-day look-back apply: a Free organization whose from is earlier than the last 30 days gets 403 LOOKBACK_EXCEEDED with details.earliest_allowed, and a from earlier than the plan's data retention is raised to it. The list is not frozen when a Free organization reaches its click limit. Attempts carry no link, so there is no link_id filter, and a member limited to their own links is refused. Page with pagination.next_cursor and send the same filters with it. A parameter this endpoint does not accept is a 400 VALIDATION_ERROR.

Parameters

NameTypeInDescription
app_idoneOfqueryOmit to read attempts of every app. A UUID reads one app. The literal "none" reads only attempts with no app.
matchedbooleanquerytrue reads only attempts that matched, false only attempts that did not. Omit to read both.
fromstringqueryStart of the window, inclusive, as an ISO instant with an offset. Without it the list starts at the earliest instant your plan may read.
tostringqueryEnd of the window, exclusive, as an ISO instant with an offset. Defaults to now. Must be after from.
limitintegerqueryRecords per page, 1 to 500.
cursorstringqueryThe next_cursor of the previous page. Send it with the same filters. A cursor from the other list is refused with 400 CURSOR_INVALID.

Responses

200One page of attribution attempts
FieldTypeDescription
data*object[]
pagination*object
window*objectThe window that was read.
400Invalid query: an unknown or repeated parameter, a value of the wrong type, a from that is not before to (VALIDATION_ERROR), or a cursor that does not belong to this list (CURSOR_INVALID)
FieldTypeDescription
error*object
401Unauthorized
FieldTypeDescription
error*object
403Forbidden. An SDK key, a key without the analytics:read or links:read scope, or a Free organization whose from is earlier than its last 30 days (LOOKBACK_EXCEEDED, with details.earliest_allowed)
FieldTypeDescription
error*object
404Not found
FieldTypeDescription
error*object
422No part of the requested window is within your plan's data retention (RETENTION_WINDOW_UNAVAILABLE)
FieldTypeDescription
error*object

An attempt is one request from an SDK asking which click led to an install. It is recorded whether or not it matched, so this list is the denominator of the match rate. fail_reason names why an attempt found no click, and matched and match_source say what a match did:

match_sourceMeaning
claimThe attempt created an install. The number of claim attempts equals the number of install records the attribution path created.
replayThe attempt answered from an install that already existed, for example a retry of the same request. It created no record.

Attempts recorded before September 9, 2026 carry no match_source. Attempts have no link, so there is no link_id filter. A member limited to their own links is refused this list.

The ip_class field says whether the request came from a carrier-grade NAT, an IPv4 address or an IPv6 address. It explains many no_candidates results, because the address a click was recorded under can differ from the address of the install. The address itself is not stored.

Paging

Both lists page with an opaque cursor. Send the next_cursor of one page as cursor on the next request, with the same filters, until has_more is false. A cursor is only valid for the list that issued it, and a cursor that is not valid returns 400 CURSOR_INVALID. Records are ordered by time and ID, so a record added while you page does not move the pages you have not read yet. limit is 1 to 500 and defaults to 100.

Worked Example

Read the next page by passing the cursor from the response:

See why attempts in October found no click:

On this page