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
| Event | Fires when | Payload |
|---|---|---|
audience.ready | An audience build finished, ready or failed | Schema |
audience.new_leads | A watched audience's run found new leads | Schema |
enrichment.completed | A batch enrichment job finished | Schema |
chips.low | The 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_leadscarries the auto-revealed leads in full, up to 1,000 per delivery. A run with more is split into parts that sharedata.batch.id, withpartandpartsset.masked_countleads were past the cap;reveal_urlopens the audience to Reveal them.- The full schema of each event is in the Events reference.
Requests have these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
Diced-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
Diced-Event | The event type, such as audience.ready |
Diced-Delivery | Id of this delivery. Retries of the same delivery keep it |
User-Agent | Diced-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:
- Read the raw request body. Don't parse and re-serialize the JSON first.
- Split the header on
,and taketand everyv1. - Compute the HMAC of
t + "." + bodyand compare it to eachv1in constant time. - Reject timestamps more than 5 minutes from now, to stop replays.
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:
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
idto skip duplicates, andcreatedto 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 withPATCH /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.