Verification Flow
Step-by-step guide to the user verification experience
Verification Flow
This guide explains what happens during a verification session from the user's perspective and how the system processes each step.
Overview
┌─────────────┐ ┌──────────────┐ ┌─────────────┐ ┌──────────────┐
│ Create │────▶│ User │────▶│ Liveness │────▶│ Complete/ │
│ Session │ │ Visits │ │ Check │ │ KYC │
└─────────────┘ └──────────────┘ └─────────────┘ └──────────────┘Step 1: Session Creation
Your application creates a session via the API:
const { data: session } = await createSession({
age_target: 18,
redirect_url: 'https://your-app.com/verified',
webhook_url: 'https://your-app.com/webhooks/bouncer'
});
// Redirect user to verification (response is wrapped in data)
window.location.href = session.verify_url;System Actions:
- Session record created with status
CREATED - Unique session ID (UUID) generated
- Short code generated for mobile-friendly URLs
- Expiration timer starts
Step 2: User Visits Verification Page
When the user opens the verification URL:
Cookie bypass (when Force Verification is off): If the user has a valid verification cookie for the required age and the session does not have Force Verification enabled, they can be treated as already verified and the session completes without showing the verification flow. When Force Verification is on (per device or per API), the cookie is ignored and the user always goes through the full flow.
User Experience:
- Sees the organization's branding (logo, colors)
- Views instructions for the verification process
- Sees the target age requirement
System Actions:
- Status changes to
PENDING visited_attimestamp recorded- Mobile device detection
- Verification SDK initialized
QR Code Flow (Desktop)
If the user visits from a desktop:
- QR code is displayed
- User scans with mobile device
- Verification continues on mobile
- Desktop page auto-updates when complete
Step 3: Liveness Check
The 3D liveness check verifies the user is a real, present person.
User Experience:
- Camera permission requested
- User centers face in the frame
- Guided through head movements
- ~3-5 seconds to complete
System Actions:
- Status changes to
VERIFICATION_STARTED biometry_started_attimestamp recorded- Liveness verification performed
- Age estimation performed
Liveness Requirements
| Check | Purpose |
|---|---|
| 3D Depth Analysis | Prevents photos/videos |
| Movement Tracking | Confirms live presence |
| Texture Analysis | Detects masks/screens |
| Eye Tracking | Ensures user attention |
Step 4: Age Evaluation
After the liveness check, the system evaluates the user's age.
Age Estimation Groups
| Group | Age Range |
|---|---|
| 0-2 | Infant |
| 3-7 | Young child |
| 8-12 | Child |
| 13-17 | Teen |
| 18-24 | Young adult |
| 25-34 | Adult |
| 35-54 | Middle-aged |
| 55+ | Senior |
Decision Logic
IF estimated_age >= target_age AND NOT force_kyc THEN
→ Complete with SUCCESS
ELSE
→ Proceed to KYCStep 5: KYC (If Required)
KYC is triggered when:
- Age estimation is below target
force_kycis enabled on the session
User Experience:
- Instructions to prepare ID document
- Capture front of ID
- Capture back of ID (if applicable)
- Wait for processing
System Actions:
- Status changes to
VERIFICATION_KYC kyc_started_attimestamp recorded- Document OCR extraction
- Birth date parsing
- Face-to-document matching
- Spoof detection
Supported Documents
- Driver's licenses
- National ID cards
- Passports
- Residence permits
KYC Checks
| Check | Description |
|---|---|
| Face Match | Compare live face to ID photo |
| Document Authenticity | Detect fake/altered documents |
| OCR Extraction | Extract name, DOB, document number |
| Spoof Detection | Detect photos of IDs, screens |
Step 6: Completion
Success
User Experience:
- Success message displayed
- Automatically redirected to
redirect_url
System Actions:
- Status changes to
COMPLETED successset totruecompleted_attimestamp recorded- Webhook sent (if configured)
Failure
User Experience:
- Failure reason displayed
- Option to retry (if attempts remain)
- Redirected to
redirect_url
System Actions:
- Status changes to
COMPLETED successset tofalse- Failure reason stored in metadata
- Webhook sent (if configured)
Reading the result from the redirect
When we send the user back to your redirect_url, we append a signed JWT as a query parameter:
https://your-app.com/verified?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9...The arrival itself proves nothing. Anyone can type your redirect_url into a browser. What
proves the outcome is the token, signed HS256 with your organization API secret:
import jwt from 'jsonwebtoken';
app.get('/verified', (req, res) => {
try {
const claims = jwt.verify(req.query.jwt, process.env.BOUNCER_API_SECRET, {
algorithms: ['HS256'], // pin it: never let the token choose
});
// claims.success, claims.id, claims.metadata
if (!claims.success) return res.redirect('/age-check-failed');
// ... grant access
} catch {
return res.status(401).send('Invalid or expired verification token');
}
});Claims: success, id, metadata, issued_at, expires_at, iat, exp. The token expires one
hour after issue.
The token travels in the query string, so it ends up in your access logs, in the browser history
and in the Referer header of the next request. Consume it on arrival, do not log the full URL,
and do not treat it as a session credential: it is a one-time proof of a single verification.
When to use which channel. The redirect is immediate and needs no extra call, so it is the natural choice when the user is in front of a browser. The webhook is what you need when the outcome must reach a back end that the user never touches, such as a door controller or a vending machine. The status endpoint is the fallback for both. They are independent: one working says nothing about the others.
Retry Handling
Users can retry verification if:
- Liveness check fails (camera issues, etc.)
- KYC document capture fails
The retry_count is incremented with each attempt.
Configure maximum retry attempts in your device or organization settings.
Duration Metrics
The system tracks several duration metrics:
| Metric | Description |
|---|---|
| Biometry Duration | Time spent on liveness check |
| KYC Duration | Time spent on ID scanning |
| Total Duration | Total time from visit to completion |
Access these via the admin dashboard for analytics.
Error States
Session Expired
If the user takes too long:
expires_attimestamp passed- User sees expiration message
- New session required
Technical Errors
- Camera permission denied
- Network connectivity issues
- SDK initialization failures
Integration Example
Complete flow handling:
// 1. Create session on your server
app.post('/start-verification', async (req, res) => {
const { data: session } = await bouncer.createSession({
age_target: 18,
redirect_url: `${process.env.APP_URL}/verification-callback`,
webhook_url: `${process.env.APP_URL}/webhooks/bouncer`,
metadata: [`user_id:${req.user.id}`]
});
// Store session ID for later lookup
await db.verifications.create({
userId: req.user.id,
bouncerSessionId: session.id,
status: 'pending'
});
res.redirect(session.verify_url);
});
// 2. Handle callback
app.get('/verification-callback', async (req, res) => {
// User has returned - check status via webhook or polling
res.render('verification-processing');
});
// 3. Handle webhook
app.post('/webhooks/bouncer', async (req, res) => {
const { session_id, success, status } = req.body;
const verification = await db.verifications.findByBouncerSession(session_id);
await db.verifications.update(verification.id, {
status: success ? 'verified' : 'failed',
completedAt: new Date()
});
// Notify user, unlock content, etc.
res.status(200).send('OK');
});