docs

Webhooks

Get audience builds, new leads, finished enrichment jobs and low balances pushed to your URL, signed with your endpoint's secret.

Add an endpoint in the app under Developers โ†’ Webhooks or with POST /v1/webhooks. Pick the events it gets and, optionally, the audiences it's for. The response includes the endpoint's signing secret (whsec_...). It's shown once.

Events

EventFires whenPayload
audience.readyAn audience build finished, ready or failedSchema
audience.new_leadsA watched audience's run found new leadsSchema
enrichment.completedA batch enrichment job finishedSchema
chips.lowThe balance dropped below 50 chips, or 10% of the last grant if higher (at most once a day), or a watch run couldn't auto-reveal for lack of chips (once per run)Schema

audience_ids on an endpoint limits the audience events to those audiences; null means all of them. Webhooks are how Zapier, Make, n8n and Clay connect to Diced: point a "catch hook" URL at an endpoint.

Payload

Every event is a POST with a JSON body:

{
  "id": "evt_9sKq2mX0aLp4ZtR7cVb1nW3e",
  "type": "audience.new_leads",
  "created": "2026-10-20T14:03:11.000Z",
  "workspace_id": "6f0c...",
  "data": {
    "audience_id": "aud_7GhT2kLm9QpX4sWv1bNc8ZrY",
    "run_id": "1a2b...",
    "new_count": 240,
    "revealed_count": 200,
    "masked_count": 40,
    "leads": [{ "id": "lead_...", "first_name": "Jane", "email": "jane@example.com", "...": "..." }],
    "reveal_url": "https://app.diced.si/?a=aud_7GhT2kLm9QpX4sWv1bNc8ZrY",
    "batch": null
  }
}
  • audience.new_leads carries the auto-revealed leads in full, up to 1,000 per delivery. A run with more is split into parts that share data.batch.id, with part and parts set. masked_count leads were past the cap; reveal_url opens the audience to Reveal them.
  • The full schema of each event is in the Events reference.

Requests have these headers:

HeaderValue
Content-Typeapplication/json
Diced-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
Diced-EventThe event type, such as audience.ready
Diced-DeliveryId of this delivery. Retries of the same delivery keep it
User-AgentDiced-Webhooks/1.0

Verify the signature

Diced-Signature is t=<timestamp>,v1=<signature>, where the signature is the hex HMAC-SHA256 of <timestamp>.<raw body>, keyed with your endpoint's whole secret (including whsec_). To verify:

  1. Read the raw request body. Don't parse and re-serialize the JSON first.
  2. Split the header on , and take t and every v1.
  3. Compute the HMAC of t + "." + body and compare it to each v1 in constant time.
  4. Reject timestamps more than 5 minutes from now, to stop replays.
verify.mjs
import { createHmac, timingSafeEqual } from "node:crypto";

/**
 * True if `header` (the Diced-Signature header) signs `rawBody` with `secret` (whsec_...),
 * and was made within `toleranceSeconds` of now. `rawBody` is the exact bytes you received.
 */
export function verifyDicedSignature(rawBody, header, secret, toleranceSeconds = 300) {
  let timestamp;
  const signatures = [];
  for (const part of String(header ?? "").split(",")) {
    const [key, value = ""] = part.split("=", 2).map((s) => s.trim());
    if (key === "t" && /^\d+$/.test(value)) timestamp = Number(value);
    else if (key === "v1" && /^[0-9a-f]{64}$/.test(value)) signatures.push(value);
  }
  if (timestamp === undefined || signatures.length === 0) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > toleranceSeconds) return false;

  const body = typeof rawBody === "string" ? Buffer.from(rawBody, "utf8") : rawBody;
  const expected = createHmac("sha256", secret).update(`${timestamp}.`).update(body).digest();
  return signatures.some((sig) => timingSafeEqual(Buffer.from(sig, "hex"), expected));
}

Use it on the raw body in your handler:

server.mjs (Express)
import express from "express";
import { verifyDicedSignature } from "./verify.mjs";

const app = express();

app.post("/diced", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyDicedSignature(req.body, req.get("Diced-Signature"), process.env.DICED_WEBHOOK_SECRET)) {
    return res.sendStatus(400);
  }
  const event = JSON.parse(req.body);
  // Ignore event ids you've already handled; deliveries can repeat.
  res.sendStatus(200); // answer fast, then do the work
  handle(event);
});

Delivery and retries

  • Answer with any 2xx within 10 seconds. Anything else, a timeout or a redirect (we don't follow them) counts as a failure.
  • A failed delivery is retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours: 7 attempts over about 21 hours.
  • Deliveries can arrive more than once and out of order. Use the event id to skip duplicates, and created to order them.
  • After 3 events in a row fail every retry, the endpoint is disabled (status: "disabled") and the workspace owners get an email. Fix the URL, then turn it back on in the app or with PATCH /v1/webhooks/{id} and { "status": "active" }.
  • The app shows each endpoint's delivery log, and you can resend any delivery from there.

Test events

Send test event in the app, or POST /v1/webhooks/{id}/test, sends the endpoint's first subscribed event type with sample data (ids like aud_Test000...), signed like a real one. It's tried once, never retried, doesn't count towards disabling, and is sent even while the endpoint is disabled. The response tells you how your URL answered.

Endpoint URLs

The URL must use https and must not include a username or password. Hosts that resolve to private or reserved addresses are refused, both when you save the endpoint and on every delivery.

On this page