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.
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.staging.didit.me/session/11111111-2222-3333-4444-555555555555",
"status": "Declined",
"vendor_data": "11111111-1111-1111-1111-111111111111",
"callback": "https://verify.staging.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"
},
"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"
}