Webhooks
VerifyHuman sends signed HTTP POSTs to URLs you register per-project. Configure URLs + event types in the dashboard under each project's Webhooks tab, or programmatically via the MCP server.
Event types
verification.completed— terminal outcome of a verification session (pass OR fail). Default event for new webhooks.verification.passed— fires only on pass.verification.failed— fires only on fail.monitoring.flagged— behavioral monitoring flagged an in-session anomaly above the project's threshold.question.flagged— Phase 3 per-question quality threshold tripped.
Payload — v2 (recommended)
Set payloadVersion: "v2" when creating the webhook. v2 carries the full envelope including the respondent id, your custom metadata, and an ISO timestamp.
{
"event": "verification.completed",
"data": {
"session_id": "sess_abc123",
"project_id": "proj_xyz",
"status": "pass",
"scores": {
"liveness": 0.95,
"uniqueness": 0.98,
"authenticity": 0.92,
"overall": 0.95
},
"respondent_id": "resp_456",
"metadata": { "panel": "prolific", "sourceId": "src_123" },
"timestamp": "2026-05-19T17:42:00.000Z",
"fraud_signals": [],
"summary": "Verification passed. Real human detected, no prior participation detected in this study."
}
}Payload — v1 (legacy, default for old webhooks)
v1 strips respondent_id, metadata, timestamp, fraud_signals, summary, and risk for back-compat with handlers written before v2 existed. Pre-existing v1 webhooks continue to receive v1 payloads until you upgrade them.
Delivery headers
| Header | Description |
|---|---|
Content-Type | Always application/json. |
X-VerifyHuman-Event | Event type (e.g., verification.completed). Branch on this without parsing the body. |
X-VerifyHuman-Signature | sha256=<hex> HMAC over the raw body using the webhook signing secret. Verify before processing. |
X-VerifyHuman-Timestamp | Unix seconds. 5-minute skew tolerance recommended on your side. |
X-VerifyHuman-Idempotency-Key | Deterministic key derived from (webhook_id, event_type, session_id). Use for dedup; don't 4xx duplicates — that triggers another retry. |
X-VerifyHuman-Attempt | 1-indexed attempt number. 1 = first delivery; ≥2 = retry. Helps distinguish "first time we've seen this" from "we already saw this." |
Signature verification
HMAC-SHA256 over {timestamp}.{raw_body} using the webhook secret. Reference implementations:
// Node.js
const crypto = require('crypto');
function verifyWebhook(body, secret, signature, timestamp) {
const message = `${timestamp}.${body}`;
const expected = crypto.createHmac('sha256', secret)
.update(message).digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}# Python
import hashlib, hmac, secrets as secrets_module
def verify_webhook(body: str, secret: str, signature: str, timestamp: str) -> bool:
message = f"{timestamp}.{body}"
expected = hmac.new(
secret.encode(),
message.encode(),
hashlib.sha256,
).hexdigest()
return secrets_module.compare_digest(signature, expected)Retry behavior
VerifyHuman runs a durable delivery queue. Every fire is persisted as a webhook_delivery_attempts row that walks a state machine until terminal:
PENDING ──► SUCCEEDED (2xx response)
├─► FAILED (timeout / 5xx → retry scheduled)
└─► EXHAUSTED (5 attempts hit OR terminal 4xx)- Timeout: 10 seconds per attempt.
- Success: any 2xx status code.
- Retries: up to 5 attempts total (one initial + four retries) with exponential backoff (~30s, 2min, 10min, 30min, 2h).
- Terminal statuses — no retry:
400, 401, 403, 404, 405, 410, 422, 429. These mean "the request will never succeed as-is"; retrying would only amplify your-side problems. The attempt moves toEXHAUSTED. - Idempotency: use
X-VerifyHuman-Idempotency-Keyto dedupe. Returning 2xx for a duplicate is correct — 4xx-ing triggers another retry. - Ordering: best-effort. With retries, a later event for the same session can land before an earlier retry of a prior event. Sequence on
data.timestampif order matters.
Delivery history
Inspect attempts via the dashboard's Webhooks tab on each project, or programmatically:
GET /api/v1/projects/{project_id}/webhooks/{webhook_id}/deliveriesReturns status code, response body, duration, attempt number, and final state per attempt. Useful for diagnosing 4xx loops without grepping your own server logs.
Webhook secret format
32-byte random value, hex-encoded. Generated server-side at webhook creation; shown to you ONCE on the dashboard. If you lose it, rotate via the dashboard or via the MCP update_webhook tool — the old secret stops being accepted immediately.
Test deliveries
The dashboard's "Test delivery" button (or the MCP test_webhook tool) fires a syntheticverification.completed event at your endpoint with valid signature headers. Use this to validate signature verification + handler dedup before going live.