# ID Before Entry — website integration guide This is the current integration contract for ID Before Entry. It is intended for a website developer or an AI coding assistant working in the merchant's own application. Examples are framework-neutral reference code: adapt the explicitly named merchant integration points before use. They are not a runnable package or a substitute for your application's authentication and access controls. ## 1. Configuration and integration boundaries Verification service origin: `https://idbeforeentry.com` Server API base: `https://idbeforeentry.com/api/public/v1` Browser script: `https://idbeforeentry.com/widget.js` If this guide shows localhost or 127.0.0.1, that is a local preview address, not a production service. Replace it with your published ID Before Entry origin before deploying. An AI tool running on another computer cannot use your local preview URL. Keep the verification service origin separate from the merchant website and return URL. Create a business account, add a website and obtain that website's public and secret keys from its Add to website tab. Configure its branding, minimum age, verification method, optional ID selfie match, maximum attempts and phone handover in the dashboard. Minimum age is copied onto the session when it starts. Do not assume every other setting is frozen for a running session. Check allowance: new accounts receive 7 days of unlimited checks from signup. Afterwards, each new session costs £1 and consumes one prepaid credit. When checks are unavailable the create API returns 402 checks_paused. Card payments are not yet available in the app; inspect Plan and billing for the account's trial and balance. A repeated create request can consume another credit. ```text ID_BEFORE_ENTRY_BASE_URL=https://idbeforeentry.com ID_BEFORE_ENTRY_PUBLIC_KEY=pk_REPLACE_WITH_YOUR_24_CHARACTER_PUBLIC_KEY ID_BEFORE_ENTRY_SECRET_KEY=sk_REPLACE_WITH_YOUR_40_CHARACTER_SECRET_KEY ID_BEFORE_ENTRY_RETURN_URL=https://YOUR-WEBSITE.example/age-check/return ``` These values are placeholders. Public keys are pk_ followed by 24 alphanumeric characters; secret keys are sk_ followed by 40. The public key may be in browser code. The secret key belongs only in server-side environment variables or a secrets manager: never expose it in client bundles, HTML, URLs, logs or chat prompts. For several websites, select the matching provider site/key from trusted server configuration for each stored binding. Add explicit production website origins (scheme, host and port) in the dashboard before using the popup. Origins use HTTPS except localhost/127.0.0.1 in local development; an empty origin list is permissive, so do not leave it empty in production. Add your hosted callback URL to Allowed return URLs. Return URL matching compares origin and path, ignoring query strings, fragments and a trailing slash; HTTPS is required except local development. Keep the callback fixed in your server configuration, never accept an arbitrary return URL from the browser. For a popup, load the script and permit its origin in your script/frame rules. The SDK creates an iframe with camera permission; configure your site's relevant CSP and Permissions Policy for that embed. Camera capture requires a secure browser context (HTTPS in production). For a hosted redirect, no browser SDK is needed. ## 2. The secure lifecycle 1. Authenticate the customer (or establish a trusted server-managed checkout session). Derive an opaque, stable reference on your server; never accept the customer identity/reference from a browser field. 2. Your server creates a session with the website's secret key. Rate-limit and guard your own start endpoint against concurrent or duplicate starts. 3. Persist the provider session ID, expected reference, customer/checkout identity, expiry and provider-site selection in your own database BEFORE handing back the client token or URL. A database failure must prevent launching an unbound check. 4. Open the popup with the returned session ID/client token, or redirect to the returned hosted URL unchanged. Both flows use the SAME server-created session. 5. When the popup closes or the customer returns, your server finds its stored binding and reads that session from the provider. Compare both session.id and session.reference with the stored values; accept only status === 'passed' AND passed === true. 6. Persist the successful result atomically/idempotently for that exact customer, checking that the binding is still current. Enforce it on your server before granting protected access. A success message in the browser is not an access control. The provider API authenticates the website, not your individual customer. A session ID submitted by the browser—even with a matching reference—is not sufficient proof of ownership. Browser callbacks, postMessage data and return-query values are untrusted notifications. Read only the ID already bound to the authenticated customer on your server. Client tokens and hosted URLs authorize access to an individual check. Send them only to the correct customer's browser. Do not put them in analytics, third-party logging or public pages. Do not reuse one customer's session for another. Store resumable tokens only if needed, protected on your server; the example stores the binding, not a reusable token. ## 3. Create a bound session on your server Implement the merchant endpoint /api/age-check/start. The merchant.requireCustomer and merchant.requireCsrf adapters are your authentication and CSRF checks. merchant.withStartGuard must apply rate limits, serialize starts for a customer and prevent duplicate pending attempts; decide explicitly how your application resumes or restarts a check. merchant.verifications.saveCurrent must durably bind the session to the customer. Implement framework error handling for invalid JSON, network/JSON parsing failures and database failures; fail closed and show a safe retry message. Do not log tokens or complete provider responses. ```javascript // YOUR SERVER: implement POST /api/age-check/start on your website. // Framework-neutral example, not a ready-made server or SDK package. // merchant.* are REQUIRED adapters for your own auth, CSRF, rate limits and database. const VERIFICATION_BASE = "https://idbeforeentry.com"; const SECRET_KEY = process.env.ID_BEFORE_ENTRY_SECRET_KEY; const RETURN_URL = "https://YOUR-WEBSITE.example/age-check/return"; async function startAgeCheck(request) { const customer = await merchant.requireCustomer(request); await merchant.requireCsrf(request); const { flow } = await request.json(); if (flow !== "popup" && flow !== "redirect") { return Response.json({ error: "invalid_flow" }, { status: 400 }); } // This adapter MUST serialize starts for this customer and stop duplicates. // A repeated POST to the provider can create another session and spend another credit. return merchant.withStartGuard(customer.id, async () => { const reference = String(customer.id); // Derived from your authenticated server context. if (!reference || reference.length > 200) throw new Error("Invalid customer reference"); if (!SECRET_KEY) throw new Error("Missing server configuration"); const response = await fetch(VERIFICATION_BASE + "/api/public/v1/sessions", { method: "POST", headers: { Authorization: "Bearer " + SECRET_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ reference, ...(flow === "redirect" ? { return_url: RETURN_URL } : {}), }), cache: "no-store", }); const session = await response.json(); if (!response.ok) { // Map the provider code to your own safe error UI; do not log the whole response. return Response.json( { error: session.error?.code || "verification_unavailable" }, { status: response.status, headers: { "Cache-Control": "no-store" } }, ); } if (!session.id || !session.client_token || !session.url) { throw new Error("Unexpected create-session response"); } // DURABLY bind before returning credentials or redirecting the customer. // Save which provider site/key belongs to this binding if you manage several sites. await merchant.verifications.saveCurrent({ customerId: customer.id, sessionId: session.id, reference, expiresAt: session.expires_at, }); return Response.json( { id: session.id, client_token: session.client_token, url: session.url }, { headers: { "Cache-Control": "no-store" } }, ); }); } ``` The example accepts only popup or redirect as the browser's flow choice. For the redirect flow, replace RETURN_URL with your fixed, allowlisted callback. API creation accepts a reference and optional expected_sex and return_url; it does not accept a per-request minimum age or method override. Those are website settings. ## 4. Popup flow Use the public key for the same website as your server secret. The loader below waits for the SDK before calling verify. Replace the CSRF meta value using your framework, and implement the two merchant endpoints from this guide. Do not use the simpler declarative auto-create button for this bound-session flow. ```html

