Integrating KYC
Request a KYC session with force_kyc and force_verification, follow it through its statuses and webhook events, tell when a success came from a document check, and set up devices.
A KYC session is an ordinary Bouncer session that requires an identity document. Tokens, request format, webhooks and their signature, and the redirect work exactly as in Age Verification. This page covers only what changes.
Request a KYC session
Create the session with POST /api/v2/session as usual, and add these two fields:
| Field | Value | What it does |
|---|---|---|
force_kyc | true | The face estimate cannot approve the session: after the face scan, only an identity document can. The face scan still comes first, because the document photo is matched against it. |
force_verification | true | The person always scans in this session. It turns off the re-verification shortcut. |
Why you need both
By default (force_verification: false), a person who recently passed a Bouncer check in the same browser, for the same or a higher age target, is approved as soon as they open the page, with no scan. That shortcut does not look at force_kyc. So a session with force_kyc: true and without force_verification: true can be approved with no document at all.
With both fields set, the shortcut never applies and the face scan cannot approve. The only way to success: true is a document that passed.
Always send force_kyc: true and force_verification: true together. There is no way to add the requirement once the session exists.
Send them as JSON booleans. 1 and 0 are accepted too; the strings "true" and "false", and null, are rejected with 422. Leave a field out to get its default, false.
Give the session enough time
expiration_time counts from the moment you create the session, not from when the person opens it, and it has to cover the face scan and the document scan. The default of 300 seconds can be short for both: use 600 or more (the maximum is 3600), and create the session when the person is ready to start.
Examples
curl -X POST https://app.bouncer.digital/api/v2/session \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"age_target": 18,
"force_kyc": true,
"force_verification": true,
"expiration_time": 600,
"redirect_url": "https://shop.example.com/kyc/return",
"webhook_url": "https://shop.example.com/webhooks/bouncer",
"metadata": { "order_id": "ORDER-123", "check": "kyc" }
}'// Node.js 18 or later (built-in fetch). Run this on your server only.
const BOUNCER_API = 'https://app.bouncer.digital/api/v2';
const headers = {
Authorization: `Bearer ${process.env.BOUNCER_API_TOKEN}`,
Accept: 'application/json',
'Content-Type': 'application/json',
};
export async function createKycSession(orderId) {
const response = await fetch(`${BOUNCER_API}/session`, {
method: 'POST',
headers,
body: JSON.stringify({
age_target: 18,
force_kyc: true, // only a document can approve
force_verification: true, // always scan: no approval carried over from an earlier visit
expiration_time: 600, // room for the face scan and the document scan
redirect_url: 'https://shop.example.com/kyc/return',
webhook_url: 'https://shop.example.com/webhooks/bouncer',
metadata: { order_id: orderId, check: 'kyc' },
}),
});
if (response.status !== 201) {
throw new Error(`Bouncer returned ${response.status}: ${await response.text()}`);
}
const { data } = await response.json();
return data; // { id, verify_url, issued_at, expires_at }: store data.id before you redirect
}A 201 response:
{
"data": {
"id": "0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"verify_url": "https://app.bouncer.digital/verify/0199a1b2-c3d4-7e5f-8a9b-0c1d2e3f4a5b",
"issued_at": 1793181600,
"expires_at": 1793182200
}
}Store the id with your order before you send the person to verify_url. The result never repeats force_kyc or force_verification, so record on your side that you asked for KYC. The example puts "check": "kyc" in metadata for that reason. Keep metadata to a few identifiers with no personal data: it travels inside the redirect token, which anyone who sees the URL can read.
Every call creates a new session, and there is no sandbox: sessions you create while testing are real, and completing one takes a real person with a real document. Handle a 429 with backoff. The other fields, limits and error responses are in Create session.
What the person does
- They open
verify_urland start the face scan. On a computer without a usable webcam they can scan the QR code shown on the page and continue on a phone. - After the face scan, the page shows "Document verification is required" and the document scanner opens. They photograph their identity document as the screen guides them.
- They see the result. On success they return to your
redirect_urlwith a signedjwt. On failure they stay on the Bouncer page, where Verify age with your ID lets them try again in the same session.
The person needs a camera and an identity document at hand. Open verify_url as a top-level page, not in an iframe. The full flow is in Verification flow, and the document step in Document check.
Statuses and events
A KYC session moves through these steps. Every webhook carries event, id, metadata, issued_at and expires_at, plus the fields in the last column.
| Step | status | Webhook event | Extra fields |
|---|---|---|---|
| Your server creates the session | created | none | |
The person opens verify_url | pending | validation.page_visited | visited_at, is_mobile |
| The face scan starts (on a phone, when the person taps Verify Now; on a computer, as soon as they allow the webcam) | verification_started | validation.biometry_started | biometry_started_at, is_retry |
| The document scan starts | verification_kyc | validation.kyc_started | kyc_started_at |
| The document is decided | completed | validation.completed | success, jwt, completed_at |
What to expect:
- Only
validation.completedcarries a result.validation.kyc_startedmeans the document scan has started, not that it passed. Never grant anything on a progress event. - No document step without a live person. If the face scan does not confirm a live person, or the person closes the scanner, there is no document scan and no event. The session stays in
verification_starteduntil they try again or it expires. - A failure can be followed by a success. After
success: false, the failure screen offers Verify age with your ID. That restarts the same session: the status goes back toverification_startedwhilesuccessstaysfalse, thenverification_kyc, then a newvalidation.completed.validation.biometry_started,validation.kyc_startedandvalidation.completedcan each arrive more than once for one session. success: trueis final. Nothing in the session changes after it.- Nothing is sent on expiry. A session abandoned before any verdict keeps its last status with
success: null. Useexpires_atas your deadline. - Do not base logic on
is_retry.
Treat every webhook as possibly the only delivery and reconcile by polling GET /api/v2/session/{id}. The same event can also arrive more than once, so deduplicate by session id and event, and keep in mind that validation.completed can come first with success: false and later with success: true. A simple way to handle all of this: use each webhook only as a trigger to read the session with GET, and update your record from that answer.
curl https://app.bouncer.digital/api/v2/session/SESSION_ID \
-H "Authorization: Bearer YOUR_API_TOKEN" \
-H "Accept: application/json" \
-H "Content-Type: application/json"// BOUNCER_API and headers as in the example above.
export async function readKycSession(sessionId) {
const response = await fetch(`${BOUNCER_API}/session/${encodeURIComponent(sessionId)}`, { headers });
if (!response.ok) {
throw new Error(`Bouncer returned ${response.status}`); // 404: unknown id, or not your organization's
}
const { data } = await response.json();
return data; // { success, id, status, metadata, issued_at, expires_at }
}
// Approved only when completed with success: true. Before expires_at, anything else may still change.
export function kycDecision(session, graceSeconds = 120) {
if (session.status === 'completed' && session.success === true) return 'approved';
const now = Math.floor(Date.now() / 1000);
return now < session.expires_at + graceSeconds ? 'pending' : 'not_verified';
}The grace period after expires_at covers a result recorded just after it, because the last step of the scan was sent before it. How long to wait, and whether to accept a late success, is your decision. Statuses and expiry are covered in full in Sessions.
Know that a success came from a document check
No field in the result says how a session was approved. These are the signals you have:
- What you asked for. If you created the session with
force_kyc: trueandforce_verification: true, or it came from a device with Force KYC Verification on, asuccess: truecan only come from a document that passed. This is the reliable signal, and the reason to record on your side that you asked for KYC. validation.kyc_startedin other sessions. In a session that did not require a document, Bouncer can still ask for one, for example when the face estimate is below your target. Once a document scan has started in a session, only a document can approve that session. So a session for which you receivedvalidation.kyc_startedand that ends withsuccess: truewas approved by a document. That event is a progress signal that may not reach you: use it for reporting, not to decide.- Not the status from GET. GET returns only the current status. Once the session is
completed, it no longer shows that it went throughverification_kyc. - Not the
facetecblock. It is reserved, its content is not part of the API contract and it may change.
If a session's result must rest on a document, require it when you create the session.
Devices
A device (a kiosk, vending machine or point of sale) can require the document in every session it starts:
- In the dashboard sidebar, open Devices. If your organization's Industry is not Vending, the same item is labeled Web. You need the Owner, Admin or Manager role.
- Click Create Device, or open a device and click Edit Device.
- Under Session Settings, turn on Force KYC Verification ("Always require document verification, even if biometry passes.").
- Save with Create Device or Save Changes.
Device sessions always require a fresh scan: Force Verification is always on for devices and cannot be turned off. So Force KYC Verification on its own makes every session of that device a KYC session.
- New sessions only. The setting is copied into each session when it is created. Changing it does not affect sessions already open.
- Printed and dynamic QR codes alike. It applies to sessions started from the device's printed QR code and from links created with
POST /api/v2/device/{deviceCode}/session-url. That endpoint has no field to require the document for one sale only. If a kiosk needs KYC for some sales and not others, create an API session withforce_kycandforce_verificationfor those sales and show itsverify_urlas a QR code. - Age target. The document's age is checked against the device's Age Target, so check it on every device. A device with no age target is checked against 18.
- Results. Device sessions carry Bouncer's own
metadata(device_id,device_code,device_name), not yours. You learn theiridand result from the device's Webhook URL, and after a success also from thejwtadded to the device's Redirect URL. The progress events,validation.kyc_startedincluded, are sent only when Send all events is on.
Everything else about devices is in Devices.
Shared reference
API tokens
Create a token and find the API Secret.
Sessions
Every field, status and expiry rule.
Webhooks
Payloads, signature verification and reconciliation.
Verification flow
What the person sees, and the return to your site with the signed token.
Devices
Kiosks, vending machines and points of sale.
API reference
Create session, get session status and device links.
Document check
The document step and what decides pass or fail.
Results
Reading and acting on a KYC result.
KYC: identity verification
The person scans their face and a physical identity document. Bouncer checks that the document matches the face and shows the required age, and tells your server whether the person passed.
Document check
What the person goes through in a KYC session, screen by screen, and exactly what makes the identity document pass or fail.