Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.fintelite.ai/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Webhooks let your application receive instant notifications when a verification status changes. No polling required. When you create a verification, include a webhook_url and AnyCheck will POST a signed payload to that URL each time the status transitions.

Real-time updates

Status changes are delivered within seconds of the verification progressing.

HMAC-SHA256 signed

Every request includes a cryptographic signature so you can verify authenticity.

Per-verification

Each verification has its own webhook URL and optional secret. No global setup required.

Replay protection

Signatures include a timestamp. Reject requests older than 5 minutes to prevent replay attacks.

Registering a Webhook

Pass webhook_url (and optionally webhook_metadata.webhook_secret) when creating a verification:
curl -X POST https://api.anycheck.ai/verifications \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "service_id": "<service-uuid>",
    "folder_id": "<folder-uuid>",
    "configuration": { ... },
    "webhook_url": "https://your-app.com/webhooks/anycheck",
    "webhook_metadata": {
      "webhook_secret": "your-signing-secret"
    }
  }'
The webhook_secret is used to sign outgoing payloads. Choose a random, high-entropy string (at least 32 characters). Keep it secret and never expose it in client-side code.

Webhook Events

AnyCheck sends a POST request to your webhook_url when the verification status transitions to any of the following:
StatusDescription
IN_PROGRESSThe job has been picked up and is being processed
NEED_REVIEWProcessing complete, manual review required
COMPLETEDVerification finished successfully
PARTIALLY_COMPLETEDSome verifications in the result group completed, others pending
FAILEDVerification processing failed
PARTIALLY_FAILEDSome verifications in the result group failed
FRAUD_DETECTEDPotential fraud detected; review required
CANCELLEDVerification was cancelled

Payload Structure

Every webhook delivers the following JSON body:
{
  "event": "verification.status_changed",
  "timestamp": "2024-06-15T10:30:45.123Z",
  "verification_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "COMPLETED",
  "data": {
    "face_match_confidence_level": "HIGH",
    "face_match_confidence_score": 0.962,
    "is_match_face": true
  }
}
FieldTypeDescription
eventstringAlways "verification.status_changed"
timestampISO 8601UTC timestamp of when the event was generated
verification_idUUIDThe verification this event belongs to
statusstringThe new verification status
dataobjectThe verification output data (service-dependent)

Security: Verifying the Signature

AnyCheck signs every webhook request with HMAC-SHA256. Two headers are included:
HeaderDescription
X-Webhook-TimestampUnix timestamp (seconds) when the request was sent
X-Webhook-SignatureSignature in the format t={timestamp},sha256={hex_signature}

Verification steps

Step 1: Check the timestamp. Reject requests where the timestamp is more than 5 minutes old (or in the future by more than 5 minutes) to prevent replay attacks. Step 2: Reconstruct the signed string. Concatenate the timestamp and raw request body:
{timestamp}.{raw_body}
Step 3: Compute HMAC-SHA256. Use your webhook_secret as the key:
expected = HMAC-SHA256(key=webhook_secret, message="{timestamp}.{raw_body}")
Step 4: Compare. Extract the sha256 portion from X-Webhook-Signature and compare using a constant-time equality check.

Code examples

import hmac
import hashlib
import time

def verify_webhook(secret: str, signature_header: str, timestamp_header: str, raw_body: bytes) -> bool:
    # Step 1: Check timestamp (reject if older than 5 minutes)
    ts = int(timestamp_header)
    age = abs(time.time() - ts)
    if age > 300:
        return False

    # Step 2: Reconstruct signed string
    signed_payload = f"{ts}.{raw_body.decode('utf-8')}".encode()

    # Step 3: Compute expected signature
    expected_sig = hmac.new(
        secret.encode(),
        signed_payload,
        hashlib.sha256
    ).hexdigest()
    expected = f"t={ts},sha256={expected_sig}"

    # Step 4: Constant-time comparison
    return hmac.compare_digest(signature_header, expected)

# In your webhook handler (e.g., Flask):
# raw_body = request.get_data()
# is_valid = verify_webhook(
#     secret=WEBHOOK_SECRET,
#     signature_header=request.headers.get("X-Webhook-Signature"),
#     timestamp_header=request.headers.get("X-Webhook-Timestamp"),
#     raw_body=raw_body,
# )

Delivery Behavior

  • Timeout: AnyCheck waits up to 10 seconds for your endpoint to respond. If it times out or returns a non-2xx status, the delivery is considered failed.
  • No automatic retry: Failed deliveries are not retried. Design your endpoint to be idempotent and use the verification ID as a deduplication key. If you miss an event, fetch the current state via GET /verifications/{id}.
  • Async delivery: Webhooks are sent asynchronously in a background goroutine and do not block verification processing.

Best Practices

Your endpoint should return 200 OK immediately and process the payload in a background queue. If your handler takes longer than 10 seconds, AnyCheck will consider the delivery failed.
The same event may be delivered more than once in edge cases. Use verification_id + status as a deduplication key to avoid processing the same transition twice.
Skip signature verification only during local development. In production, always reject requests with an invalid or missing signature.
New event types may be added in the future. Write your handler to gracefully ignore any event value it does not recognize.
For critical use cases, pair webhooks with periodic polling (GET /verifications/{id}) to catch any missed events.