Auvral

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 as ngrok http 3000 gives 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 sessionId against your user. It is how you will recognise the webhook.
  • url is for one person and expires quickly. Create the session when you are about to show it, not in a batch ahead of time.
  • allowedOrigins is 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:

verify.html
<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:

webhook.js
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:

On this page