Auvral

Webhooks

How Auvral tells your backend what happened: delivery, retries, idempotency, and why this is the only authoritative signal.

Webhooks are how your backend learns the outcome of a verification. Today they are the only way, so treat your webhook handler as the place where verification changes your system's state.

Set one up

Dashboard, Integrations, Webhooks, Create webhook.

FieldNotes
NameFor your own reference. Up to 100 characters.
Webhook URLMust be https://.
FlowsWhich flows this webhook receives events for. At least one.
Signing secretLeave blank to generate one, or supply your own.

A webhook is subscribed to flows, not to event types: it receives all four event types for verifications on that flow. Filter by type in your handler.

Reveal the signing secret from the webhook's page and store it alongside your API key. It is a separate credential with its own rotation.

Send test event delivers a webhook.test event with the same headers and signature scheme as a real one. Use it to confirm reachability and to develop against.

The four events

typeWhen
verification.startedYour user began the verification
verification.submittedYour user finished submitting; there is not yet a verdict
verification.succeededVerdict: verified
verification.failedVerdict: rejected

Plus webhook.test, sent only by the dashboard's test button.

The success event is verification.succeeded

Not verification.completed. If you are working from an older integration guide or an example that mentions verification.completed or a flat { verificationId, sessionId, event } body, it is out of date. See Event reference.

verification.failed is a rejected verification, not a delivery failure. It is a normal outcome.

Payload

Every event has the same envelope:

{
  "id": "6f1d0a4c-8e22-4b7e-9a13-5c0be9f4d871",
  "type": "verification.succeeded",
  "createdAt": "2026-08-25T15:12:44.301Z",
  "data": {
    "verification": {
      "id": "b47ac10b-58cc-4372-a567-0e02b2c3d479",
      "status": "VERIFIED",
      "flowId": "3f1c9a52-4d7e-4b3a-9c21-8e5f6a0b7d34",
      "flowName": "Onboarding",
      "sessionId": "9b2f4c1e-7a83-4d06-b5f1-2c9e8d3a6047",
      "referenceId": "user_8842",
      "metadata": { "orderId": "order-8841" },
      "startedAt": "2026-08-25T15:11:02.884Z",
      "endedAt": "2026-08-25T15:12:44.120Z",
      "steps": [
        { "name": "document_verification", "status": "VERIFIED" },
        { "name": "biometric_verification", "status": "VERIFIED" }
      ]
    }
  }
}

Match it to your user with data.verification.sessionId (the sessionId you stored when you created the session) or data.verification.referenceId (your own id for the person).

The payload carries no personal data: no name, no document number, no image.

Full field-by-field detail in the Event reference.

Headers

HeaderValue
x-auvral-signaturev1=<hex>, HMAC-SHA256 of `${timestamp}.${rawBody}`
x-auvral-timestampUnix seconds; the same value that went into the signature
x-auvral-event-idThe event's id. Stable across retries, deduplicate on this
x-auvral-event-typee.g. verification.succeeded
x-auvral-delivery-idThis individual delivery attempt
x-auvral-attemptAttempt number
user-agentAuvral-Webhooks/2

Verify the signature. This is not optional.

Your webhook URL is reachable by anyone who learns it, and the signature is the only thing that separates a real Auvral event from a forged one claiming success.

Three rules, all of which have bitten real integrations:

  1. Read the raw body before parsing it. The signature covers exact bytes. Any framework that hands you a parsed object has already destroyed what you need: a re-serialized JSON object will not match.
  2. Reject stale timestamps. Without a freshness check, a captured request can be replayed against you forever. Five minutes is a reasonable window.
  3. Compare in constant time. crypto.timingSafeEqual in Node, hmac.compare_digest in Python.

Verifying signatures has working code in Node, Python, Go, PHP and Ruby.

Delivery, retries and idempotency

  • Success is any 2xx. Anything else, a 4xx, a 5xx, a timeout or a DNS failure, is a failed attempt.
  • Timeout is 10 seconds per attempt.
  • Up to 6 attempts, with backoff of 1 minute, 5 minutes, 30 minutes, 2 hours, then 6 hours between them. An endpoint that is down for an afternoon still receives its events.
  • Your response body is recorded and visible in the dashboard, so a useful error message makes your own debugging easier.

Make your handler idempotent

Retries reuse the same x-auvral-event-id, so a handler that is not idempotent can process the same verification up to six times: six approval emails, six credited accounts. Record the event id, and ignore one you have already processed.

Acknowledge fast. Return 2xx as soon as you have verified the signature and durably recorded the event; do the real work afterwards, out of band. A handler that does slow work inline will hit the 10-second timeout and get retried, which is how the duplicate problem starts.

Events are not ordered: a retried verification.submitted can arrive after verification.succeeded. Drive your logic from type and data.verification.status, not from arrival order.

Monitoring

The webhook's page in the dashboard shows every delivery attempt (timestamp, HTTP status, response body, attempt number) and lets you retry a failed one by hand or pause the webhook without deleting it.

The browser never decides

The postMessage is a UI signal, not a proof

Never unlock an account, approve a payment or change business state from it. The truth is the webhook or a server-side GET. It lives in the browser and the user controls their browser.

The SDK's onResult callback arrives in your user's browser. It is genuinely useful for updating your interface immediately, and genuinely forgeable by the person sitting in front of it.

The split to hold onto:

SignalTrust it forNever trust it for
onResult in the browserShowing a success screen, advancing your UIAccount state, payments, limits, entitlements
A signature-verified webhookEverythingNothing

A clean pattern: onResult polls your own backend, which reports success only once your webhook handler has run.

Reconciling, not polling

A missed or double-processed webhook can be reconciled by asking the API afterwards: GET /v1/sessions/{sessionId} and GET /v1/verifications/{id} both answer with your API key.

That does not make the two things below advisory. Verify signatures and make your handler idempotent: the read is a repair tool for the delivery you lost, not a substitute for the delivery. Reach for it on a schedule or when something is missing, never in a loop while a verification runs, and never as the thing your user's page waits on.

On this page