Webhooks
Webhooks allow your application to receive real-time notifications about changes to a verification session status on web app workflows. Here's how you can configure and handle these notifications securely.
Configuring the Webhook Endpoint
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.
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 with a
status
ofNot Started
. - 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 workflow_id
, vendor_data
, metadata
, and session_id
fields are also included.
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, workflow_id } = jsonBody;
// Example: upsert to database, handle "Approved" status, etc.
// e.g. upsertVerification(session_id, status, vendor_data, workflow_id);
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 without decision
Field
{
"session_id": "11111111-2222-3333-4444-555555555555",
"status": "In Progress",
"created_at": 1627680000,
"timestamp": 1627680000,
"workflow_id": "11111111-2222-3333-4444-555555555555",
"vendor_data": "11111111-1111-1111-1111-111111111111",
"metadata": {
"user_type": "premium",
"account_id": "ABC123"
},
}
Example with decision
Field
{
"session_id": "11111111-2222-3333-4444-555555555555",
"status": "Declined", // status of the verification session
"created_at": 1627680000,
"timestamp": 1627680000,
"workflow_id": "11111111-2222-3333-4444-555555555555",
"vendor_data": "11111111-1111-1111-1111-111111111111",
"metadata": {
"user_type": "premium",
"account_id": "ABC123"
},
"decision": {
"session_id": "11111111-2222-3333-4444-555555555555",
"session_number": 43762,
"session_url": "https://verify.didit.me/session/11111111-2222-3333-4444-555555555555",
"status": "In Review",
"workflow_id": "11111111-2222-3333-4444-555555555555",
"features": ["ID_VERIFICATION", "NFC", "LIVENESS", "FACE_MATCH", "POA", "PHONE", "AML", "IP_ANALYSIS"],
"vendor_data": "11111111-1111-1111-1111-111111111111",
"metadata": {
"user_type": "premium",
"account_id": "ABC123"
},
"callback": "https://verify.didit.me/",
// optional field if ID_VERIFICATION feature is enabled
"id_verification": {
"status": "Approved",
"document_type": "Identity Card",
"document_number": "CAA000000",
"personal_number": "99999999R",
"portrait_image": "https://example.com/portrait.jpg",
"front_image": "https://example.com/front.jpg",
"front_video": "https://example.com/front.mp4",
"back_image": "https://example.com/back.jpg",
"back_video": "https://example.com/back.mp4",
"full_front_image": "https://example.com/full_front.jpg",
"full_back_image": "https://example.com/full_back.jpg",
"date_of_birth": "1980-01-01",
"age": 45,
"expiration_date": "2031-06-02",
"date_of_issue": "2021-06-02",
"issuing_state": "ESP",
"issuing_state_name": "Spain",
"first_name": "Carmen",
"last_name": "Española Española",
"full_name": "Carmen Española Española",
"gender": "F",
"address": "Avda de Madrid 34, Madrid, Madrid",
"formatted_address": "Avda de Madrid 34, Madrid, Madrid 28822, Spain",
"place_of_birth": "Madrid",
"marital_status": "Single",
"nationality": "ESP",
"parsed_address": {
"id": "7c6280a2-fb6a-4258-93d5-2ac987cbc6ba",
"city": "Madrid",
"label": "Spain ID Card Address",
"region": "Madrid",
"street_1": "Avda de Madrid 34",
"street_2": null,
"postal_code": "28822",
"raw_results": {
"geometry": {
"location": {
"lat": 37.4222804,
"lng": -122.0843428
},
"location_type": "ROOFTOP",
"viewport": {
"northeast": {
"lat": 37.4237349802915,
"lng": -122.083183169709
},
"southwest": {
"lat": 37.4210370197085,
"lng": -122.085881130292
}
}
},
},
},
"extra_files": [
"https://example.com/extra_id_verification.jpg",
],
"warnings": [
{
"risk": "QR_NOT_DETECTED",
"additional_data": null,
"log_type": "information",
"short_description": "QR not detected",
"long_description": "The system couldn't find or read the QR code on the document, which is necessary for document verification. This could be due to poor image quality or an unsupported document type.",
}
],
},
// optional field if NFC feature is enabled
"nfc": {
"status": "In Review",
"portrait_image": "https://example.com/portrait.jpg",
"signature_image": "https://example.com/signature.jpg",
"chip_data": {
"document_type": "ID",
"issuing_country": "ESP",
"document_number": "123456789",
"expiration_date": "2030-01-01",
"first_name": "John",
"last_name": "Smith",
"birth_date": "1990-05-15",
"gender": "M",
"nationality": "ESP",
"address": "CALLE MAYOR 123 4B, MADRID, MADRID",
"place_of_birth": "MADRID, MADRID"
},
"authenticity": {
"sod_integrity": true,
"dg_integrity": true
},
"certificate_summary": {
"issuer": "Common Name: CSCA SPAIN, Serial Number: 3, Organization: DIRECCION GENERAL DE LA POLICIA, Country: ES",
"subject": "Common Name: DS n-eID SPAIN 2, Organizational Unit: PASSPORT, Organization: DIRECCION GENERAL DE LA POLICIA, Country: ES",
"serial_number": "118120836164494130086420187336801405660",
"not_valid_after": "2031-02-18 10:21:11",
"not_valid_before": "2020-11-18 10:21:11"
},
"warnings": [
{
"risk": "DATA_INCONSISTENT",
"additional_data": null,
"log_type": "warning",
"short_description": "OCR and NFC mrz code extracted are not the same",
"long_description": "The Optical Character Recognition (OCR) data and the NFC chip data don't match, indicating potential document tampering or data inconsistency."
}
],
},
// optional field if LIVENESS feature is enabled
"liveness": {
"status": "Approved",
"method": "ACTIVE_3D",
"score": 89.92,
"reference_image": "https://example.com/reference.jpg",
"video_url": "https://example.com/video.mp4",
"age_estimation": 24.3,
"warnings": [
{
"risk": "LOW_LIVENESS_SCORE",
"additional_data": null,
"log_type": "information",
"short_description": "Low liveness score",
"long_description": "The liveness check resulted in a low score, indicating potential use of non-live facial representations or poor-quality biometric data."
}
]
},
// optional field if FACE_MATCH feature is enabled
"face_match": {
"status": "In Review",
"score": 65.43,
"source_image": "https://example.com/source-image.jpg",
"target_image": "https://example.com/target-image.jpg",
"warnings": [
{
"risk": "LOW_FACE_MATCH_SIMILARITY",
"additional_data": null,
"log_type": "warning",
"short_description": "Low face match similarity",
"long_description": "The facial features of the provided image don't closely match the reference image, suggesting a potential identity mismatch."
}
]
},
// optional field if PHONE feature is enabled
"phone": {
"status": "Approved",
"phone_number_prefix": "+34",
"phone_number": "600600600",
"full_number": "+34600600600",
"country_code": "ES", // ISO 3166-1 alpha-2
"country_name": "Spain",
"carrier": {
"name": "Orange",
"type": "mobile"
},
"is_disposable": false,
"is_virtual": false,
"verification_method": "sms",
"verification_attempts": 1,
"verified_at": "2024-07-28T06:47:35.654321Z",
"warnings": [],
},
// optional field if POA feature is enabled
"poa": {
"status": "Approved",
"document_type": "Bank Statement",
"issuer": "National Bank",
"issue_date": "2024-05-15",
"document_language": "EN",
"name_on_document": "John A. Smith",
"name_match_score": 92.5,
"address": "123 Main St, Apartment 4B, New York, NY 10001",
"formatted_address": "123 Main St, Apartment 4B, New York, NY 10001, USA",
"parsed_address": {
"street_1": "123 Main St",
"street_2": "Apartment 4B",
"city": "New York",
"region": "NY",
"postal_code": "10001",
"raw_results": {
"geometry": {
"location": {
"lat": 40.7128,
"lng": -74.0060
}
}
}
},
"document_file": "https://example.com/poa_document.pdf",
"extra_files": [
"https://example.com/extra_poa.pdf",
],
"warnings": [],
},
// optional field if AML feature is enabled
"aml": {
"status": "In Review",
"total_hits": 1,
"hits": [
{
"id": "cl-info-probidad-3fd0f04facc53bd94ebec9aaedf56d18",
"match": false,
"score": 0.4,
"target": true,
"caption": "PABLO ALFONSO ESCOBAR NAVARRO",
"datasets": ["cl_info_probidad"],
"features": {
"country_mismatch": 1.0,
"person_name_phonetic_match": 0.67
},
"last_seen": "2024-10-28T02:50:03",
"first_seen": "2024-01-17T02:50:01",
"properties": {
"name": ["PABLO ALFONSO ESCOBAR NAVARRO"],
"topics": ["role.pep"],
"country": ["cl"]
}
}
],
"score": 40.0,
"screened_data": {
"full_name": "Pablo Escobar",
"nationality": "COL",
"date_of_birth": "1975-12-01",
"document_number": "CAA000000"
},
"warnings": [
{
"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."
}
],
},
// optional field if IP_ANALYSIS feature is enabled
"ip_analysis": {
"status": "Approved",
"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",
"locations_info": {
"ip": {
"location": {"latitude": 40.2206327, "longitude": 1.5770097},
"distance_from_id_document": 23.4,
"distance_from_poa_document": 12.3
},
"id_document": {
"location": {"latitude": 41.2706327, "longitude": 1.9770097},
"distance_from_ip": 23.4,
"distance_from_poa_document": 18.7,
},
"poa_document": {
"location": {"latitude": 41.2706327, "longitude": 1.9770097},
"distance_from_ip": 12.3,
"distance_from_id_document": 18.7
},
},
"warnings": [],
},
"reviews": [
{
"user": "compliance@example.com",
"new_status": "Declined",
"comment": "Possible match found in AML screening",
"created_at": "2024-07-18T13:29:00.366811Z"
}
],
"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.