> ## Documentation Index
> Fetch the complete documentation index at: https://docs.privue.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Callbacks

> Be told when a verification is submitted or gated, instead of polling for it.

When a verification is submitted, or a gate check stops it, Privue posts a callback to an endpoint
registered against its workflow. It tells you which verification moved; you read the record with
`GET /verifications/{verification_id}` as usual. On a hosted workflow the submission is the user's;
where you supply the documents it is the run of the checks finishing.

Registering an endpoint is optional, and it is done **per workflow**. Each of your journeys posts to
its own endpoint or to none, so a `uat` workflow reports to wherever you are testing and a production
one to where your real work goes. A workflow with no endpoint registered delivers nothing, and you poll
its verifications instead.

## What earns a callback

`verification.submitted` and `verification.gated` are the events we post, so they are the transitions
you can be told about:

| Transition | Callback |
| - | - |
| The verification is submitted, by the user or by the run of the checks | `verification.submitted` |
| A [gate check](/verification/step-types#gate-check) stops the journey, on your system's answer or on the workflow's fallback where it gave none | `verification.gated` |
| The run completes | None. It follows the callback rather than earning one of its own, and a submitted record is already final. |
| You cancel, reset or purge the verification | None. You made the change, so nothing is delivered back to you. |
| Privue marks a run `failed` | None. |
| The retention window runs out and the record expires | None. |

Every other status is read rather than delivered: poll `GET /verifications/{verification_id}` for one
record, or list and count them across your account with
[Tracking verifications](/verification/tracking).

## Registering an endpoint

Tell us which workflow, the URL to post to, and how a delivery should identify itself. The URL must be
absolute and `https`, with no credentials and no fragment.

Registration belongs to the workflow, so publishing a new version of a journey leaves its endpoint
exactly as it stands, and changing an endpoint needs no change to the journey. Registering one workflow
says nothing about any of your others.

You choose one of two ways to be sure a delivery is ours, whichever fits the endpoint you already run.

**A secret you choose.** Pick a long random string and Privue sends it back in a header on every
delivery, `X-Privue-Secret` by default or one you name. Keep it wherever your handler can read it, and
tell Privue to change it whenever you want.

**A token from your own auth service.** If your endpoint only accepts tokens it issued, give Privue the
URL of the service that issues them, the credentials to present, any header it needs to route the
request, and where the token sits in its answer. Privue posts those credentials to it and carries the
token it returns on the delivery, as `Authorization: Bearer <token>` by default or in a header and with
a prefix you name.

A fresh token is fetched for each delivery, immediately before it is sent, so nothing we hold can go
stale between the two and every delivery arrives with a token your service issued seconds earlier.

## The payload

```json theme={"system"}
{
  "event": "verification.submitted",
  "verification_id": "3f2504e0-4f89-41d3-9a0c-0305e82c3301",
  "status": "submitted",
  "workflow_key": "merchant-onboarding",
  "version": 2,
  "reference_user_id": "merchant-42",
  "occurred_at": "2026-08-13T18:24:11"
}
```

| Field | Meaning |
| - | - |
| `event` | `verification.submitted` or `verification.gated`. |
| `verification_id` | The verification to read. |
| `status` | The verification's status when the callback was sent. |
| `workflow_key` | The workflow the verification runs. |
| `version` | The version of that workflow the verification runs, the same `version` its record reports. |
| `reference_user_id` | Your own identifier for the subject, as you supplied it when creating it. |
| `occurred_at` | When the verification was submitted, or when the check stopped it, in IST. |

The callback carries no step data. A verification is only submitted with every step settled, and a
gated one has ended where the check stopped it, so the record is final the moment you are told about
it: read it with `GET /verifications/{verification_id}` and you have the whole result, with
`gated_by_step` naming the check on a gated one.

## Checking it came from us

Every request carries `X-Privue-Event`, the event name and the same value as `event` in the body. A
[gate check](/verification/step-types#gate-check) posts to your own system the same way and opens with
the same fields up to `reference_user_id`, under the event `verification.check`.
Whatever proves the delivery is ours travels beside it, in the form you registered.

On a secret you chose, reject anything whose header does not match it, and compare the two with your
language's constant-time function - `hmac.compare_digest` in Python, `crypto.timingSafeEqual` in Node -
rather than `==`. The secret travels in the request, so treat it like any other credential: keep it out
of logs, and ask us to change it if it is ever exposed.

On a token from your own auth service, check it exactly as your other endpoints check one. Answer `401`
or `403` to a token you will not accept and Privue stops rather than retrying, because a credential your
own service rejects will be rejected again.

Serve the endpoint over `https` only, either way.

## Responding, retries, and repeats

Answer `2xx` within 10 seconds. Do the least you can before answering: record the `verification_id` and
return, then read the record on your own time. A handler that reads the record and runs your onboarding
before answering is a handler that times out and gets the same callback again.

A timeout, a refused connection, and a `5xx` are all retried with exponential backoff: by default 12
attempts, the first repeat 5 seconds after the first attempt, each pause twice the last and none longer
than 30 minutes, which comes to about two hours. The retries are part of the endpoint's registration,
so ask us for a different number of attempts, first pause, multiplier or longest pause where yours
needs one, up to 20 attempts and an hour between any two. The record waits for them before it
completes. An answer that says the request itself was wrong - `400`, `401`, `403`, `404`, `405`, `410`,
`415`, `422` - is not retried, because repeating it would get the same answer.

Deliveries can repeat. A response that is lost on the way back looks identical to one that never
arrived, so a callback you already handled can be delivered a second time. Make your handler
idempotent, keyed on `verification_id` and `occurred_at` together: a repeat of one delivery carries
both unchanged.

Key on the pair rather than on `verification_id` alone, because a verification you reset and that is
submitted or gated again is a second run with a second result, and it earns a callback of its own. Its
`occurred_at` is the moment that run ended, so it tells the two apart.

## When a callback cannot be delivered

The verification completes anyway, and a gated one stays gated. The result was final before you were
told anything, so an endpoint that is down holds nothing back, and the record says what happened to the
callback:

```json theme={"system"}
"callback": {
  "status": "failed",
  "settled_at": "2026-08-10T14:43:21",
  "reason": "the endpoint answered 503"
}
```

`status` is `delivered` or `failed`, `settled_at` is when the callback was accepted or when the attempts
ran out, and `reason` says why nothing was accepted on a failed one. The field is null until the
callback settles, and on every verification of a workflow with no endpoint registered. A reset clears it along
with the rest of the journey: it described the ending the reset undid.

So a run of failed callbacks is recoverable without us: list the records for the period your endpoint
was down, and `callback.status` tells you which of them you were never told about. See
[Catching up on callbacks you missed](/verification/tracking#catching-up-on-callbacks-you-missed).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.