For AI agents: a documentation index is available at the root level at /llms.txt and /llms-full.txt. Append /llms.txt to any URL for a page-level index, or .md for the markdown version of any page.
  • Getting Started
    • Introduction
    • How Verifa Works
    • Quickstart
    • Choosing an Integration Method
  • Use Cases
    • KYC Onboarding
    • Age Verification
    • AML Compliance
    • Fraud Prevention
    • Marketplace Trust & Safety
  • Core Concepts
    • Overview
    • Sessions
    • Verifications & Checks
    • Workflows
    • Identities
    • Cases
    • Screening & Reports
    • Lists
  • Integration Guides
    • Overview
    • JavaScript SDK
    • Web Capture Flow
    • API-Only Integration
    • Mobile SDK
    • Webhooks Guide
    • MCP Server
    • Migrating from Persona
  • API Details
    • Overview
    • Authentication
    • Pagination
    • Rate Limiting
    • Versioning
    • Errors
    • Webhooks
    • Idempotency
    • Key Inflection
    • Data Access
    • Data Retention
  • Tutorials
    • Creating Your First Verification Session
    • Creating a Workflow
    • Receiving Webhooks & Validating Signatures
    • Handling Webhook Events
    • Custom Document Types & AI Extraction
  • Best Practices
    • Testing
    • Preventing Duplicates
    • Fraud Signals
    • Changelog
  • API Reference
On this page
  • Quick setup
  • 1. Create a webhook endpoint
  • 2. Handle events
  • 3. Test it
  • Event catalog
  • Session events
  • Screening events
  • Identity events
  • Consortium events
  • Case events
  • Document events
  • Quality assurance events
  • Event filtering
  • Payload format
  • Signature verification
  • Replay protection
  • Retry behavior
  • Circuit breaker
  • Managing endpoints
  • Rotate the signing secret
  • Clone an endpoint
  • View delivery history
  • Retry a failed delivery
  • Key inflection
  • Best practices
  • Related
Integration Guides

Webhooks Guide

Was this page helpful?
Previous

MCP Server

Next
Built with

Webhooks deliver real-time event notifications to your server when things happen in Verifa — sessions complete, screening finds matches, identities update, and more. This guide covers setup, security, event filtering, and best practices.

Quick setup

1. Create a webhook endpoint

$curl -X POST https://api.withverifa.com/api/v1/webhooks/endpoints \
> -H "X-API-Key: vk_live_your_key_here" \
> -H "Content-Type: application/json" \
> -d '{
> "url": "https://your-app.com/webhooks/verifa",
> "enabled_events": ["session.approved", "session.declined", "session.requires-review"],
> "description": "Production webhook"
> }'
1{
2 "id": "webhook_abc123",
3 "url": "https://your-app.com/webhooks/verifa",
4 "secret": "whsec_abc123def456...",
5 "enabled_events": ["session.approved", "session.declined", "session.requires-review"],
6 "enabled": true,
7 "created_at": "2026-02-01T12:00:00Z"
8}

Store the secret securely — you need it to verify webhook signatures. The whsec_* value is shown once at creation and again only when you rotate it. You cannot retrieve it later. Each endpoint has its own per-endpoint secret, so rotating one endpoint never affects another.

Per-endpoint signing secrets are mandatory for endpoints created on or after 2026-04-17. Endpoints created before that date may still fall back to the deprecated org-level webhook secret; rotate those endpoints to mint a dedicated whsec_* and stop relying on the org-wide value.

2. Handle events

The X-Verifa-Signature header uses the Stripe-style format t=<unix_ts>,v1=<hex_hmac>. The HMAC-SHA256 covers f"{t}.{raw_body}" and receivers should reject any delivery whose t= is more than 5 minutes from current time (replay protection). See the Tutorial: Receiving Webhooks & Validating Signatures for full code samples and a reference recipe.

