Webhooks

Webhooks let VerifyAI push events to your backend the moment something happens — a verification finishes, a human overturns a verdict, an inspection session is submitted — instead of you polling the API. You register an HTTPS endpoint, subscribe it to the events you care about, and we POST a signed JSON payload to it.

This page is the conceptual model: what events exist, what delivery guarantees you get, and how to trust a payload. For the step-by-step setup (registering an endpoint, verifying signatures in code, a production Node handler), see Setting up webhooks.

Why webhooks

Polling GET /api/v1/verifications/:id in a loop is laggy and wasteful. Webhooks invert the flow:

  • Lower latency. You hear about a result as soon as it's written, not on your next poll tick.
  • Less load. No spinning on the API waiting for a status to flip.
  • Out-of-band events. Some events — like a human review changing a verdict — have no request for you to poll against. Webhooks are the only push channel for them.

Event types

You subscribe an endpoint to a specific list of events. Three are available today:

| Event | Fires when | | ------------------------ | ------------------------------------------------------------------ | | verification.completed | A /api/v1/verify call finishes processing (compliant or not). | | verification.reviewed | A human review changes a verification's verdict in the dashboard. | | inspection.submitted | A self-inspection session is finalized by the recipient. |

For verification.* events, the payload's data object is the same Verification object the API returns. For inspection.submitted, data carries the session summary instead of a single verification.

completed vs. reviewed

verification.completed is the machine's verdict at capture time. verification.reviewed is a later, human-issued correction — for example, an operator approving a ride that exhausted its retries and was auto-flagged. Treat the reviewed verdict as the final word.

The envelope

Every delivery shares a stable top-level envelope, with the event-specific body nested under data:

json
{
  "id": "evt_9Q2v...",
  "type": "verification.completed",
  "created_at": "2026-06-02T12:00:03Z",
  "data": {
    "id": "ver_8x92m4k9",
    "status": "success",
    "is_compliant": false,
    "policy": "pol_forest1",
    "category": "bad_parking",
    "violation_reasons": ["not_on_tactile_paving"],
    "evaluation_source": "cloud_vlm"
  }
}

| Field | Description | | ------------ | ------------------------------------------------------------------- | | id | Unique event ID, prefix evt_. Use this for idempotency. | | type | One of your subscribed event types. | | created_at | ISO 8601 timestamp of when the event was generated. | | data | The event payload — a Verification object for verification.*. |

Delivery semantics

VerifyAI guarantees at-least-once delivery. The practical consequences:

  • Duplicates happen. A delivery that doesn't get a 2xx is retried, and a slow 2xx can still be retried before we see it — so the same event can arrive more than once. Your handler must be idempotent.
  • Order is not guaranteed. A verification.reviewed can in principle arrive before you've finished processing its verification.completed. Reconcile against the verification's current state rather than assuming arrival order.
  • 2xx means "received," not "processed." Acknowledge as soon as the event is safely persisted or enqueued, then do heavy work off the request path. Doing slow work inline risks a timeout, which we read as a failure and retry.

Signatures

Never trust a payload you haven't verified. Every delivery carries two headers:

| Header | Description | | ----------------------- | ----------------------------------------------------------------------- | | X-VerifyAI-Signature | Hex HMAC-SHA256 of the raw request body, keyed by your signing secret. | | X-VerifyAI-Timestamp | UNIX seconds when we sent the delivery. Use it to reject stale replays. |

The signing secret is returned once, when you register the endpoint. To verify a delivery:

  1. Compute HMAC-SHA256(signing_secret, raw_body) over the exact raw bytes of the request body — before any JSON parsing.
  2. Compare it to X-VerifyAI-Signature using a constant-time comparison (such as crypto.timingSafeEqual), never ===.
  3. Reject the delivery if X-VerifyAI-Timestamp is more than a few minutes old, to defend against replays.
Verify before you parse

The most common webhook bug is parsing the JSON first and signing the re-serialized object. Whitespace and key order differ, so the HMAC never matches. Always compute the signature over the raw bytes you received, then parse.

Retries and idempotency

A delivery that doesn't return a 2xx is retried with exponential backoff over roughly 24 hours. Because of at-least-once delivery, build your handler to deduplicate on the envelope id:

  • On receipt, record the evt_ id in a table with a unique constraint.
  • If the insert conflicts, you've already processed it — return 200 and stop.
  • Run side effects only on the first successful insert.

This makes repeated deliveries of the same event harmless, which is exactly the property at-least-once delivery requires.

What's next

  • Setting up webhooks — register an endpoint, verify signatures, and ship a hardened Node handler.
  • Verifications — the object inside every verification.* payload.
  • Retries — the SDK-side loop that runs before any event reaches your webhook.

Get in Touch

Questions about pricing, integrations, or custom deployments? We'd love to hear from you.