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

HeaderDescription
Content-TypeAlways application/json.
X-VerifyHuman-EventEvent type (e.g., verification.completed). Branch on this without parsing the body.
X-VerifyHuman-Signaturesha256=<hex> HMAC over the raw body using the webhook signing secret. Verify before processing.
X-VerifyHuman-TimestampUnix seconds. 5-minute skew tolerance recommended on your side.
X-VerifyHuman-Idempotency-KeyDeterministic key derived from (webhook_id, event_type, session_id). Use for dedup; don't 4xx duplicates — that triggers another retry.
X-VerifyHuman-Attempt1-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 to EXHAUSTED.
  • Idempotency: use X-VerifyHuman-Idempotency-Key to 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.timestamp if 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}/deliveries

Returns 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.