Documentation Index
Fetch the complete documentation index at: https://docs.verisecid.com/llms.txt
Use this file to discover all available pages before exploring further.
title: “Webhooks”
description: “Receive submission and verification events for hosted KYC sessions”
icon: “link”
Webhooks notify your server about submission creation and final verification outcomes for hosted sessions.
- Enable webhooks in Console → Workspace Settings → Notifications
- Set
webhookUrl (HTTPS)
- A signing
webhookSecret is stored per workspace
- Choose events to receive
Security
Each request includes an HMAC signature header using your webhookSecret.
X-KYC-Signature: sha256=<hex-hmac-of-body>
Verify the signature by recomputing HMAC‑SHA256 over the exact raw body JSON string and comparing constant‑time to the header value.
Event delivery
- Sent as JSON over HTTPS to your configured endpoint
- At‑least‑once delivery — de‑duplicate using the
id field
- Events are filtered by your workspace
webhookEvents allow‑list
- Monitor deliveries in your logs; retries/backoff may be added in the future
Event types
submission.created — Emitted immediately after a submission is persisted
verification.completed — Hosted session finished with pass
verification.failed — Hosted session finished with fail
KYC status
In addition to the legacy session lifecycle (not_started → processing → completed/failed), webhook events include a high-level KYC status for business decisions:
- Approved: AI pass and auto-approve enabled
- Submitted: AI pass and auto-approve disabled (awaiting manual decision)
- Declined: AI fail, or manual rejection
Payload shape
{
"type": "submission.created | verification.completed | verification.failed",
"id": "<eventType>:<submissionId>",
"timestamp": "2025-08-10T21:50:43.235Z",
"status": "Approved | Submitted | Declined",
"data": {
"session": {
"id": "<sessionId>",
"status": "Approved | Submitted | Declined",
"kycStatus": "not_started | processing | completed | failed",
"redirectUrl": "https://...",
"metadata": {},
"createdAt": { "_seconds": 0, "_nanoseconds": 0 },
"updatedAt": { "_seconds": 0, "_nanoseconds": 0 },
"images": {
"documentFrontUrl": "https://...",
"documentBackUrl": "https://...",
"selfieUrls": ["https://...", "https://...", "https://..."]
}
},
"submission": {
"id": "<submissionId>",
"sessionId": "<sessionId>",
"workspaceId": "<workspaceId>",
"pass": true,
"expectedDocumentType": "passport | id-card",
"detectedDocumentType": "passport | id-card | unknown",
"document": {},
"face": {},
"overall": { "pass": true, "reasons": [], "user_message": null },
"kycStatus": "Approved | Submitted | Declined",
"createdAt": { "_seconds": 0, "_nanoseconds": 0 }
}
}
}
We do not include raw image bytes in webhook payloads. Use the signed URLs for access control or copy to your storage if needed.
Test delivery
Use the API to trigger a test event:
POST /v1/kyc/webhooks/test
Body:
{ "workspaceId": "<workspaceId>" }
Response (200): { ok: true, status: 200 } for successful delivery. Failures respond with { ok: false, ... } but still HTTP 200.
Example handler
import type { NextApiRequest, NextApiResponse } from 'next';
import crypto from 'crypto';
function verifySignature(rawBody: string, header: string | undefined, secret: string) {
if (!header) return false;
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== 'POST') return res.status(405).end();
const signature = req.headers['x-kyc-signature'] as string | undefined;
const secret = process.env.KYC_WEBHOOK_SECRET || '';
const raw = (req as any).rawBody ?? JSON.stringify(req.body);
if (!verifySignature(raw, signature, secret)) return res.status(401).json({ error: 'invalid_signature' });
const evt = req.body as { type: string; id: string; timestamp: string; data: any };
// idempotent handling
// upsert evt.id to a processed store before acting
switch (evt.type) {
case 'submission.created':
break;
case 'verification.completed':
break;
case 'verification.failed':
break;
default:
// ignore
}
return res.status(200).json({ received: true });
}