JavaScript
Python
1const express = require("express");
2const crypto = require("crypto");
3
4const app = express();
5const TOLERANCE = 300; // 5-minute replay window
6
7function verify(rawBody, header, secret) {
8 const parts = Object.fromEntries(
9 (header || "").split(",").map((p) => {
10 const i = p.indexOf("=");
11 return i > 0 ? [p.slice(0, i), p.slice(i + 1)] : [p, ""];
12 })
13 );
14 const ts = parseInt(parts.t || "0", 10);
15 const v1 = parts.v1 || "";
16 if (!ts || !v1) return false;
17 if (Math.abs(Math.floor(Date.now() / 1000) - ts) > TOLERANCE) return false;
18
19 const signed = Buffer.concat([Buffer.from(`${ts}.`), rawBody]);
20 const expected = crypto
21 .createHmac("sha256", secret)
22 .update(signed)
23 .digest("hex");
24 const a = Buffer.from(expected);
25 const b = Buffer.from(v1);
26 return a.length === b.length && crypto.timingSafeEqual(a, b);
27}
28
29app.post("/webhooks/verifa", express.raw({ type: "application/json" }), (req, res) => {
30 const signature = req.headers["x-verifa-signature"];
31 if (!verify(req.body, signature, process.env.VERIFA_WEBHOOK_SECRET)) {
32 return res.status(401).send("Invalid or expired signature");
33 }
34
35 // Process event
36 const event = JSON.parse(req.body);
37 console.log(`Received ${event.event} for session ${event.data.session_id}`);
38
39 // Respond immediately — process asynchronously
40 res.status(200).send("OK");
41 processEvent(event);
42});

3. Test it

Send a test event to verify your endpoint is working:

$curl -X POST https://api.withverifa.com/api/v1/webhooks/endpoints/webhook_abc123/test \
> -H "X-API-Key: vk_live_your_key_here"
1{
2 "success": true,
3 "http_status": 200,
4 "url": "https://your-app.com/webhooks/verifa"
5}

Event catalog

Session events

EventDescription
session.createdA new verification session was created.
session.startedThe user opened the capture URL.
session.expiredThe capture token expired before completion.
session.approvedVerification approved (auto or manual).
session.declinedVerification rejected.
session.requires-reviewSession needs manual review.
session.awaiting_externalWorkflow paused at external gate.
session.redactedPII and documents permanently deleted.
session.retention-expiredRetention window elapsed; data auto-redacted.
session.link_generatedA shareable verification link was created.
session.updatedSession external_ref or metadata updated.
session.submittedSession submitted for processing after document capture.
session.resumedSession token regenerated via resume endpoint.
session.requires-second-reviewSession requires a second reviewer (dual review).
session.resubmission-requiredImage quality insufficient; user must re-submit.

Screening events

EventDescription
screening.completedA screening check finished.
screening.monitoring.new_hitsContinuous monitoring found new matches.
screening.monitoring.clearMonitoring run completed with no new hits.
check.completedA standalone check completed.
check.matchedA standalone check found matches.
check.monitoring.new_hitsCheck monitoring found new hits.
check.monitoring.clearCheck monitoring found no new hits.

Identity events

EventDescription
identity.createdNew identity record created.
identity.updatedIdentity data updated.
identity.flaggedIdentity flagged for review.
identity.archivedIdentity archived.
identity.restoredIdentity restored from archive.
identity.mergedTwo identity records merged.
identity.redactedIdentity PII permanently deleted.
identity.tag-addedTag added to identity.
identity.tag-removedTag removed from identity.
identity.relationship.createdIdentity relationship established.
identity.relationship.deletedIdentity relationship removed.

Consortium events

EventDescription
consortium.match_foundCross-org face match detected via the Verifa Network. Both involved orgs receive the event.

Case events

EventDescription
case.createdReview case created.
case.claimedCase claimed by a reviewer.
case.approvedCase approved (final decision).
case.rejectedCase rejected (final decision).
case.escalatedCase escalated to a higher reviewer.

Document events

EventDescription
document.uploadedDocument file uploaded.
document.classifiedDocument type detected.
document.extractedData extracted from document.
document.verifiedDocument verification completed.
document.comparedDocument compared against another.
document.redactedDocument permanently deleted.

Quality assurance events

EventDescription
smart_qa.finding.createdQA finding generated.
smart_qa.finding.criticalCritical QA finding detected.

Event filtering

Subscribe to specific events or use "*" to receive all events:

$curl -X PATCH https://api.withverifa.com/api/v1/webhooks/endpoints/webhook_abc123 \
> -H "X-API-Key: vk_live_your_key_here" \
> -H "Content-Type: application/json" \
> -d '{
> "enabled_events": ["session.approved", "session.declined", "screening.monitoring.new_hits"]
> }'

Payload format

All webhook payloads follow this structure:

1{
2 "event": "session.approved",
3 "data": {
4 "session_id": "session_abc123",
5 "external_ref": "user_abc123",
6 "status": "approved",
7 "is_sandbox": false,
8 "created_at": "2026-02-01T12:00:00Z"
9 },
10 "created_at": "2026-02-01T12:05:00Z"
11}

The data object contents vary by event type. Session events include session_id, external_ref, and status. Identity events include identity_id and the changed fields.

Signature verification

