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.
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:
{
"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
2xxis retried, and a slow2xxcan 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.reviewedcan in principle arrive before you've finished processing itsverification.completed. Reconcile against the verification's current state rather than assuming arrival order. 2xxmeans "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:
- Compute
HMAC-SHA256(signing_secret, raw_body)over the exact raw bytes of the request body — before any JSON parsing. - Compare it to
X-VerifyAI-Signatureusing a constant-time comparison (such ascrypto.timingSafeEqual), never===. - Reject the delivery if
X-VerifyAI-Timestampis more than a few minutes old, to defend against replays.
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
200and 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.