Skip to main content

Webhooks

Webhooks deliver verification results to your server in real time. When a verification completes, is approved, is rejected, or triggers a fraud alert, Kora IDV sends an HTTP POST to your configured endpoint.

Setup

You can configure webhooks in two ways:

  1. Global endpoint — Set a webhook URL in your dashboard settings to receive all events for your tenant
  2. Per-verification callback — Pass a callbackUrl when creating a verification to receive events for that specific verification

If both are configured, events are delivered to both endpoints.

Payload format

All webhook payloads follow the same structure:

{
"id": "evt_def456",
"eventType": "verification.completed",
"resourceType": "verification",
"resourceId": "ver_abc123",
"tenantId": "your-tenant-uuid",
"timestamp": "2025-01-15T10:35:00Z",
"data": {
"verificationId": "ver_abc123",
"externalId": "user-123",
"status": "verified",
"decision": "auto_approve",
"decisionReason": "All checks passed",
"overallScore": 96.3
}
}

Signature verification

Every webhook includes the following headers:

HeaderValue
X-SignatureHex-encoded HMAC-SHA256 of "{timestamp}.{body}"
X-Signature-Algorithmsha256
X-TimestampUnix-seconds timestamp at which the event was signed
X-Webhook-IDUnique event ID (matches the body's id field)
X-Event-TypeThe event type (e.g. verification.completed)

The signed payload is the literal concatenation "{timestamp}.{body}" (timestamp, a literal period, then the raw request body). The timestamp is bound into the signature to prevent replay attacks — reject any webhook whose timestamp is older than your tolerance window (5 minutes is a sensible default).

Use the official server SDKs whenever possible — they implement the signature scheme and timestamp tolerance correctly:

import express from "express";
import { verifyWebhookSignature, parseWebhookPayload } from "@koraidv/node";

app.post(
"/webhooks/koraidv",
express.raw({ type: "application/json" }), // raw body required
(req, res) => {
const isValid = verifyWebhookSignature(
req.body, // raw body (Buffer)
req.headers["x-signature"], // hex signature
req.headers["x-timestamp"], // unix seconds
process.env.KORAIDV_WEBHOOK_SECRET,
);
if (!isValid) return res.status(401).end();

const event = parseWebhookPayload(req.body.toString("utf-8"));
// ... process event.eventType / event.data
res.status(200).end();
}
);

The verifier rejects webhooks older than 5 minutes by default. Override with { toleranceSeconds: 600 } as the 5th argument if you need a wider window.

warning

Always use timing-safe comparison functions (crypto.timingSafeEqual, hmac.compare_digest, hmac.Equal) to prevent timing attacks.

Event types

EventDescriptionTrigger
verification.createdNew verification session createdPOST /verifications
verification.completedAll verification steps processedPOST /verifications/{id}/complete
verification.verifiedVerification approved (auto or manual)Score above threshold or manual approval
verification.rejectedVerification rejectedCritical failure, sanctions hit, or manual rejection
verification.expiredSession expired before completion24-hour timeout
document.uploadedDocument image uploaded and processedPOST /verifications/{id}/document
document.verifiedDocument authenticity checks completedAfter document processing
liveness.completedLiveness session finishedAll challenges submitted
fraud_alert.createdFraud signal detectedDuplicate document, spoof, tampering

Example payloads

verification.completed

{
"id": "evt_def456",
"eventType": "verification.completed",
"resourceType": "verification",
"resourceId": "ver_abc123",
"tenantId": "your-tenant-uuid",
"timestamp": "2025-01-15T10:35:00Z",
"data": {
"verificationId": "ver_abc123",
"externalId": "user-123",
"status": "verified",
"decision": "auto_approve",
"decisionReason": "All checks passed",
"overallScore": 96.3
}
}

fraud_alert.created

{
"id": "evt_yza567",
"eventType": "fraud_alert.created",
"resourceType": "fraud_alert",
"resourceId": "fa_ghi789",
"tenantId": "your-tenant-uuid",
"timestamp": "2025-01-15T10:32:00Z",
"data": {
"alertId": "fa_ghi789",
"verificationId": "ver_abc123",
"alertType": "document_tampered",
"severity": "critical",
"description": "Document tampering detected: PHOTO_REPLACED"
}
}

Retry behavior

Failed deliveries (non-2xx response or timeout) are retried up to 5 times with exponential backoff:

AttemptDelay
11 minute
25 minutes
330 minutes
42 hours
524 hours

After 3 consecutive failures, you'll receive an email notification. Failed deliveries can also be manually retried from the webhook logs in your dashboard.

Best practices

  • Return 200 quickly — Process webhooks asynchronously. Return a 200 status immediately, then process the event in a background job.
  • Handle duplicates — Use the event id field for idempotency. The same event may be delivered more than once.
  • Verify signatures — Always verify the X-Signature header (and reject webhooks older than your tolerance window) before trusting the payload.
  • Log payloads — Store raw webhook payloads for debugging and audit trails.
  • Use HTTPS — Webhook endpoints must use HTTPS in production.