Deeplinkly

Glossary/Attribution mechanics

Webhook

Definition

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.

In attribution it is how an install, a re-engagement or a conversion reaches your warehouse, your CRM or your Slack channel seconds after it happens rather than in tomorrow's export. The mechanics are simple enough to underestimate: the three things that decide whether a webhook integration is reliable are signature verification, idempotent handling, and responding before you do any work.

What a delivery looks like

A POST with a JSON body and a small set of headers. The headers carry everything needed to verify and de-duplicate the delivery, which is why a receiver that only reads the body is missing half the contract.

POST /hooks/attribution — headers and body
POST /hooks/attribution HTTP/1.1
Content-Type: application/json
X-Event-Id: evt_01HQ8ZK3M4N5P6Q7R8S9T0
X-Event-Type: install.attributed
X-Timestamp: 1755388800
X-Signature: sha256=8f4b2c1d9e6a3f7b5c2d8e1a4f9b6c3d7e2a5f8b1c4d9e6a3f7b5c2d8e1a4f9b

{
  "id": "evt_01HQ8ZK3M4N5P6Q7R8S9T0",
  "type": "install.attributed",
  "created_at": "2026-08-17T09:20:00Z",
  "data": {
    "install_id": "ins_9f2c1a",
    "platform": "android",
    "attributed_at": "2026-08-17T09:19:58Z",
    "attribution_type": "deterministic",
    "click_id": "clk_4b7d2e",
    "campaign": { "source": "email", "medium": "newsletter", "name": "spring" },
    "deep_link": "https://example.com/products/42",
    "referrer": "utm_source=email&utm_campaign=spring"
  }
}
The headers that make a delivery verifiable, and what each is for.
HeaderPurposeIf you ignore it
X-Event-IdStable identifier for the eventRetries create duplicate rows
X-Event-TypeRouting without parsing the bodyEvery handler parses everything
X-TimestampReplay protectionA captured request replays forever
X-SignatureHMAC over timestamp and bodyAnyone who learns the URL can post to it
Content-Typeapplication/jsonFramework-dependent parsing surprises

An unauthenticated webhook endpoint is a public write API

The URL is not a secret — it travels through logs, proxies and config files. Without signature verification, anyone who learns it can post fabricated installs into your reporting and your downstream automation. Verify before you parse.

Verifying the signature

The standard construction is an HMAC-SHA256 over the timestamp and the raw request body, compared in constant time. Two details do the work: signing the raw bytes rather than re-serialised JSON, and rejecting old timestamps so a captured request cannot be replayed.

Verification in Node, with the traps marked
import crypto from "node:crypto";

// Read the RAW body. Re-serialising parsed JSON changes key order
// and whitespace, so the signature will never match.
app.post("/hooks/attribution",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const timestamp = req.get("X-Timestamp");
    const signature = req.get("X-Signature");

    // Reject anything older than five minutes: replay protection.
    if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
      return res.status(400).send("stale");
    }

    const expected = "sha256=" + crypto
      .createHmac("sha256", process.env.WEBHOOK_SECRET)
      .update(timestamp + "." + req.body)      // raw Buffer
      .digest("hex");

    // Constant-time compare. === leaks timing information.
    const ok = signature?.length === expected.length &&
      crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
    if (!ok) return res.status(401).send("bad signature");

    // Acknowledge FIRST, then work. See the next section.
    res.status(200).send("ok");
    enqueue(JSON.parse(req.body.toString()));
  });
The four verification mistakes that account for most failures.
MistakeConsequence
Signing re-serialised JSON instead of raw bytesSignatures never match; teams disable verification to ship
Comparing with ===Timing side channel on the secret
No timestamp checkA captured request is replayable indefinitely
Body parser installed globallyThe raw body is gone before the handler runs

Retries, ordering and idempotency

Delivery is at-least-once, not exactly-once, and not ordered. Any sender worth using retries on failure, which means your receiver will see the same event twice — usually on the day something downstream is already slow.

Sender behaviour a receiver must be built for.
PropertyRealityWhat the receiver must do
Delivery countAt least onceKey on X-Event-Id and ignore repeats
OrderingNot guaranteedOrder by created_at, never by arrival
Retry scheduleExponential backoff over hoursReturn 2xx only when you have durably accepted it
TimeoutTypically a few secondsAcknowledge first, process asynchronously
DisablingEndpoints failing for long enough get disabledAlert on delivery failure, not only on processing errors
Payload growthNew fields are added over timeIgnore unknown fields rather than failing validation

