Dashboard

Webhooks

VerifyHuman sends real-time HTTP POST notifications to your server when events occur in your account. Use webhooks to automate workflows, sync data, and respond to verifications instantly.


Quick Start

1. Create an Endpoint

Go to Dashboard > Webhooks > Add Endpoint and enter:

2. Save Your Signing Secret

When you create an endpoint, a signing secret (prefixed whsec_) is displayed once. Copy and store it securely — you will need it to verify incoming webhook signatures.

3. Receive Events

VerifyHuman will POST a JSON payload to your URL whenever a subscribed event occurs. Your endpoint should:

4. Test Your Endpoint

Click Send Test Event on the endpoint detail page to send a test.ping event. This verifies connectivity and signature verification without affecting real data.


Event Envelope

Every webhook delivery sends a JSON body with this structure:

{
  "id": "e4b0c3a7-1234-4f5e-8a6b-9c0d1e2f3a4b",
  "type": "verification.completed",
  "created": "2025-01-15T14:30:00.000000Z",
  "environment": "live",
  "data": {
    "verification_id": "ver_abc123",
    "status": "PASS",
    "confidence": 92.5,
    "product": "verifyhuman"
  }
}
Field Type Description
id string Unique event ID (UUID). Stable across retries.
type string Event type (e.g., verification.completed).
created string ISO 8601 UTC timestamp of when the event was created.
environment string live or test.
data object Event-specific payload data.

Delivery Headers

Each webhook request includes these headers:

Header Example Description
X-VerifyHuman-Signature sha256=abc123... HMAC-SHA256 signature for payload verification.
X-VerifyHuman-Timestamp 1705312200 Unix timestamp (seconds) when the signature was generated.
X-VerifyHuman-Event verification.completed The event type.
X-VerifyHuman-Event-Id e4b0c3a7-... Unique event ID (matches id in the payload).
X-VerifyHuman-Delivery-Id d1e2f3a4-... Unique delivery attempt ID. Changes on retries.
X-VerifyHuman-Environment live live or test.
Content-Type application/json Always JSON.
User-Agent VerifyHuman-Webhooks/1.0 Identifies VerifyHuman as the sender.

Signature Verification

Every webhook is signed using your endpoint's signing secret. Always verify signatures to ensure the request came from VerifyHuman and has not been tampered with.

How Signing Works

  1. VerifyHuman creates the signed payload: {timestamp}.{json_body}
  2. Computes HMAC-SHA256 using your signing secret
  3. Sends the signature as sha256={hex_digest} in the X-VerifyHuman-Signature header

Python

import hmac
import hashlib
import time

WEBHOOK_SECRET = "whsec_your_secret_here"
MAX_TIMESTAMP_AGE = 300  # 5 minutes

def verify_webhook(request):
    signature = request.headers.get('X-VerifyHuman-Signature', '')
    timestamp = request.headers.get('X-VerifyHuman-Timestamp', '')
    body = request.get_data()

    # Reject stale timestamps
    try:
        ts = int(timestamp)
        if abs(time.time() - ts) > MAX_TIMESTAMP_AGE:
            return False
    except (ValueError, TypeError):
        return False

    # Reconstruct signed payload
    signed_payload = f'{timestamp}.'.encode('utf-8') + body

    # Compute expected signature
    expected = 'sha256=' + hmac.new(
        WEBHOOK_SECRET.encode('utf-8'),
        signed_payload,
        hashlib.sha256
    ).hexdigest()

    # Constant-time comparison
    return hmac.compare_digest(signature, expected)

Node.js / JavaScript

const crypto = require('crypto');

const WEBHOOK_SECRET = 'whsec_your_secret_here';
const MAX_TIMESTAMP_AGE = 300; // 5 minutes

