Webhooks
Webhooks allow your application to receive real-time notifications about changes to a verification status. Here's how you can configure and handle these notifications securely.
Configuring the Webhook Endpoint
1. Follow Steps 1 and 2 from the Quick Start Guide
- Refer to the Quick Start Guide to set up your team and application if you haven't already.
2. Add Your Webhook URL and Copy the Webhook Secret Key
- Go to your verification settings.
- Enter your webhook URL.
- Copy the
Webhook Secret Key
, which you'll use to validate incoming requests.

Webhook Types
We send webhooks in the following scenarios:
- Session Starts – When a new verification session begins, we immediately send its initial status.
- Status Changes – Whenever the verification status is updated (e.g., Approved, Declined, In Review, Abandoned).
If the status is one of Approved, Declined, In Review, or Abandoned, the webhook includes a decision
field with detailed verification information. The vendor_data
field is also included, if applicable.
Only webhook events created in the last 30 days can be retrieved.
Code Examples
To ensure the security of your webhook endpoint, verify the authenticity of incoming requests using your Webhook Secret Key
. The most important step is to always sign and verify the exact raw JSON—any re-stringification can alter the payload and invalidate the signature.
Always store and HMAC the raw JSON string (rather than re-stringifying after parsing). Differences in whitespace, float formatting, or key ordering will break the signature verification.
const express = require("express");
const bodyParser = require("body-parser");
const crypto = require("crypto");
const app = express();
const PORT = process.env.PORT || 1337;
// Load the webhook secret from your environment (or config)
const WEBHOOK_SECRET_KEY = process.env.WEBHOOK_SECRET_KEY || "YOUR_WEBHOOK_SECRET_KEY";
// 1) Capture the raw body
app.use(
bodyParser.json({
verify: (req, res, buf, encoding) => {
if (buf && buf.length) {
// Store the raw body in the request object
req.rawBody = buf.toString(encoding || "utf8");
}
},
})
);
// 2) Define the webhook endpoint
app.post("/webhook", (req, res) => {
try {
const signature = req.get("X-Signature");
const timestamp = req.get("X-Timestamp");
// Ensure all required data is present
if (!signature || !timestamp || !req.rawBody || !WEBHOOK_SECRET_KEY) {
return res.status(401).json({ message: "Unauthorized" });
}
// 3) Validate the timestamp to ensure the request is fresh (within 5 minutes)
const currentTime = Math.floor(Date.now() / 1000);
const incomingTime = parseInt(timestamp, 10);
if (Math.abs(currentTime - incomingTime) > 300) {
return res.status(401).json({ message: "Request timestamp is stale." });
}
// 4) Generate an HMAC from the raw body using your shared secret
const hmac = crypto.createHmac("sha256", WEBHOOK_SECRET_KEY);
const expectedSignature = hmac.update(req.rawBody).digest("hex");
// 5) Compare using timingSafeEqual for security
const expectedSignatureBuffer = Buffer.from(expectedSignature, "utf8");
const providedSignatureBuffer = Buffer.from(signature, "utf8");
if (
expectedSignatureBuffer.length !== providedSignatureBuffer.length ||
!crypto.timingSafeEqual(expectedSignatureBuffer, providedSignatureBuffer)
) {
return res.status(401).json({
message: `Invalid signature. Computed (${expectedSignature}), Provided (${signature})`,
});
}
// 6) Parse the JSON and proceed (signature is valid at this point)
const jsonBody = JSON.parse(req.rawBody);
const { session_id, status, vendor_data } = jsonBody;
// Example: upsert to database, handle "Approved" status, etc.
// e.g. upsertVerification(session_id, status, vendor_data);
return res.json({ message: "Webhook event dispatched" });
} catch (error) {
console.error("Error in /webhook handler:", error);
return res.status(401).json({ message: "Unauthorized" });
}
});
// Start the server
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
});
Webhook Body Object Examples
The webhook payload varies depending on the status of the verification session. When the status is Approved
or Declined
, the body includes the decision
field. For all other statuses, the decision
field is not present.
Example with decision
Field
{
"session_id": "11111111-2222-3333-4444-555555555555",
"status": "Declined", // status of the verification session
"created_at": 1627680000,
"timestamp": 1627680000,
"vendor_data": "11111111-1111-1111-1111-111111111111",
"decision": {
"session_id": "11111111-2222-3333-4444-555555555555",
"session_number": 43762,
"session_url": "https://verify.didit.me/session/11111111-2222-3333-4444-555555555555",
"status": "Declined",
"vendor_data": "11111111-1111-1111-1111-111111111111",
"callback": "https://verify.didit.me/",
"features": "OCR + FACE",
"kyc": {
"status": "Approved",
"ocr_status": "Approved",
"epassport_status": "Approved",
"document_type": "Passport",
"document_number": "BK123456",
"personal_number": "999999999",
"portrait_image": "https://example.com/portrait.jpg",
"front_image": "https://example.com/front.jpg",
"front_video": "https://example.com/front.mp4",
"back_image": null,
"back_video": null,
"full_front_image": "https://example.com/full_front.jpg",
"full_back_image": null,
"date_of_birth": "1990-01-01",
"expiration_date": "2026-03-24",
"date_of_issue": "2019-03-24",
"issuing_state": "ESP",
"issuing_state_name": "Spain",
"first_name": "Sergey",
"last_name": "Kozlov",
"full_name": "Sergey Kozlov",
"gender": "M",
"address": null,
"formatted_address": null,
"is_nfc_verified": false,
"parsed_address": null,
"place_of_birth": "Madrid, Spain",
"marital_status": "SINGLE",
"nationality": "ESP",
"created_at": "2024-07-28T06:46:39.354573Z"
},
"aml": {
"status": "In Review",
"total_hits": 1,
"score": 70.35, // score of the highest hit from 0 to 100
"hits": [
{
"id": "aaaaaaa-1111-2222-3333-4444-555555555555",
"match": false,
"score": 0.7034920634920635, // score of the hit from 0 to 1
"target": true,
"caption": "Kozlov Sergey Alexandrovich",
"datasets": ["ru_acf_bribetakers"],
"features": {
"person_name_jaro_winkler": 0.8793650793650793,
"person_name_phonetic_match": 0.5
},
"last_seen": "2024-07-20T17:53:03",
"first_seen": "2023-06-23T12:02:51",
"properties": {
"name": ["Kozlov Sergey Alexandrovich"],
"alias": ["Козлов Сергей Александрович"],
"notes": [
"Assistant Prosecutor of the Soviet District of Voronezh. Involved in the case against the Ukrainian pilot Nadiya Savchenko"
],
"gender": ["male"],
"topics": ["poi"],
"position": [
"Organizers of political repressions",
"Организаторы политических репрессий"
]
},
"last_change": "2024-02-27T17:53:01"
}
]
},
"face": {
"status": "Approved",
"face_match_status": "Approved",
"liveness_status": "Approved",
"face_match_similarity": 97.99,
"liveness_confidence": 87.99,
"source_image": "https://example.com/source.jpg",
"target_image": "https://example.com/target.jpg",
"video_url": "https://example.com/video.mp4",
"age_estimation": 24.3,
"gender_estimation": {
"male": 99.23,
"female": 0.77
}
},
"location": {
"device_brand": "Apple",
"device_model": "iPhone",
"browser_family": "Mobile Safari",
"os_family": "iOS",
"platform": "mobile",
"ip_country": "Spain",
"ip_country_code": "ES",
"ip_state": "Barcelona",
"ip_city": "Barcelona",
"latitude": 41.4022,
"longitude": 2.1407,
"ip_address": "83.50.226.71",
"isp": null,
"organization": null,
"is_vpn_or_tor": false,
"is_data_center": false,
"time_zone": "Europe/Madrid",
"time_zone_offset": "+0100",
"status": "Approved",
"document_location": {
"latitude": 4,
"longitude": -72
},
"ip_location": {
"longitude": 2.1407,
"latitude": 41.4022
},
"distance_from_document_to_ip_km": {
"distance": 8393.68,
"direction": "NE"
}
},
"warnings": [
{
"feature": "AML",
"risk": "POSSIBLE_MATCH_FOUND",
"additional_data": null,
"log_type": "warning",
"short_description": "Possible match found in AML screening",
"long_description": "The Anti-Money Laundering (AML) screening process identified potential matches with watchlists or high-risk databases, requiring further review."
}
],
"reviews": [
{
"user": "compliance@example.com",
"new_status": "Declined",
"comment": "Possible match found in AML screening",
"created_at": "2024-07-18T13:29:00.366811Z"
}
],
"extra_images": [],
"created_at": "2024-07-24T08:54:25.443172Z"
}
}
For a complete list of possible properties and their values for the decision
field, please refer to our API
Reference.
Example without decision
Field
{
"session_id": "11111111-2222-3333-4444-555555555555",
"status": "Kyc Expired",
"created_at": 1627680000,
"timestamp": 1627680000,
"vendor_data": "11111111-1111-1111-1111-111111111111"
}