Glossary/Attribution mechanics
Server-to-server attribution
Definition
Server-to-server attribution is the practice of sending install and conversion events to an attribution endpoint from your own backend rather than from the device SDK, using an identifier captured at click time to join them to the campaign.
The device is a hostile reporting environment: apps get killed, networks drop, and any payload a client sends can be forged. S2S moves the reporting boundary to a place you control, which buys you reliability and trustworthiness at the cost of having to carry the click identifier yourself. It does not replace the SDK — something still has to observe the install.
The three hops, and where the identifier lives
S2S attribution is a join across three events that happen on three different machines. The only thing making it a join rather than a guess is that a single opaque identifier — the click ID — is carried from the first hop to the third.
| Hop | Happens on | Identifier carrier | Fails when |
|---|---|---|---|
| Click | Your link server | Generated and stored | Never — you own this |
| Store visit | App Store / Play | Android: install referrer. iOS: not carried | iOS drops URL parameters here |
| First open | The device | SDK reads referrer or matches deferred | App opened without ever seeing the link |
| Install event | Your backend | Click ID sent in the payload | Client never forwarded it |
| Later conversions | Your backend | Your own user ID, joined to the click ID | User ID assigned before click ID stored |
The row that surprises people is the last one. If a user registers before your backend has seen the click ID, the join is broken forever unless you write the click ID onto the user record at the moment the two are first known together. Storing it only on the session is the single most common cause of conversions that attribute to nothing while installs attribute fine.
The actual request
An S2S install postback is an ordinary signed HTTP request. There is no magic in the format — what makes it trustworthy is the signature, the timestamp and the idempotency key, all of which are absent from most client-sent equivalents.
POST /v1/events/install HTTP/1.1
Host: api.example.com
Content-Type: application/json
X-Signature: sha256=8f4c2b91ad0e5c7f1b3a9d6e2f8c0a4b7d1e3f5a9c2b8d4e6f0a1c3b5d7e9f2a
X-Signature-Timestamp: 1755532800
Idempotency-Key: install-6aafb7a5-0170-41b5-bbe4-fe71dedf1e28
{
"event": "install",
"click_id": "clk_01J9Z3QK7M2N4P6R8T0V",
"app_id": "com.example.app",
"platform": "android",
"occurred_at": "2026-08-18T14:22:31Z",
"install_referrer": "utm_source=newsletter&utm_campaign=august&clk=clk_01J9Z3QK7M2N4P6R8T0V",
"referrer_click_timestamp_seconds": 1755528000,
"install_begin_timestamp_seconds": 1755531900,
"device": {
"os_version": "15",
"advertising_id": null,
"app_set_id": "d1f3a7c9-2b48-4e6a-9c1d-5f7b3e9a0c24"
}
}HTTP/1.1 200 OK
Content-Type: application/json
{
"event_id": "evt_01J9Z3RB4X8Y2Z6A0C4E",
"deduplicated": false,
"attribution": {
"status": "attributed",
"method": "deterministic",
"click_id": "clk_01J9Z3QK7M2N4P6R8T0V",
"campaign": "august-newsletter",
"source": "newsletter",
"click_to_install_seconds": 4531,
"window_days": 7
}
}Two fields in the response are worth surfacing in your own logs. method tells you whether the match was deterministic or modelled — folding probabilistic matches into the same count as deterministic ones destroys the only quality signal you have. And click_to_install_seconds is the raw material for the CTIT distribution, which is how fraud shows up before it shows up in spend.
What S2S actually buys you
| Property | Device SDK | Server-to-server |
|---|---|---|
| Forgeable by a user | Yes | No — the key never ships |
| Survives app kill mid-request | Often not | Yes |
| Blocked by network conditions | Frequently | Rarely |
| Retryable with idempotency | Hard | Straightforward |
| Sees purchase and refund state | Only what the client knows | Authoritative |
| Sees the install itself | Yes | No — needs the client to report it |
| Adds latency to your critical path | No | Yes, if sent synchronously |
The last two rows are why S2S is a complement rather than a replacement. Only the device knows the app was opened for the first time; only the server knows whether the payment cleared. A sensible split is that the SDK reports the install and session lifecycle, and the backend reports everything with money or entitlement attached — including refunds, which a client will never send you.
Sign the payload and verify the timestamp, or you have built an open endpoint
An unsigned S2S endpoint is worse than a client SDK, because it looks authoritative while accepting anything. Verify an HMAC over the raw body, reject signatures older than a few minutes to stop replay, and compare digests in constant time. The webhook page covers the same verification in the other direction.
Idempotency is not optional
Every retry policy that guarantees delivery guarantees duplicates. A network timeout after the server committed the write looks identical to a failure, so the sender retries, and the install is counted twice. The fix is a deterministic key on the sending side and a uniqueness constraint on the receiving side.
CREATE TABLE attribution_events (
event_id TEXT PRIMARY KEY,
idempotency_key TEXT NOT NULL,
click_id TEXT,
app_id TEXT NOT NULL,
event_name TEXT NOT NULL,
occurred_at TIMESTAMPTZ NOT NULL,
received_at TIMESTAMPTZ NOT NULL DEFAULT now(),
payload JSONB NOT NULL,
-- Two deliveries of the same event can never both land.
CONSTRAINT attribution_events_idem UNIQUE (app_id, idempotency_key)
);
-- Retries become no-ops instead of double counts.
INSERT INTO attribution_events (event_id, idempotency_key, click_id, app_id,
event_name, occurred_at, payload)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (app_id, idempotency_key) DO NOTHING;- Derive the key from the event, not from the attempt — a UUID generated per request defeats the purpose.
- Send asynchronously from a queue, so a slow endpoint never delays a user's request.
- Retry with exponential backoff and a cap; a stuck queue that retries forever is an outage amplifier.
- Log the response
attribution.status, not just the HTTP code. A 200 that saysunattributedis still a 200. - Keep clocks synchronised. Timestamp-based signature rejection fails confusingly on a drifting host.
UTM builder
An S2S join is only as good as the identifier the click carried. This builds correctly-encoded campaign URLs with a live preview, which is also the fastest way to catch the unencoded ampersand that silently truncates an Android install referrer before your backend ever sees the click ID.
Open the utm builder →Frequently asked questions
- What is server-to-server attribution?
- It is sending install and conversion events to an attribution endpoint from your own backend rather than from the device, joining them to a campaign using a click identifier captured when the user first tapped the link. The device SDK still reports the install itself; the server takes over for events where reliability and authenticity matter.
- How does the click ID reach the server?
- On Android it survives the store through the Play install referrer, which the SDK reads on first open and forwards to your backend. On iOS nothing survives the store boundary, so the click ID must be recovered by a deferred deep-link match at first open. Once your backend has it, write it onto the user record so later conversions can still join.
- Is server-to-server attribution more accurate than SDK attribution?
- It is more trustworthy rather than more accurate. A server-sent event cannot be forged by a user, cannot be lost to an app being killed mid-request, and can be retried safely with an idempotency key. Accuracy still depends entirely on whether the click identifier survived the journey, which is a link problem rather than a transport problem.
- Do I still need a mobile SDK if I use S2S?
- Yes, for the install itself. Only the device can observe a first app open, read the Play install referrer, or resolve a deferred deep link, and your server has no way to learn those things independently. The usual division is that the SDK reports install and session lifecycle while the backend reports purchases, refunds and anything tied to entitlement.
- How do I stop duplicate events from retries?
- Send a deterministic idempotency key derived from the event itself, never from the retry attempt, and enforce it with a uniqueness constraint on the receiving table so a repeated delivery becomes a no-op rather than a second install. Any delivery guarantee strong enough to survive timeouts will produce duplicates, so this belongs in the schema rather than in application code.
Related terms
- Webhook — A webhook is an HTTP request a service sends to a URL you control when an event occurs, delivering the event's data without your system having to poll for it.
- Deterministic attribution — Deterministic attribution credits an install to a specific click by matching an identifier that is present in both records, producing a one-to-one link rather than a statistical estimate.
- Play Install Referrer — The Play Install Referrer is a Google Play API that lets a newly installed Android app read the referrer string and click timestamps recorded when the user arrived at its Play Store listing.
- Click-to-install time — Click-to-install time is the elapsed time between the click on an ad and the first open of the installed app, measured per install and analysed as a distribution rather than an average.