function verifyWebhook(req) {
  const signature = req.headers['x-verifyhuman-signature'] || '';
  const timestamp = req.headers['x-verifyhuman-timestamp'] || '';
  const body = req.rawBody; // Use raw body, not parsed JSON

  // Reject stale timestamps
  const ts = parseInt(timestamp, 10);
  if (isNaN(ts) || Math.abs(Date.now() / 1000 - ts) > MAX_TIMESTAMP_AGE) {
    return false;
  }

  // Reconstruct signed payload
  const signedPayload = Buffer.concat([
    Buffer.from(`${timestamp}.`, 'utf-8'),
    Buffer.from(body)
  ]);

  // Compute expected signature
  const expected = 'sha256=' + crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(signedPayload)
    .digest('hex');

  // Constant-time comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature, 'utf-8'),
    Buffer.from(expected, 'utf-8')
  );
}

PHP

<?php
$webhookSecret = 'whsec_your_secret_here';
$maxTimestampAge = 300; // 5 minutes

$signature = $_SERVER['HTTP_X_VERIFYHUMAN_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_VERIFYHUMAN_TIMESTAMP'] ?? '';
$body = file_get_contents('php://input');

// Reject stale timestamps
$ts = intval($timestamp);
if (abs(time() - $ts) > $maxTimestampAge) {
    http_response_code(401);
    echo json_encode(['error' => 'Stale timestamp']);
    exit;
}

// Reconstruct signed payload
$signedPayload = $timestamp . '.' . $body;

// Compute expected signature
$expected = 'sha256=' . hash_hmac('sha256', $signedPayload, $webhookSecret);

