Webhooks Guide
Webhooks let D-ME push verification results to your server the moment they're ready. Instead of polling for status, your app reacts in real time.
How it works
- You register an HTTPS endpoint on your server
- D-ME sends a signed
POSTrequest when verification events occur - Your server verifies the HMAC signature, then processes the event
- Respond with
200 OKwithin 10 seconds to acknowledge delivery
If your server doesn't respond with
2xx within 10 seconds, D-ME retries with exponential backoff: 1 min, 5 min, 30 min, 2 hr, 8 hr. After 5 failed attempts, the webhook is marked failed.Step 1 — Register a webhook endpoint
cURL
curl -X POST https://api.d-id.me/v1/webhooks \
-H "Authorization: Bearer dme_live_xxxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/d-me",
"events": ["verification.completed", "verification.failed"],
"description": "Production KYC webhook"
}'Save the secret from the response — it's used to verify incoming requests:
{
"data": {
"id": "wh_01hxyz1234567890",
"url": "https://your-app.com/webhooks/d-me",
"events": ["verification.completed", "verification.failed"],
"secret": "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"active": true,
"created_at": "2026-05-01T10:00:00Z"
}
}The webhook secret is shown only once. Store it immediately in your environment variables as
DME_WEBHOOK_SECRET.Event types
| Event | When it fires |
|---|---|
verification.completed | Verification finished with status approved |
verification.failed | Verification finished with status declined or error |
Payload format
{
"event": "verification.completed",
"data": {
"id": "ver_01hxyz1234567890",
"status": "approved",
"country": "SN",
"id_type": "national_id",
"external_ref": "user_001",
"created_at": "2026-05-01T10:30:00Z",
"completed_at": "2026-05-01T10:30:25Z",
"score": null
},
"timestamp": "2026-05-01T10:30:25Z"
}Step 2 — Verify the signature
Every webhook includes two headers:
| Header | Value |
|---|---|
d-me-signature | HMAC-SHA256 hex digest |
d-me-timestamp | Unix timestamp (seconds) of event creation |
The signature is computed over timestamp.raw_body using your webhook secret. Always use timingSafeEqual to compare — never string equality.
import crypto from 'crypto';
export async function POST(request: Request) {
const rawBody = await request.text();
const signature = request.headers.get('d-me-signature') ?? '';
const timestamp = request.headers.get('d-me-timestamp') ?? '';
// Reconstruct the signed payload
const signedPayload = `${timestamp}.${rawBody}`;
// Compute expected signature
const expectedSig = crypto
.createHmac('sha256', process.env.DME_WEBHOOK_SECRET!)
.update(signedPayload)
.digest('hex');
// Compare (use timingSafeEqual to prevent timing attacks)
const sigBuffer = Buffer.from(signature, 'hex');
const expectedBuffer = Buffer.from(expectedSig, 'hex');
if (sigBuffer.length !== expectedBuffer.length ||
!crypto.timingSafeEqual(sigBuffer, expectedBuffer)) {
return new Response('Unauthorized', { status: 401 });
}
// Replay protection: reject events older than 5 minutes
const eventTime = parseInt(timestamp, 10);
if (Date.now() / 1000 - eventTime > 300) {
return new Response('Stale event', { status: 400 });
}
const event = JSON.parse(rawBody);
if (event.event === 'verification.completed') {
const { id, status, external_ref } = event.data;
// Update your DB: mark user verification as status
await db.users.updateVerification(external_ref, { verificationId: id, status });
}
return new Response('OK', { status: 200 });
}Replay protection
The d-me-timestamp header prevents replay attacks. Reject any event where |now - timestamp| > 300 (5 minutes). This is shown in all the examples above.
Step 3 — Test your endpoint
Use the test endpoint to fire a synthetic event to your registered webhook without creating a real verification:
cURL
curl -X POST https://api.d-id.me/v1/webhooks/wh_01hxyz1234567890/test \
-H "Authorization: Bearer dme_live_xxxx"You can also test from the dashboard webhooks page — click "Send test event" on any registered endpoint.
Best practices
- Always verify the HMAC signature before processing any payload
- Respond with
200quickly — offload heavy processing to a background queue - Make your handler idempotent — the same event may be delivered more than once
- Use
external_refto correlate webhook events with your internal user records - Store the raw body before parsing — you need it for signature verification
- Rotate webhook secrets when you rotate API keys