Webhooks

Signed events posted to your endpoint: a lookup finished, usage crossed a threshold, a teammate joined. Add endpoints from Webhooks in the dashboard.

One envelope for every event

Every event, whatever its type, arrives in the same shape. Only data changes. Branch on type.

lookup.completed
{
  "id": "evt_9f2c1d3e4a5b6c7d8e9f0a1b2c3d4e5f",
  "type": "lookup.completed",
  "createdAt": "2026-09-25T10:00:00.000Z",
  "apiVersion": "2026-09",
  "accountId": "b7aa4dae-c079-4a49-a8a6-b806c2549316",
  "apiKeyId": "3b30316c-ea01-4d9d-9709-a77a194ee1ee",
  "data": {
    "lookupId": "48735993-28a7-4f1e-99da-0d1d6cb19859",
    "response": { "lookupId": "48735993-...", "input": { "email": "someone@example.com", "...": "..." }, "risk": { "score": 14 }, "...": "the same body POST /v2/lookup returns" }
  }
}

lookup.completed is sent once per request, not once per module. It carries the same response body the API returned, including the risk scores. usage.threshold_reached is sent at 80% and 100% of your allowance, once each per billing period. On a credit plan the scope is credit (amounts in millicents); on the free plan it names the module.

usage.threshold_reached
{
  "id": "evt_…",
  "type": "usage.threshold_reached",
  "data": { "scope": "credit", "percent": 80, "used": 3120000, "limit": 3900000, "periodStart": "2026-09-01T00:00:00.000Z" }
}

Verify every request

Each request has a DFP-Signature: t=<unix seconds>,v1=<hex> header. The signature is an HMAC-SHA256 of <t>.<raw body> with your endpoint’s signing secret. The timestamp is inside the signed material, so it cannot be changed without breaking the signature. Reject anything older than five minutes. A captured request is then useless to replay.

Verifying a signature
import { createHmac, timingSafeEqual } from "node:crypto";

// Use the RAW request body - not a re-serialised object.
export function verify(header, rawBody, secret, maxAgeSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.trim().split("=")));
  const t = Number(parts.t);
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > maxAgeSeconds) return false; // replay
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  const given = Buffer.from(parts.v1 ?? "", "hex");
  return given.length === expected.length && timingSafeEqual(given, expected);
}

// verify(req.headers["dfp-signature"], rawBody, process.env.DFP_WEBHOOK_SECRET)

Retries and duplicates

Answer with any 2xx within 10 seconds to acknowledge. Anything else is retried with exponential backoff, eight attempts over about two hours. Every retry, and every resend from the dashboard, carries the same event id, so store the ids you have processed and skip repeats. The event id is also sent as DFP-Event-Id.

If an endpoint keeps failing for three days it is switched off, and the account holder is emailed. Switch it back on from the dashboard once the receiver is fixed. Endpoints must be public HTTPS URLs; redirects are not followed.