Quickstart
From a sandbox key to a verified test user: create a session, embed the SDK, receive a signed webhook.
By the end of this page you will have created a verification session, run a real verification in sandbox, and received a signed webhook telling you it succeeded.
You need a browser with a camera, and somewhere your webhook endpoint can be reached from the internet.
Prefer to see it working first?
Try it out runs a full sandbox verification in your browser, with no key and no code, so you know what to expect before you wire any of this up.
1. Get your sandbox key and a flow id
In the Auvral dashboard, go to Integrations, API keys.
Your organization has exactly one active key per environment. The sandbox key can be revealed from that page whenever you need it: click Show. If the page tells you the key has never been revealed, click Rotate to mint one.
Copy the sandbox key. It starts with sk_auvral_test_.
This key is a server-side secret
It authenticates as your whole organization. It must never appear in browser JavaScript, a mobile app bundle, a public repository, or a URL. The verification flow does not need it and never receives it.
Then go to Flows and copy the id of the flow you want to run.
The flow has to be published: a flow you have just created is Not published and
answers 409 FLOW_NOT_PUBLISHED. Publish it from its detail
page, or, while you are still editing it, run its draft with a sandbox key by sending
flowVersion: "draft".
2. Set up a webhook endpoint
Go to Integrations, Webhooks and click Create webhook:
- Webhook URL must be
https://. For local development, a tunnel such asngrok http 3000gives you a public URL that forwards to your machine. - Flows: select the flow from step 1. A webhook only receives events for the flows it is subscribed to.
- Signing secret: leave blank to have one generated.
Save it, then reveal the signing secret and keep it next to your API key. You need it in step 5.
Use Send test event to confirm your endpoint is reachable before you go any further.
It delivers a webhook.test event with the same headers and the same signature scheme as
a real one.
3. Create a session
From your backend, not the browser:
curl -X POST "$AUVRAL_API_URL/v1/sessions" \
-H "x-api-key: $AUVRAL_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"flowId": "3f1c9a52-4d7e-4b3a-9c21-8e5f6a0b7d34",
"referenceId": "user_8842",
"metadata": { "orderId": "order-8841" },
"allowedOrigins": ["https://app.example.com"]
}'This environment's API base URL is https://api.dev.auvral.com
{
"statusCode": 201,
"data": {
"sessionId": "9b2f4c1e-7a83-4d06-b5f1-2c9e8d3a6047",
"url": "https://verify.dev.auvral.com/c/Jk3nQ7pXsR2vLm9Ab4Cd1e",
"expiresAt": "2026-08-25T15:42:11.084Z"
}
}Note the envelope: the payload is under data, and statusCode repeats the HTTP status.
Every Auvral response is shaped this way.
Three things to know about what came back:
- Store
sessionIdagainst your user. It is how you will recognise the webhook. urlis for one person and expires quickly. Create the session when you are about to show it, not in a batch ahead of time.allowedOriginsis the origin of your page, the one that will embed the flow. Optional in the schema, effectively required for any embedded integration. See below.
Omitting allowedOrigins breaks the SDK callbacks, silently
The verification flow only sends its result to a page whose origin is in the session's
allowedOrigins. Leave the field out and the list is empty, so nothing ever matches:
nothing is sent at all, and onResult, onClose and the loading indicator never
fire.
There is nothing on the network to inspect, and the verification itself completes
normally, webhook included: only your page stays silent. The embedded flow does log one
[auvral] warning to the browser console, which is the one place this failure names
itself.
Pass the exact origin of the embedding page: scheme, host and port, no path.
https://app.example.com, not https://app.example.com/onboarding. A value that is not
an origin at all is refused when you create the session, with a 400
INVALID_ALLOWED_ORIGINS naming it. What stays silent is the list that is well formed and
still wrong: a mismatched port, or http where you meant https.
Omit it only when you are opening the URL as a plain link rather than embedding it.
Expose this behind your own route, something like POST /verification/start, returning
only url to the browser. The API key stays on the server.
4. Open the flow in the browser
Load the SDK and hand it the URL:
<script src="https://sdk.dev.auvral.com/v1/verification.js"></script>
<button id="verify">Verify my identity</button>
<script>
document.getElementById("verify").addEventListener("click", async () => {
// your own endpoint from step 3, it holds the API key, the browser does not
const res = await fetch("/verification/start", { method: "POST" });
const { url } = await res.json();
new AuvralVerification().init({
url,
onResult: ({ status, verificationId, sessionId }) => {
// A UI signal. Update the screen; do not change business state here.
console.log("result", status, verificationId, sessionId);
},
onClose: ({ reason }) => {
console.log("closed", reason);
},
onError: ({ code, message }) => {
console.error("integration error", code, message);
},
});
});
</script>Sandbox and production
The snippet above is the sandbox URL. In production, load it from
<script src="https://sdk.auvral.com/v1/verification.js"></script>See Web SDK for versioning and Subresource Integrity.
The SDK asks for camera permission. If your site sends a Content-Security-Policy or a
Permissions-Policy header it will block this, in ways the browser console explains
poorly. Read Browser requirements before you debug
anything.
Now complete the verification: photograph a document, take a selfie. On a desktop you may be offered a QR code to finish on your phone instead. Either way, nothing changes for your integration.
The postMessage is a UI signal, not a proof
onResult tells you what the user's browser says happened. 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.
Use onResult to move to your next screen. Use the webhook in step 5 to decide anything
that matters.
5. Receive and verify the webhook
Your endpoint has already received verification.started and verification.submitted.
When there is a verdict you get verification.succeeded or verification.failed:
{
"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" }
]
}
}
}data.verification.sessionId matches the sessionId you stored in step 3, and
referenceId is the value you sent, which is your own id for the person.
Verify the signature before you trust any of it. Every delivery carries
x-auvral-signature: v1=<hex>, an HMAC-SHA256 over `${timestamp}.${rawBody}` using
your webhook's signing secret:
import express from "express";
import crypto from "crypto";
const app = express();
// raw body, a parsed object cannot be verified
app.post("/webhooks/auvral", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.get("x-auvral-signature") ?? "";
const timestamp = req.get("x-auvral-timestamp") ?? "";
const rawBody = req.body.toString("utf8");
const expected = crypto
.createHmac("sha256", process.env.AUVRAL_WEBHOOK_SECRET)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const received = signature.replace(/^v1=/, "");
const valid =
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!valid) return res.sendStatus(401);
// reject anything older than five minutes
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return res.sendStatus(401);
const event = JSON.parse(rawBody);
// retries reuse the event id, process each one once
if (alreadyProcessed(req.get("x-auvral-event-id"))) return res.sendStatus(200);
if (event.type === "verification.succeeded") {
approveUser(event.data.verification.referenceId);
}
res.sendStatus(200); // acknowledge fast: the sender times out at 10s
});Signature verification is mandatory. Anyone who learns your endpoint URL can POST to it; the signature is the only thing that distinguishes Auvral from them.
Verifying signatures has the same logic in Python, Go, PHP and Ruby, and explains the raw-body, replay and deduplication rules in full.
You are done
From here:
- Authentication: rotating keys, going live.
- Web SDK: callbacks and closing the overlay.
- Webhooks: retries and idempotency.
- Errors: what each status code means.