``` The SDK promise resolves when the popup closes. onResult may fire earlier, when a result is ready; onComplete receives the same normalized result as the resolved promise. Avoid running your grant/update logic twice if you also use callbacks. A second verify call while a popup is active returns that active promise. ### Browser SDK options and result | Option | Contract | | --- | --- | | siteKey | Required public key for the website. Invalid format rejects the promise. | | session: { id, token } | Resume the API-created session. token is the POST response's client_token. | | onResult(result) | Called when a result is reported, before popup close. Notification only. | | onComplete(result) | Called when the popup closes; same normalized value as the promise. | | onError(code) | Startup error notification. It does not automatically close the popup or reject its promise. | | reference | Optional, up to 200 characters, for browser-created sessions only. In the recommended flow the API-stored reference wins. | | expectedSex | Optional male or female for browser-created sessions only. In the recommended flow use server-side expected_sex. | ```javascript { sessionId: "SESSION_UUID_OR_NULL", status: "passed", passed: true, reason: null, facePhoto: null } ``` The browser uses camelCase sessionId/facePhoto; the REST API uses id/face_photo. The browser result can be passed, failed, cancelled, expired or pending; its passed field is derived from its status, not server-side proof. Startup/early-close results may have sessionId: null. Common startup onError codes include unknown_site, site_paused, checks_paused, origin_not_allowed, return_url_not_allowed and session_not_found; tolerate other codes and browser/network failures too. IDBeforeEntry.close() removes the popup UI and resolves with the last result, or a cancelled-shaped result if none exists. It does not itself cancel the stored server session. Closing during initial loading can therefore resolve cancelled with no session ID while the API still shows pending. The widget's own close control attempts cancellation once its session has loaded. Always confirm the stored session rather than assuming a UI close changed provider state. There is no public REST cancel endpoint in this API. The SDK retains compatibility aliases/events: window.Carded, carded:ready, data-carded-verify and carded:complete. New programmatic code should use window.IDBeforeEntry. The declarative attributes create browser-originated sessions and do not implement this guide's server binding. ## 5. Hosted redirect flow Use the same merchant start endpoint with flow: redirect. It supplies the fixed allowlisted return_url to the create API. Redirect to session.url only after the binding has been saved. On your return page, restore the customer's authenticated context and call your own confirm endpoint. The page must not grant access based on the query string. ```html

