Building an install-attribution pipeline with webhooks
Stream install.attributed events into your warehouse, verify the signatures, and reconcile probabilistic, deterministic, and SKAN in one place.
Polling an attribution API on a cron is how you end up with a pipeline that's always a few minutes stale and occasionally hammering an endpoint for data that hasn't changed. If you want attribution in your warehouse as it happens, let the platform push it to you.
Subscribe to the events that matter
Register an HTTPS endpoint and subscribe to the lifecycle you care about — typically install attribution and any later re-attribution. Each delivery is a self-contained record of how an install was matched.
{
"event": "install.attributed",
"installId": "inst_9f2",
"matchType": "probabilistic",
"channel": "tiktok",
"deepLinkPath": "/promo/summer",
"customData": { "code": "SUMMER50" }
}Verify the signature — against the raw body
A webhook endpoint is a public URL accepting state-changing data, so authenticate every delivery. Compute an HMAC over the exact bytes you received and compare in constant time. The most common bug here is verifying after a JSON middleware has re-serialized the body, which changes the bytes and breaks the signature.
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(rawBody: string, signature: string, secret: string) {
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signature);
return a.length === b.length && timingSafeEqual(a, b);
}Land it idempotently
- Key rows on installId so a retried delivery updates rather than duplicates.
- Store matchType and score as columns, never collapsed into a single attributed flag.
- Keep the raw payload too — schemas evolve, and you'll want to re-derive metrics later.
How should the endpoint respond?
Acknowledge fast, process later. Verify the signature, write the raw payload to a queue or a staging table, and return 200 — all before you touch your warehouse. If you do the enrichment inline, a slow downstream query turns into a webhook timeout, which turns into a retry, which arrives while the first one is still running.
- Return 2xx only once you have durably stored the payload — not before, or a crash loses the event silently.
- Return 5xx for anything you want redelivered, and 4xx only for payloads that will never succeed. A 4xx on a transient failure discards the event permanently.
- Never return 3xx. Most webhook senders don't follow redirects, and the delivery is recorded as failed.
- Keep the handler under a couple of seconds. Everything expensive belongs behind the queue.
What happens if my endpoint goes down?
Deliveries retry with backoff, so a short outage is absorbed and you receive a burst when you recover — which is exactly why idempotency on installId is not optional. The failure mode that actually costs you data is a longer outage that exhausts the retry schedule, or worse, an endpoint that returns 200 while silently failing to store anything.
Two safeguards. Alert on a drop in delivery volume rather than on errors alone — an endpoint that quietly stops receiving looks healthy from the inside. And keep a reconciliation job that periodically compares your warehouse against the attribution API for the same window, so a gap is detected by a scheduled query rather than by someone noticing a chart looks wrong a fortnight later.
Reconcile the three sources of truth
Webhooks give you deterministic and probabilistic matches in real time; SKAN postbacks arrive later, aggregated and delayed. Land all three in the same schema, reconcile on a stable window, and always be able to answer 'which source said so' for any number on the dashboard.
| Source | Latency | Granularity | Use it for |
|---|---|---|---|
| Attribution webhook | Seconds | Per install, with match type and score | Product logic and day-to-day reporting |
| SKAdNetwork postback | Hours to days | Aggregated by campaign | Reconciling paid iOS network spend |
| Ad network reporting | Varies | Aggregated, vendor-smoothed | A hypothesis to check, never the source of truth |
“Ingest the raw postback yourself, store it forever, and reconcile. A vendor's summarized report is a starting hypothesis, not the truth.”
Ahsan
LinkTrail Engineering
Ahsan is on the LinkTrail engineering team and the engineer behind its SDKs for iOS, Android, React Native, and Flutter. He started the company after the Firebase Dynamic Links shutdown left teams with links that opened the store and forgot where the user was going — and after too many vendor calls that ended without a price. He writes here about deferred deep linking, install attribution after ATT, and the parts of the mobile growth stack the category tends to leave vague.
All posts by Ahsan