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:
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.
Registering an endpoint
Tell us which workflow, the URL to post to, and how a delivery should identify itself. The URL must be absolute andhttps, 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
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 carriesX-Privue-Event, the event name and the same value as event in the body. A
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
Answer2xx 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: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.