Webhooks
Signed HTTP POST when a search completes or fails.
Subscribe to search.completed and search.failed events to skip polling. Every delivery is signed with HMAC-SHA256 so you can verify it came from us.
Setting up an endpoint
Open the API Keys dashboard, scroll to the Webhooks card, click Add endpoint, and enter your URL. You will see the signing secret once. Store it in your server's env vars.
Your endpoint must return a 2xx response within 10 seconds. Non-2xx or timeout triggers a retry.
Payload shape
{
"event": "search.completed",
"timestamp": 1776709919,
"data": {
"searchId": "k1710dek6438k67zhfxqt9ay3d8576da",
"teamId": "kx76fgaysm8b9kqsgjczj8ys9d839b6y",
"domain": "acme.com",
"role": "CEO",
"status": "completed",
"results": [
{
"email": "jane@acme.com",
"name": "Jane Doe",
"title": "Chief Executive Officer",
"score": 95,
"source": "verified"
}
]
}
}Headers
| Header | Example | Purpose |
|---|---|---|
X-DME-Event | search.completed | Which event fired. |
X-DME-Delivery | mn776zwave3z9p7ncdcta4ywph856e9m | Unique ID for this delivery attempt. Idempotency key. |
X-DME-Signature | t=1776709919,v1=982232... | Timestamp and HMAC-SHA256 hex signature. |
Content-Type | application/json | |
User-Agent | decisionmaker.email-webhooks/1.0 |
Verifying the signature
The signing input is <timestamp>\n<raw-body>. Compute HMAC-SHA256 with your secret and compare with a timing-safe equal.
Node
import crypto from "node:crypto";
export function verify(req, secret) {
const header = req.headers["x-dme-signature"]; // "t=...,v1=..."
const parts = Object.fromEntries(
header.split(",").map((kv) => kv.split("=")),
);
const signingInput = `${parts.t}\n${req.rawBody}`;
const expected = crypto
.createHmac("sha256", secret)
.update(signingInput)
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(parts.v1),
);
}Python
import hmac, hashlib
def verify(headers, raw_body, secret):
header = headers["x-dme-signature"] # "t=...,v1=..."
parts = dict(kv.split("=", 1) for kv in header.split(","))
signing_input = f"{parts['t']}\n{raw_body}"
expected = hmac.new(
secret.encode(),
signing_input.encode(),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Always use the raw request body, not a re-serialized JSON object. Any whitespace difference will break the signature.
Retries
Failed deliveries retry with exponential backoff:
| Attempt | Delay |
|---|---|
| 2 | 60s |
| 3 | 120s |
| 4 | 240s |
| 5 | 480s |
| 6 | 960s |
After 6 attempts, the delivery is marked failed and dropped.
Auto-disable
If an endpoint accumulates 20 consecutive failed deliveries, it is automatically disabled. Delete and recreate to re-enable.
Replay protection
The timestamp is part of the signing input, so an attacker cannot replay an old payload with a new timestamp. Still, reject deliveries with a timestamp older than 5 minutes on your side to limit the replay window if a secret leaks.