// Constant-time comparison
if (!hash_equals($expected, $signature)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

// Signature valid — process the event
$event = json_decode($body, true);
$eventType = $event['type'];
// ... handle event
?>

Event Types

Verification Events

Event Trigger Category
verification.completed Any verification reaches a terminal state (PASS, FAIL, or ERROR) verification
verification.passed A verification completed with PASS status verification
verification.failed A verification completed with FAIL or ERROR status verification

Example payload (verification.completed):

{
  "id": "e4b0c3a7-1234-4f5e-8a6b-9c0d1e2f3a4b",
  "type": "verification.completed",
  "created": "2025-01-15T14:30:00.000000Z",
  "environment": "live",
  "data": {
    "verification_id": "ver_abc123",
    "status": "PASS",
    "confidence": 92.5,
    "product": "verifyhuman",
    "user_id": 42
  }
}

Compliance / KYC Events

Event Trigger Category
compliance.completed A compliance/KYC screening completed (cleared or potential match) compliance
compliance.hit_detected A compliance screening found a potential AML/PEP/Sanctions match compliance
compliance.report_ready A KYC/compliance report has been generated and delivered compliance

Example payload (compliance.completed):

{
  "id": "c8d3e6f1-5678-4a9b-2e0f-3a4b5c6d7e8f",
  "type": "compliance.completed",
  "created": "2025-01-15T14:30:00.000000Z",
  "environment": "live",
  "data": {
    "verification_id": "ver_abc123",
    "status": "PASS",
    "verification_type": "kyc_plus",
    "confidence": 92.5,
    "screening_status": "Cleared",
    "total_hits": 0
  }
}

Example payload (compliance.hit_detected):

{
  "id": "d9e4f7a2-6789-4b0c-3f1a-4b5c6d7e8f9a",
  "type": "compliance.hit_detected",
  "created": "2025-01-15T14:35:00.000000Z",
  "environment": "live",
  "data": {
    "verification_id": "ver_def456",
    "status": "PASS",
    "verification_type": "kyc_plus",
    "confidence": 88.0,
    "screening_status": "Potential Match",
    "total_hits": 2
  }
}

Example payload (compliance.report_ready):

{
  "id": "e0f5a8b3-7890-4c1d-4a2b-5c6d7e8f9a0b",
  "type": "compliance.report_ready",
  "created": "2025-01-15T14:40:00.000000Z",
  "environment": "live",
  "data": {
    "verification_id": "ver_abc123",
    "status": "PASS",
    "verification_type": "kyc_plus",
    "confidence": 92.5,
    "delivery_method": "email",
    "delivery_target": "compliance@example.com"
  }
}

Stateless v1 Compliance Events (VerifyTrust API)

These events are emitted by the new stateless VerifyTrust API endpoints (POST /api/compliance/v1/screen and POST /api/compliance/v1/rescreen). They are delivery notifications for screening results — they do not create a compliance workflow, case record, or customer profile inside VerifyHuman. Webhook payloads are generated from the same public-safe response returned by the API: they do not include subject payloads, raw provider responses, or watchlist source records.

Event Trigger Category
compliance.screening.completed A POST /api/compliance/v1/screen completed successfully compliance
compliance.screening.failed A POST /api/compliance/v1/screen failed after authentication and validation (e.g. provider unavailable) compliance
compliance.rescreen.completed A POST /api/compliance/v1/rescreen completed successfully compliance
compliance.rescreen.risk_changed A rescreen completed and risk_change is risk_increased or risk_decreased (never for risk_unchanged or unknown_without_prior_result) compliance

Example payload (compliance.screening.completed):

{
  "id": "f1a6b9c4-8901-4d2e-5b3c-6d7e8f9a0b1c",
  "type": "compliance.screening.completed",
  "created": "2026-04-24T10:15:00.000000Z",
  "environment": "live",
  "data": {
    "object": {
      "id": "vcmp_01HXX2ABCDEF",
      "object": "compliance.screening",
      "status": "completed",
      "client_reference_id": "customer_123",
      "risk": {
        "score": 45,
        "level": "medium",
        "decision": "review",
        "reasons": ["pep_possible_match"]
      },
      "flags": ["PEP_POSSIBLE_MATCH"],
      "matches": [],
      "data_retention": {
        "input_payload_stored": false,
        "result_payload_stored": false,
        "raw_provider_response_stored": false,
        "biometric_media_stored": false,
        "retention_model": "ephemeral_processing_only"
      }
    }
  }
}

Example payload (compliance.screening.failed):

{
  "id": "a2b7c0d5-9012-4e3f-6c4d-7e8f9a0b1c2d",
  "type": "compliance.screening.failed",
  "created": "2026-04-24T10:16:00.000000Z",
  "environment": "live",
  "data": {
    "object": {
      "object": "compliance.screening",
      "status": "failed",
      "client_reference_id": "customer_123",
      "error": {
        "type": "provider_unavailable",
        "code": "compliance_provider_unavailable",
        "message": "Compliance screening provider is temporarily unavailable."
      },
      "data_retention": {
        "input_payload_stored": false,
        "result_payload_stored": false,
        "raw_provider_response_stored": false,
        "biometric_media_stored": false,
        "retention_model": "ephemeral_processing_only"
      }
    }
  }
}

Example payload (compliance.rescreen.completed):

{
  "id": "b3c8d1e6-0123-4f4a-7d5e-8f9a0b1c2d3e",
  "type": "compliance.rescreen.completed",
  "created": "2026-04-24T10:17:00.000000Z",
  "environment": "live",
  "data": {
    "object": {
      "id": "vcmp_01HXX9ZYXWVU",
      "object": "compliance.screening",
      "status": "completed",
      "client_reference_id": "customer_123",
      "risk": {
        "score": 12, "level": "low", "decision": "clear", "reasons": []
      },
      "flags": [],
      "matches": [],
      "rescreen": {
        "previous_screening_id": "vcmp_01HXX2ABCDEF",
        "reason": "periodic_rescreen",
        "risk_change": "risk_unchanged"
      },
      "data_retention": {
        "input_payload_stored": false,
        "result_payload_stored": false,
        "raw_provider_response_stored": false,
        "biometric_media_stored": false,
        "retention_model": "ephemeral_processing_only"
      }
    }
  }
}

Example payload (compliance.rescreen.risk_changed):

{
  "id": "c4d9e2f7-1234-4a5b-8e6f-9a0b1c2d3e4f",
  "type": "compliance.rescreen.risk_changed",
  "created": "2026-04-24T10:18:00.000000Z",
  "environment": "live",
  "data": {
    "object": {
      "id": "vcmp_01HXX9ZYXWVU",
      "object": "compliance.screening",
      "status": "completed",
      "client_reference_id": "customer_123",
      "risk": {
        "score": 78, "level": "high", "decision": "review",
        "reasons": ["sanctions_strong_match"]
      },
      "flags": ["SANCTIONS_STRONG_MATCH"],
      "rescreen": {
        "previous_screening_id": "vcmp_01HXX2ABCDEF",
        "reason": "regulatory_refresh",
        "risk_change": "risk_increased"
      },
      "data_retention": {
        "input_payload_stored": false,
        "result_payload_stored": false,
        "raw_provider_response_stored": false,
        "biometric_media_stored": false,
        "retention_model": "ephemeral_processing_only"
      }
    }
  }
}

Replay protection

The X-VerifyHuman-Timestamp header carries the Unix timestamp at the moment the signature was computed. Reject any webhook event whose timestamp is older than your configured replay window — 5 minutes is the recommended default. A typical verifier:

import hmac, hashlib, time

REPLAY_WINDOW_SECONDS = 5 * 60

def verify(secret: str, body: bytes, sig_header: str, ts_header: str) -> bool:
    # 1. Replay-window check.
    try:
        ts = int(ts_header)
    except (TypeError, ValueError):
        return False
    if abs(time.time() - ts) > REPLAY_WINDOW_SECONDS:
        return False

    # 2. Recompute signature over "{timestamp}.{raw_body}".
    expected = "sha256=" + hmac.new(
        secret.encode("utf-8"),
        f"{ts_header}.".encode("utf-8") + body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, sig_header)

Billing Events

Event Trigger Category
quota.exceeded A user has exhausted their verification credits billing

Example payload:

{
  "id": "f5c1d4b8-2345-4a6f-9b7c-0d1e2f3a4b5c",
  "type": "quota.exceeded",
  "created": "2025-01-15T15:00:00.000000Z",
  "environment": "live",
  "data": {
    "user_id": 42,
    "credits_remaining": 0,
    "plan": "starter"
  }
}

Security Events

Event Trigger Category
throttling.triggered Verification throttle activated for an IP or user security

Example payload:

{
  "id": "a6d2e5c9-3456-4b7a-0c8d-1e2f3a4b5c6d",
  "type": "throttling.triggered",
  "created": "2025-01-15T15:30:00.000000Z",
  "environment": "live",
  "data": {
    "reason": "rate_limit_exceeded",
    "ip_address": "203.0.113.42",
    "user_id": 42
  }
}

System Events

Event Trigger Category
test.ping Synthetic test event sent via the dashboard or API system

Example payload:

{
  "id": "b7e3f6d0-4567-4c8b-1d9e-2f3a4b5c6d7e",
  "type": "test.ping",
  "created": "2025-01-15T16:00:00.000000Z",
  "environment": "test",
  "data": {
    "message": "Test webhook delivery"
  }
}

Environment Behavior


Delivery and Retry Behavior

Delivery Flow

  1. An event occurs (e.g., a verification completes)
  2. VerifyHuman creates a durable event record and delivery records for each matching endpoint
  3. The delivery worker picks up pending deliveries and sends signed HTTP POST requests
  4. Your endpoint should return a 2xx status code within 10 seconds
  5. If delivery fails, retries are scheduled automatically

What Counts as Success

Any HTTP 2xx response (200–299) is treated as a successful delivery.

What Triggers Retries

Retries are scheduled for:

What Does NOT Retry

These HTTP status codes are treated as terminal failures (no retry): 400, 401, 403, 404, 405, 406, 410, 411, 413, 414, 415, 422

Retry Schedule

Attempt Delay Cumulative Wait
1 Immediate
2 1 minute 1 minute
3 5 minutes 6 minutes
4 30 minutes 36 minutes
5 2 hours ~2.5 hours
6 12 hours ~14.5 hours
7 24 hours ~38.5 hours

After 7 attempts, the delivery is marked as failed_terminal.

Delivery Statuses

Status Meaning
pending Queued for first delivery attempt
processing Currently being sent
delivered Successfully delivered (2xx response)
retry_scheduled Failed, retry scheduled at a future time
failed_terminal All retry attempts exhausted or non-retryable error
skipped Endpoint was inactive, deleted, or had an environment mismatch

Idempotency

The event ID (X-VerifyHuman-Event-Id) is stable across retries. Use it to deduplicate events on your end. The delivery ID (X-VerifyHuman-Delivery-Id) is unique per attempt, so you can distinguish retries.


Endpoint Health

Health States

State Condition Dashboard Badge
Healthy Active, no recent failures, at least one successful delivery Green — Active
New Active, no deliveries yet Blue — New
Warning Active, 2–4 consecutive failures Yellow — Warning
Failing Active, 5+ consecutive failures Red — Failing
Auto-Disabled Automatically disabled after 10 consecutive failures Red — Auto-Disabled
Inactive Manually disabled Gray — Inactive

Auto-Disable Behavior

After 10 consecutive delivery failures, an endpoint is automatically disabled to protect your server and avoid unnecessary retries. When this happens:

Re-Enabling an Endpoint

  1. Go to the endpoint detail page
  2. Click Enable
  3. The failure counter resets to zero
  4. New events will begin delivering again

Replay and Redelivery

Replay an Event

From the Event Detail page, click Replay Event to resend an event to all eligible active endpoints (or a specific endpoint). Replay:

Redeliver a Failed Delivery

From the Delivery Detail page, click Redeliver to create a new delivery attempt for the same event to the same endpoint. This is useful for recovering from transient failures after fixing your endpoint.


Secret Rotation

You can rotate your signing secret at any time from the endpoint detail page:

  1. Click Rotate next to the signing secret
  2. Copy the new secret — it is shown only once
  3. Update your server to accept the new secret
  4. All future deliveries will use the new secret

Note: Rotate secrets immediately if you suspect a leak. All deliveries (including retries of older events) are signed with the current secret at the time of sending, so after rotation all subsequent delivery attempts will use the new secret.


Managing Endpoints via API

All webhook operations are available through the REST API at /api/webhooks/.

Operation Method Path
List endpoints GET /api/webhooks/endpoints
Create endpoint POST /api/webhooks/endpoints
Get endpoint GET /api/webhooks/endpoints/{id}
Update endpoint PUT /api/webhooks/endpoints/{id}
Delete endpoint DELETE /api/webhooks/endpoints/{id}
Enable endpoint POST /api/webhooks/endpoints/{id}/enable
Disable endpoint POST /api/webhooks/endpoints/{id}/disable
Rotate secret POST /api/webhooks/endpoints/{id}/rotate-secret
Send test event POST /api/webhooks/endpoints/{id}/test
List deliveries GET /api/webhooks/deliveries
Get delivery GET /api/webhooks/deliveries/{id}
Redeliver POST /api/webhooks/deliveries/{id}/redeliver
List events GET /api/webhooks/events
Get event GET /api/webhooks/events/{id}
Replay event POST /api/webhooks/events/{id}/replay
List event types GET /api/webhooks/event-types

All API endpoints require authentication and webhooks.read or webhooks.manage permissions.


Security


Troubleshooting

My endpoint is not receiving events

My signature verification is failing

My endpoint was auto-disabled

Deliveries are stuck in "processing"