``` On completion, the verification page's Back to the website action appends carded_session and carded_status to the return URL, and carded_reason when a reason exists. These legacy names are intentional. They are hints, not signatures. The return is user-driven; abandoned checks and closed tabs may never return. Without return_url, the hosted page finishes on its own completion screen. ## 6. Confirm and enforce the result on your server Implement /api/age-check/confirm. merchant.verifications.currentFor must return only the authenticated customer's stored binding. confirmIfCurrent must atomically check the current binding and record verification for that customer, returning true for an already-confirmed identical binding. Never trust a client-supplied verified flag or query status. The example returns only a small safe summary to the browser. ```javascript // YOUR SERVER: implement POST /api/age-check/confirm on your website. // merchant.* are REQUIRED adapters for your auth, CSRF and database. const VERIFICATION_BASE = "https://idbeforeentry.com"; const SECRET_KEY = process.env.ID_BEFORE_ENTRY_SECRET_KEY; async function confirmAgeCheck(request) { const customer = await merchant.requireCustomer(request); await merchant.requireCsrf(request); const input = await request.json(); const binding = await merchant.verifications.currentFor(customer.id); if (!binding) return Response.json({ error: "no_bound_check" }, { status: 404 }); // The redirect query is a hint to compare, never the ID to fetch from the provider. if (input.returnedSessionId != null && input.returnedSessionId !== binding.sessionId) { return Response.json({ error: "check_mismatch" }, { status: 403 }); } if (!SECRET_KEY) throw new Error("Missing server configuration"); const response = await fetch( VERIFICATION_BASE + "/api/public/v1/sessions/" + encodeURIComponent(binding.sessionId), { headers: { Authorization: "Bearer " + SECRET_KEY }, cache: "no-store", }, ); if (!response.ok) { // Fail closed. Show a retry/support message; do not mark the customer as verified. return Response.json( { error: "verification_unavailable" }, { status: 503, headers: { "Cache-Control": "no-store" } }, ); } const session = await response.json(); if (session.id !== binding.sessionId || session.reference !== binding.reference) { return Response.json({ error: "check_mismatch" }, { status: 403 }); } const verified = session.status === "passed" && session.passed === true; if (verified) { // Atomic + idempotent: re-check that this binding is still current for this customer. // Store the provider session ID and verified outcome against this exact customer. const saved = await merchant.verifications.confirmIfCurrent(customer.id, binding.sessionId); if (!saved) return Response.json({ error: "check_changed" }, { status: 409 }); } return Response.json( { verified, status: session.status, reason: session.reason }, { headers: { "Cache-Control": "no-store" } }, ); } // pending: keep access locked; confirm again later with bounded polling/backoff. // failed: keep access locked and present the reason/next step. // cancelled or expired: keep access locked; offer an explicit new attempt when appropriate. // Network errors, unknown statuses and malformed responses must never grant access. // Do not forward the entire provider payload: face_photo may contain a customer's photo. ``` Use the matching site's secret for the stored binding. In a multiple-site integration, the provider site/key selection must also be stored and validated server-side. Your recovery path can repeat GET confirmation with bounded polling/backoff, but must not blindly repeat POST creation. There is no idempotency-key contract: duplicate POSTs can create extra sessions and consume extra credit. Do not assume a timeout means a session was not created. ## 7. REST API reference Requests are server-to-server. Send Authorization: Bearer sk_... with the selected website's secret. Send Content-Type: application/json on POST. Successful and structured error responses use Cache-Control: no-store. The client token is not a substitute for the server secret on these endpoints. ### POST /api/public/v1/sessions | Optional JSON field | Type and validation | | --- | --- | | reference | String, trimmed, maximum 200 characters. Omitted/empty is stored as null. Supply your server-assigned value for the secure flow. | | return_url | Valid URL, maximum 1000 characters, must match the website's allowed return URLs. Omit for popup flow. | | expected_sex | male or female. Optional advanced comparison; omit unless your product explicitly requires it. | Returns HTTP 201 with the session fields plus client_token and url. The generated URL contains the public site key, session ID and client token. The site is selected by the secret key, not by a siteKey request field. Example response (illustrative placeholders): ```json { "object": "verification_session", "id": "8f0c2a5e-6b1d-4c2e-9a77-1f6b3c9d2e10", "status": "pending", "passed": false, "reason": null, "reference": "customer-42", "min_age": 18, "attempts": 0, "source": "api", "created_at": "2026-01-01T10:00:00.000Z", "completed_at": null, "expires_at": "2026-01-01T10:30:00.000Z", "face_photo": null, "client_token": "CLIENT_TOKEN_RETURNED_BY_THE_API", "url": "https://YOUR-VERIFICATION-DOMAIN/verify/pk_REPLACE_WITH_YOUR_24_CHARACTER_PUBLIC_KEY?session=8f0c2a5e-6b1d-4c2e-9a77-1f6b3c9d2e10&token=CLIENT_TOKEN_RETURNED_BY_THE_API" } ``` ### GET /api/public/v1/sessions/:id Returns HTTP 200 for a session belonging to the authenticated website. Replace :id with the UUID saved on your server. The GET response does not include client_token or url. Example passed response: ```json { "object": "verification_session", "id": "8f0c2a5e-6b1d-4c2e-9a77-1f6b3c9d2e10", "status": "passed", "passed": true, "reason": null, "reference": "customer-42", "min_age": 18, "attempts": 1, "source": "api", "created_at": "2026-01-01T10:00:00.000Z", "completed_at": "2026-01-01T10:02:00.000Z", "expires_at": "2026-01-01T10:30:00.000Z", "face_photo": null } ``` | Response field | Meaning | | --- | --- | | object | verification_session | | id | Session UUID. | | status / passed | Current status and boolean; passed is true only for status passed. | | reason | Failure reason code only when status is failed; otherwise null. | | reference | Your supplied reference or null. | | min_age | Minimum age copied onto this session at creation. | | attempts | Number of attempts recorded for the session. | | source | api for this server-created flow; widget or hosted for directly created sessions. | | created_at / completed_at | ISO timestamps; completed_at may be null. | | expires_at | Session deadline. Pending sessions expire after 30 minutes. It does not invalidate an already passed result. | | face_photo | Photo data URL or null, available only within its sharing window. Never an ID document image. | ### Status handling | Status | What your integration should do | | --- | --- | | pending | Do not grant access. Let the customer finish; confirm again later as needed. | | passed | Grant only after the stored-session/reference match, passed === true and your atomic customer update. | | failed | Do not grant access. Use reason for appropriate next steps. | | cancelled | Do not grant access. Offer a deliberate new attempt if appropriate. | | expired | Do not grant access. An unfinished session exceeded its deadline; offer a deliberate new attempt. | ### Errors ```json { "error": { "code": "invalid_key", "message": "That secret key is not valid." } } ``` | HTTP | Code | Action | | --- | --- | --- | | 400 | invalid_json | POST body is not valid JSON. Correct your request. | | 400 | invalid_request | POST fields failed validation. Correct the field/type/length. | | 400 | return_url_not_allowed | Add the exact callback origin/path in website settings. | | 401 | missing_key | Send the server secret in the Authorization header. | | 401 | invalid_key | Check the key and site; it may have been rotated. | | 402 | checks_paused | New checks are unavailable under the account's current allowance or paused state. Inspect Plan and billing/account status. | | 403 | site_paused | Re-enable the website before creating checks. | | 404 | not_found | GET ID is malformed, unknown or belongs to another website; do not grant access. | | 500 | server_error | Session creation failed. Handle safely; do not blindly retry creation. | Also handle transport failures, invalid/non-JSON responses and unexpected HTTP statuses by failing closed. GET confirmation can be retried with backoff. The public API currently defines only POST create and GET read; it does not define a public webhook subscription, public cancel endpoint, idempotency header, npm SDK package or public API 429 contract. The widget's rate_limited startup code is a separate internal flow and should not be documented as a REST 429 response. ### Failed-check reason codes | Code | Meaning | | --- | --- | | `underage` | The date of birth on the ID is below the minimum age set for this widget. | | `document_expired` | The photo ID has passed its expiry date. | | `document_unreadable` | The date of birth or photo on the ID could not be read clearly. | | `document_not_accepted` | The image was not an accepted photo ID. Only passports and driving licences are accepted. | | `document_not_original` | The ID appeared to be a photocopy, a printout or a picture of a screen. | | `document_type_mismatch` | The ID photographed was not the type the person chose (passport or driving licence). | | `face_mismatch` | The selfie did not appear to match the photo on the ID. | | `selfie_unclear` | The selfie was missing a clear, single face. | | `suspected_fraud` | The ID or selfie showed signs of tampering or spoofing. | | `face_unclear` | The face scan did not show one clear, live face. | | `age_estimate_too_close` | The face scan could not confidently show the person is over the minimum age. | | `sex_mismatch` | The sex shown on the ID or face scan did not match the sex the website expected. | | `sex_unclear` | The sex the website expected could not be confirmed from the face scan or ID. | The customer UI manages retries according to the website's attempt limit and the reason. A final API failed status is final for that session. An age estimate that is too close can switch to photo ID when both methods are offered and attempts remain. Do not infer guaranteed retry behaviour solely from a reason code. ## 8. Optional expected-sex comparison If your product explicitly needs this optional comparison, supply expected_sex: male or female from trusted server data when creating the session. For directly browser-created sessions the SDK calls this expectedSex. The check compares the expected value with the document/face result, and returns only the overall result/reason (sex_mismatch or sex_unclear when relevant); detected sex is not returned or stored. It is not a general profile or identity field. If a face scan cannot confirm this value and attempts remain, the flow moves to photo ID without requesting an ID selfie, including when the website normally offers face-only checks. The existing face photo may carry through that fallback. An unclear or mismatching value on the ID fallback is final. Do not add this optional field automatically to an ordinary age-check integration or describe estimates as certain. ## 9. Photos and customer data ID document images are analysed in memory and not saved to ID Before Entry's database or storage. Photos are analysed by an AI service. The API does not expose names, addresses, dates of birth or document numbers. Face scan photos and optional ID selfies are different: face_photo may contain a photo data URL for 24 hours, after which it is hidden and expired photos are removed automatically. The expiry timestamp for that photo is not included in this API response. Always tolerate face_photo: null. Do not infer its presence from the current route or selfie setting: a saved face scan photo can carry through an ID fallback. Do not log full session objects or automatically forward photos to analytics/browser clients. Decide how your business will handle any received face photos and describe that accurately in your own customer information. The example confirm endpoint deliberately omits them. ## 10. Launch checklist Phone handover: when enabled in website settings, the widget lets someone starting on a computer scan a QR code and take photos on a phone. It reuses the same session ID and client token; the original device polls for completion and receives the result. No separate phone session or mobile API integration is needed. Do not create a second session for this handover. Your own server must still confirm the stored session before allowing access. - Replace every domain, key and CSRF placeholder; keep secret keys server-side and out of client builds. - Implement authentication/server-managed checkout identity, CSRF, rate limits and duplicate-start guards on your merchant endpoints. - Save the exact customer/session/reference/provider-site binding before exposing the client token or hosted URL. - Configure HTTPS origins and fixed allowed return URLs. Test your CSP, iframe camera permissions and SDK loading failures. - Test popup and/or redirect flows, plus phone QR handover if enabled. Confirm the result reaches the original customer context. - Test passed, failed, pending, cancelled and expired outcomes, plus early popup close, network failure and abandoned hosted pages. - Tamper with browser session IDs, references and return-query statuses: none may grant access to the wrong customer. - Verify server-side protected actions require the durable confirmed outcome; a browser success message alone must do nothing privileged. - Test invalid/rotated keys, paused websites, exhausted allowance and duplicate start requests without creating uncontrolled extra sessions. - Confirm multi-site bindings use the right site's secret and customer; make completion updates atomic and idempotent. - Verify logs/analytics exclude secrets, client tokens, token-bearing URLs, full provider payloads and photos. - Provide clear retry/support messages, and check your customer-facing explanation of any face-photo sharing. ## Instructions for an AI coding assistant Inspect the merchant application's framework, authentication, checkout/session model, CSRF mechanism and database before adapting these examples. Implement the merchant adapters in that stack rather than inventing an SDK package. Keep provider calls on the server, implement the binding and confirmation checks exactly, and use the actual contract above. Do not change the ID Before Entry customer widget, claim a webhook exists, expose the secret, trust browser outcomes, or invent legal/accuracy/compliance guarantees. Describe any missing merchant configuration clearly and test the fail-closed paths before going live.