Every webhook includes an X-Verifa-Signature header in the Stripe-style format t=<unix_ts>,v1=<hex_hmac>:

  • t — Unix timestamp the signature was generated at
  • v1 — hex-encoded HMAC-SHA256 of f"{t}.{raw_request_body}", signed with your endpoint’s whsec_* secret

Always verify signatures to prevent spoofed events. Use the raw request body bytes (not parsed JSON) for verification, use a constant-time comparison function for the v1 value, and reject deliveries whose t= is more than 5 minutes from your current clock to prevent replay attacks. See the Tutorial: Receiving Webhooks & Validating Signatures for the full reference recipe and language-specific examples.

The verification helper at src/core/security.py:verify_webhook_signature also accepts the legacy bare-hex HMAC over the raw body for in-flight deliveries during the deprecation window. New outbound deliveries always use the timestamped t=,v1= format, so build new receivers against that format.

Replay protection

Receivers should layer the following defenses:

  1. Verify the signature using the t=,v1= recipe above — reject anything that doesn’t validate.
  2. Reject stale timestamps. If abs(now - t) > 300, drop the delivery even if the HMAC checks out. This is the same 5-minute window enforced by every Verifa client.
  3. Dedupe on idempotency_key. Verifa retries failed deliveries, and the same event may arrive more than once with the same idempotency_key in the payload body. Maintain a short-lived cache (Redis, DB row) keyed on idempotency_key to skip duplicates rather than hashing payloads.

Retry behavior

If your endpoint returns a non-2xx status or doesn’t respond within 30 seconds, Verifa retries with exponential backoff:

AttemptDelay
1st retry~30 seconds
2nd retry~2 minutes
3rd retry~8 minutes
4th retry~32 minutes
5th retry~2 hours

After 5 failed retries, the delivery is marked as failed.

Circuit breaker

If an endpoint accumulates 10 consecutive failed deliveries (all retries exhausted), Verifa automatically disables the endpoint and sends an email notification to your organization’s admins. Re-enable it in the dashboard or via API after fixing the issue.

Managing endpoints

Rotate the signing secret

$curl -X POST https://api.withverifa.com/api/v1/webhooks/endpoints/webhook_abc123/rotate-secret \
> -H "X-API-Key: vk_live_your_key_here"

Returns the new whsec_* secret in the response body. This is the only other time the secret is shown — store it immediately. The previous secret is invalidated as soon as rotation completes, so deploy the new value to your server before or immediately after rotating. Rotating one endpoint never affects another endpoint’s secret.

Clone an endpoint

Duplicate an endpoint with a new secret (useful for migration):

$curl -X POST https://api.withverifa.com/api/v1/webhooks/endpoints/webhook_abc123/clone \
> -H "X-API-Key: vk_live_your_key_here"

View delivery history

$curl https://api.withverifa.com/api/v1/webhooks/endpoints/webhook_abc123/deliveries \
> -H "X-API-Key: vk_live_your_key_here"

Retry a failed delivery

$curl -X POST https://api.withverifa.com/api/v1/webhooks/deliveries/webhookdlv_abc123/retry \
> -H "X-API-Key: vk_live_your_key_here"

Key inflection

Control the casing of payload keys per endpoint:

$curl -X PATCH https://api.withverifa.com/api/v1/webhooks/endpoints/webhook_abc123 \
> -H "X-API-Key: vk_live_your_key_here" \
> -H "Content-Type: application/json" \
> -d '{
> "key_inflection": "camel"
> }'

Options: snake (default), camel, kebab.

Best practices

  • Respond quickly. Return 200 immediately and process the event asynchronously. If your handler takes too long, the delivery may time out and trigger unnecessary retries.

  • Handle duplicates. Use the idempotency_key field on the payload body to deduplicate — the same key arrives on every retry of the same delivery. In rare cases (network issues, retries), you may receive the same event more than once.

  • Verify signatures and timestamps. Parse t= and v1= from the X-Verifa-Signature header, recompute the HMAC over f"{t}.{raw_body}", and reject any delivery whose t= is more than 5 minutes from now. Never process unverified events.

  • Exempt from CSRF. If your framework has CSRF protection, exempt your webhook endpoint path.

  • Use specific events. Subscribe only to the events you need rather than "*". This reduces noise and makes your handler simpler.

  • Monitor delivery health. Check the delivery history in the dashboard regularly. If you see frequent failures, investigate timeouts or errors on your endpoint.

Related

  • Webhooks — Quick reference for event types and payload format
  • Sessions — Session events and lifecycle
  • Screening & Reports — Screening event details
  • Errors — Error codes returned by webhook API endpoints