# Webhooks (/docs/api/webhooks)



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 [#setting-up-an-endpoint]

Open the [API Keys dashboard](/dashboard/api-keys), 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 [#payload-shape]

```json
{
  "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 [#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 [#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 [#node]

```ts
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 [#python]

```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 [#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 [#auto-disable]

If an endpoint accumulates 20 consecutive failed deliveries, it is automatically disabled. Delete and recreate to re-enable.

Replay protection [#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.
