D

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

  1. You register an HTTPS endpoint on your server
  2. D-ME sends a signed POST request when verification events occur
  3. Your server verifies the HMAC signature, then processes the event
  4. Respond with 200 OK within 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

EventWhen it fires
verification.completedVerification finished with status approved
verification.failedVerification 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:

HeaderValue
d-me-signatureHMAC-SHA256 hex digest
d-me-timestampUnix 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 200 quickly — offload heavy processing to a background queue
  • Make your handler idempotent — the same event may be delivered more than once
  • Use external_ref to 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