Do the work after you respond

A handler that writes to the warehouse, calls a CRM and posts to Slack before returning 200 will exceed the sender's timeout on its worst day, be retried, and do all of it twice. Validate, enqueue, return 200, then process. This is the single change that turns a flaky webhook integration into a boring one.

Idempotency is cheap to implement and expensive to retrofit: a unique index on the event ID, and an insert that ignores conflicts, is enough for most receivers.

Idempotent insert
CREATE TABLE webhook_events (
  event_id    text PRIMARY KEY,
  event_type  text NOT NULL,
  created_at  timestamptz NOT NULL,
  payload     jsonb NOT NULL,
  received_at timestamptz NOT NULL DEFAULT now()
);

-- A retry hits the primary key and does nothing.
INSERT INTO webhook_events (event_id, event_type, created_at, payload)
VALUES ($1, $2, $3, $4)
ON CONFLICT (event_id) DO NOTHING;

Webhooks against the alternatives

When a webhook is the right integration, and when it is not.
WebhookPolling an APIBatch export
LatencySecondsPoll intervalHours
Cost at low volumeVery lowWastefulLow
Needs a public endpointYesNoNo
Survives receiver downtimeVia sender retriesYes — you control the scheduleYes
Backfill and replayUsually limitedTrivialTrivial
Good forReal-time reaction to eventsReconciliationAnalytics and warehousing

The mature setup uses two of them: webhooks for anything that must react quickly — routing a new install to a lifecycle campaign, alerting on a fraud signal — and a periodic reconciliation pass against the API or an export, so a delivery lost during an outage is repaired without anyone noticing. Treating webhooks as the sole source of truth is what makes a missed delivery permanent.

Test with the real payload, not a hand-written one

Capture a genuine delivery, replay it against a local endpoint through a tunnel, and confirm your signature check passes with the raw bytes. Most integrations that fail in production fail on the difference between a real body and the pretty-printed JSON someone pasted into a test.

Free deep linking tools

Webhook delivery is configured per project in the dashboard, and the receiver contract above is the part you build. While you are wiring up attribution, the free tools cover the link side — association files, live domain checks and campaign URL tagging — with no login.

Open the free deep linking tools

Frequently asked questions

What is a webhook?
It is an HTTP request — almost always a POST with a JSON body — that a service sends to a URL you control when an event happens, so your system learns about it immediately instead of polling for changes. In attribution, webhooks deliver events such as an attributed install or a conversion to your warehouse, CRM or alerting within seconds.
How do I verify a webhook signature?
Compute an HMAC-SHA256 over the timestamp and the raw request body using the shared secret, and compare it to the signature header in constant time. Two details matter: sign the raw bytes rather than re-serialised JSON, because key order and whitespace change the hash, and reject deliveries whose timestamp is more than a few minutes old so captured requests cannot be replayed.
Why does my webhook receive the same event twice?
Because delivery is at-least-once. If the sender does not receive a 2xx response within its timeout — including when your handler is merely slow — it retries with exponential backoff. Store the event ID with a unique constraint and ignore conflicts, and acknowledge the delivery before doing any downstream work rather than after.
Are webhooks delivered in order?
No. Retries, parallel delivery and network variance all mean events can arrive out of sequence, so an event created later can be received first. Order by the created_at or equivalent field in the payload rather than by arrival time, and design handlers so that applying an older event after a newer one does not overwrite current state.
What should a webhook endpoint return?
A 2xx status as soon as the delivery has been validated and durably queued, with no downstream work done first. Anything in the 4xx range other than an authentication failure tells the sender the payload is unacceptable and is usually not retried; a 5xx or a timeout schedules a retry. Endpoints that fail persistently are typically disabled by the sender.

Related terms

  • Unattributed installsAn install is unattributed when no signal linking it to a prior ad click or link tap survived the journey through the app store, which can mean the install was organic or that the signal existed and was lost.
  • Migrating off Firebase Dynamic LinksMigrating off Firebase Dynamic Links means replacing the link generation, the hosting domain, and the SDK integration of a service that was shut down on 25 August 2025, after which page.link URLs stopped resolving.