Skip to main content

First verification

Time to run a real verification with the widget you embedded in the previous step.

What you're about to test

A complete user flow:

  1. User clicks Start
  2. Browser asks for camera permission
  3. User photographs the front of their document (camera or gallery)
  4. User photographs the back — unless you set require-doc-back="false" or the document type is passport
  5. User takes a selfie (camera only)
  6. Widget runs quality checks, uploads each image to the Veridia API, then calls submit
  7. You receive a veridia:complete event carrying the verificationId
  8. The verdict (approved / review / rejected) is computed server-side moments later

Run it

Open the page where you embedded the widget. Use a real device with a camera — the widget is mobile-first but works on laptops too.

Testing on localhost

Nothing to configure. Keys are created with an empty allowed-origins list, and an empty list permits every origin — localhost included. If the widget refuses to start, the cause is elsewhere; check e.detail.code on the veridia:error event.

Tips for the document capture

  • Hold the document flat on a contrasting surface (avoid white-on-white)
  • Don't cover any corners with your fingers
  • Avoid direct light reflecting off the document — this raises the heavy_glare flag
  • Make sure the document is fully in frame
  • Hold the phone steady; blurry frames are rejected by the quality check before upload

Tips for the selfie

  • Face the camera directly
  • Good even lighting (no backlight)
  • Remove sunglasses and hats that cover the face
  • Stay still for the capture

Inspect the event payload

Add listeners to see exactly what the widget emits. These two are the only events the widget dispatches:

<script>
const w = document.querySelector('veridia-widget');

w.addEventListener('veridia:complete', (e) => {
console.log('verification complete:', e.detail);
});

w.addEventListener('veridia:error', (e) => {
console.error('verification error:', e.detail);
});
</script>

When the user finishes, your console shows exactly two fields:

{
"verificationId": "vf_AG07CDWRRFQV4T05ZXG2",
"status": "queued"
}

That's it. There is no userRef in this event, and no verdict.

  • No userRef: the widget does not copy it into the event, and GET /v1/verify/{id} does not return it either. Only the webhook echoes it. Persist verificationId → your user id from this handler, or you will have a verdict you can't attribute.
  • No verdict: status: "queued" describes the pipeline, not the person. Fetch the verdict from your server (step 3) or receive it by webhook.

See it in your dashboard

Go to your Veridia dashboard and open the Review queue. You'll see your verification with:

  • Submitter info (user ref, submitted timestamp)
  • The six scores: OCR confidence, face match, liveness, document quality, MRZ validity, and name match
  • Flags, each with a level and a text (e.g. heavy_glare, possible_screen_capture)
  • The overall confidence and the final verdict

Click into a verification to see the full breakdown and the captured images.

What happens behind the scenes

StepWhereWhat
1BrowserQuality checks (Laplacian, Tenengrad, Brenner) before anything is sent
2BrowserOne PUT per image to /v1/verify/upload/{verificationId}/{role} on the Veridia API
3WorkerValidates the upload token, checks the bytes are real JPEG, stores them
4WorkerOCR via Workers AI — extracts name, document number, dates
5WorkerDispatches to backend with the extracted data
6BackendFace match (insightface buffalo_s)
7BackendLiveness scoring
8BackendMRZ checksums and MRZ-vs-visual-zone consistency
9BackendAML / sanctions screening
10BackendWeighted confidence score, then verdict
11BackendPersists to MySQL with audit trail
12BackendQueues the webhook, if one is configured
Images do not go to R2 from the browser

You may see older material describing presigned uploads straight to Cloudflare R2. That is not what happens. Every image is PUT to the Veridia API itself, authenticated by a short-lived X-Veridia-Upload-Token that /v1/verify/init returns inside each upload slot's headers. The API validates and stores the bytes.

This matters in two places. If you are tightening a Content Security Policy, the host you need to allow is api.xxuxe.online, not an R2 domain — allowlisting R2 will block your uploads. And if you are ever writing a client by hand instead of using the widget, you must forward the slot's headers verbatim; your API key does not authenticate that endpoint, and building the request yourself without the upload token returns a 400 on every image.

What the verdict means

VerdictRoughly whenWhat you should do
approvedConfidence ≥ 90 and no hard failuresTrust the user, complete onboarding
reviewConfidence between 60 and 90, or any hard failure, or a sanctions hitSend to your manual review queue
rejectedConfidence below 60Block, ask the user to retry, or escalate

Two rules worth internalizing, because they are not visible from the confidence number alone:

  • A hard failure never auto-approves. No face on the document, no face on the selfie, a failed MRZ checksum, a detected spoof — any of these forces at least review, whatever the score says.
  • A sanctions match forces review, never an automatic rejection. A person is expected to make that call.

These thresholds are deployment-wide settings, not per-tenant knobs. Do not reimplement the banding on your side from confidence; read verdict and act on it.

Error codes the widget emits

These are the real values of e.detail.code on veridia:error. The whole list:

CodeMeaningTypical response
camera_deniedUser denied the camera permission promptExplain why you need it, offer a retry
camera_unavailableNo camera present, or the page is not on HTTPS/localhostTell the user to switch device or browser
user_cancelledUser pressed Cancel and left the flowNot an error. Log it — this is your funnel drop-off signal
invalid_api_keyKey missing, mistyped, revoked, or wrong environmentFix your config. Not user-facing
insufficient_creditsYour tenant is out of creditsTop up. Alert yourself — every user is blocked until you do
rate_limitedRate limit hitBack off and retry
api_unreachableNetwork failure or 5xx from the APITransient. The widget already retried
upload_failedAn image upload failed after three retries with backoffSurface it; usually a bad mobile connection
no_face_in_selfieNo face detected in the selfieReserved. Normally handled inline as a retake prompt
blurry_imageImage too blurryReserved. Normally handled inline as a retake prompt
internal_errorAnything unexpected, including unmapped API errorsLog the whole e.detail and investigate

Two things about that table are easy to get wrong:

camera_denied and user_cancelled are the ones you'll actually see most. Together they account for most abandoned flows in real traffic. Neither is a bug, and both need a product answer rather than an error screen.

Blur and "no face" are usually not events. The widget handles those inline: it tells the user to retake, and after two consecutive failures on the same shot it lets the photo through anyway rather than trapping the user in a loop. So don't build your image-quality telemetry on these codes — it will stay empty. upload_failed, by contrast, does reach your handler once the retries are exhausted, so wire up an alert for it.

Next step

You have a verification result. Now learn how to read the verdict.

Step 3: Handling results →