Verdict Capture Webhooks
Get verification and inspection events pushed to your backend as they happen. Every delivery is signed with HMAC-SHA256, retried if your endpoint is down, and carries a delivery id for deduplication. The receivers below run against real signed deliveries in our test suite.
Quick Start
1. Install
# 1. In the Verdict Capture dashboard: Settings -> Webhooks -> Add an endpoint (HTTPS URL)
#
# 2. Choose its events:
# - verification.completed a photo check finished, compliant or not
# - inspection.submitted an inspection link was submitted and processed
# - inspection.reviewed someone recorded a review decision on an inspection
# - inspection.decision a lending inspection got its automatic decision
#
# 3. Copy the signing secret (whsec_...). It is shown once.
# "Send test event" delivers a signed webhook.test to the endpoint.2. Use
const crypto = require("node:crypto");
const express = require("express");
const { Pool } = require("pg");
const db = new Pool({ connectionString: process.env.DATABASE_URL });
const app = express();
const SIGNING_SECRET = process.env.VERIFY_AI_WEBHOOK_SECRET; // whsec_...
// Create this PostgreSQL table before starting the receiver:
// CREATE TABLE verifyai_webhook_inbox (
// delivery_id text PRIMARY KEY, event text NOT NULL, payload jsonb NOT NULL,
// received_at timestamptz NOT NULL DEFAULT now(), processed_at timestamptz
// );
// A worker polls rows WHERE processed_at IS NULL and marks them only after
// successful processing. Retry failed work; make business effects idempotent.
// If forwarding to another queue, mark processed only after durable enqueue.
// Never store a duplicate receipt separately from the work obligation.
// X-VerifyAI-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<raw body>">
function verifyVerifyAISignature(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
if (typeof rawBody !== "string" && !Buffer.isBuffer(rawBody)) return false;
let t = "";
let v1 = "";
for (const part of String(signatureHeader || "").split(",")) {
const [key, value = ""] = part.trim().split("=");
if (key === "t") t = value;
if (key === "v1") v1 = value;
}
if (!/^[0-9]+$/.test(t) || !/^[0-9a-f]{64}$/i.test(v1)) return false;
// A timestamp more than five minutes from now is a replay: reject it.
if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false;
const expected = crypto
.createHmac("sha256", secret)
.update(t + ".")
.update(rawBody) // the exact bytes received, never re-serialized JSON
.digest();
return crypto.timingSafeEqual(expected, Buffer.from(v1, "hex"));
}
app.post("/webhooks/verifyai", express.raw({ type: "application/json" }), async (req, res) => {
if (!verifyVerifyAISignature(req.body, req.get("X-VerifyAI-Signature"), SIGNING_SECRET)) {
return res.status(401).send("invalid signature");
}
const deliveryId = req.get("X-VerifyAI-Delivery");
if (!deliveryId) return res.status(400).send("missing delivery id");
let payload;
try { payload = JSON.parse(req.body.toString("utf8")); }
catch { return res.status(400).send("invalid JSON"); }
const { event, data } = payload;
if (event === "webhook.test") return res.status(200).send("ok");
try {
// verification.completed, inspection.submitted, inspection.reviewed,
// inspection.decision (keep the highest data.sequence per inspection_id).
await db.query("INSERT INTO verifyai_webhook_inbox (delivery_id, event, payload) VALUES ($1, $2, $3::jsonb) ON CONFLICT (delivery_id) DO NOTHING", [deliveryId, event, JSON.stringify(data)]);
return res.status(200).send("ok"); // committed or already durably recorded
} catch {
return res.status(503).send("inbox unavailable"); // Verify will retry
}
});
app.listen(3000);Features
Signed Payloads
X-Verdict Capture-Signature carries t=<unix seconds>,v1=<hex HMAC-SHA256 of t.rawBody>, keyed by your endpoint's secret. Verify it before trusting a payload.
Automatic Retries
A delivery that doesn't get a 2xx within 5 seconds is retried after 1 minute and again 5 minutes later, three attempts in total. Failed deliveries can be redelivered from the dashboard.
Event Filtering
Each endpoint subscribes only to the events it needs: verification.completed, inspection.submitted, inspection.reviewed or inspection.decision.
Replay Protection
Every attempt is signed with a fresh timestamp. Reject a t more than five minutes from your clock, and deduplicate on X-Verdict Capture-Delivery, which every retry reuses.
Dashboard Redelivery
Each endpoint's delivery log shows recent deliveries, attempts and your endpoint's response. Redeliver a failed or retrying delivery with one click.
Multiple Endpoints
Add endpoints for different environments, services, or downstream consumers (CRM, ops, billing), each with its own secret and event list.
Code Examples
Requirements
- A public HTTPS endpoint (private and loopback addresses are refused)
- Your endpoint's signing secret (whsec_…), shown once when you add it
- Access to the raw request body, before any JSON parsing
- Idempotent processing keyed on X-Verdict Capture-Delivery (a delivery can arrive more than once)
Frequently Asked Questions
Stream Verification Events to Your Stack
Signed, retried, replay-protected webhooks for any backend. Start your free sandbox today.
Other ways to integrate
Mobile SDKs, server-side HTTP examples, the REST API, webhooks, and no-code automation platforms.