Skip to content

DEVELOPER SETUP GUIDE

Add age verification to your website

Follow the steps below to create a check, guide your customer through it and confirm the result.
Use the same flow with your developer or an AI coding tool.

Open plain text
On this page
You choose the experience. Your developer connects it.

Start with your business settings below. Then share this guide with whoever builds your website. You can also paste the full guide into an AI coding tool. The steps explain what happens; open the code only when you need it.

New to this? What the words mean
Widget
The branded window where your customer completes an age check.
Session
The record of one customer’s check, from starting it to getting a result.
Server or backend
The part of your website that works behind the scenes. It keeps your secret key safe and checks the result.
API
The connection your website uses to ask ID Before Entry to start a check and return its result.
Service address for your developer
ID Before Entry base URLhttps://YOUR_ID_BEFORE_ENTRY_HOST

Use this verification service address in the examples. Your own website has a separate address.

1. Prepare your website

You can choose your branding and age-check settings in your ID Before Entry account. Connecting the check to your website is a job for your website developer. They will use your server, sometimes called the backend: the part of your website that runs behind the scenes, away from the customer’s browser.

  1. Add your website. In your dashboard, open Websites and give your website a name you will recognise.
  2. Make the check yours. Add your logo and wording, choose the minimum age and how customers will verify it. Decide whether to ask for a selfie with photo ID and let customers move a check from their computer to their phone.
  3. Give your developer the setup instructions. Open Add to website for this website. Its public key identifies the check; its secret key lets your server create checks and read results. Ask your developer to store the secret securely on your server. Never put it in a webpage or an AI chat. It is only shown when created or replaced.
  4. Confirm where the check will run. Ask your developer to add the correct website addresses in your settings. If customers use a separate verification page, also choose the page they should return to. Test and live website addresses need their own entries.
Setup values for your developer
ValueWhere it belongs
ID_BEFORE_ENTRY_BASE_URLYour server configuration: the published ID Before Entry address. Current example: https://YOUR_ID_BEFORE_ENTRY_HOST.
ID_BEFORE_ENTRY_SECRET_KEYServer environment only. Use the sk_… key for the website you are connecting. Never use a public frontend environment variable.
siteKeyBrowser popup configuration. Your website’s pk_… public key; it must belong to the same website as the secret key.
return_urlYour own application’s return page, for example https://your-site.example/age-check/return. It must match your allowed return addresses.
referenceAn opaque customer or checkout reference chosen by your server, up to 200 characters. Avoid names, email addresses or document details.

Who does what? ID Before Entry runs the age check. Your developer connects it to your website, so your website knows which customer is checking and what to do with the result. The code examples below are for them to add to your website.

Trying it out: new accounts have 7 days of unlimited trial checks. Afterwards, each new check costs £1 and uses one prepaid credit. New checks pause when your allowance is unavailable. Card payments are not yet available in the app; your Plan and billing page shows your trial and credit balance.

2. Choose your customer journey

Choose where you want customers to complete their check. Both options link the result back to the right customer. We call the record of one age check a session; your developer connects that record to your customer’s account or checkout.

Your website must confirm the answer. Your developer makes your server check the result directly with ID Before Entry before letting a customer continue. A message in their browser or a return to your website is not proof of a pass.

3. Start a check for the right customer

For your developer: connect the start of each age check to the right customer. Your website supplies a customer reference, such as its own account or checkout number, and saves which check belongs to them. The customer must not be able to choose someone else’s reference or check.

You do not need to write this code yourself. Give your developer the example below or the complete integration guide. They need to adapt it to your website’s sign-in, checkout and record-keeping systems; it is a template, not code you can simply paste into any website.

View example codeYour backend · create and save a check
Your backend · create and save a check
// 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://YOUR_ID_BEFORE_ENTRY_HOST";
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" } },
    );
  });
}

What you should see: starting a check opens a new journey for that customer. Behind the scenes, your website has saved which check belongs to their account or checkout, before giving that customer access to it.

4B. Use a separate verification page

Choose this instead of the popup if you want customers to visit a separate page for their check. Tell your developer which page customers should return to. They will use the unique check link created for each customer and make sure the return page is allowed in your website settings.

View example codeYour application · hosted check and return
Your application · hosted check and return
<!-- YOUR WEBSITE. No browser SDK is needed for a hosted redirect. -->
<!-- Sessions are created by your server using https://YOUR_ID_BEFORE_ENTRY_HOST/api/public/v1/sessions. -->
<meta name="csrf-token" content="RENDER_YOUR_CUSTOMER_CSRF_TOKEN_HERE">
<button id="start-age-check" type="button">Verify my age</button>
<p id="age-check-message" role="status"></p>
<script>
  async function merchantPost(path, body) {
    const response = await fetch(path, {
      method: "POST",
      credentials: "same-origin",
      headers: {
        "Content-Type": "application/json",
        "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content,
      },
      body: JSON.stringify(body),
    });
    if (!response.ok) throw new Error("Your website could not complete the request");
    return response.json();
  }

  document.getElementById("start-age-check").addEventListener("click", async (event) => {
    const button = event.currentTarget;
    button.disabled = true;
    try {
      const session = await merchantPost("/api/age-check/start", { flow: "redirect" });
      window.location.assign(session.url); // Use the API URL unchanged.
    } catch {
      document.getElementById("age-check-message").textContent =
        "The age check could not start. Please try again later.";
      button.disabled = false;
    }
  });

  // ON YOUR RETURN PAGE, after the customer returns and auth is restored:
  // Run this in that page's own script, not immediately on the start page.
  async function confirmReturnedAgeCheck() {
    const params = new URLSearchParams(window.location.search);
    const result = await merchantPost("/api/age-check/confirm", {
      returnedSessionId: params.get("carded_session"),
    });
    // carded_status and carded_reason are untrusted hints. Never use them to grant access.
    // Only the server-confirmed response can update this page's UI.
    document.getElementById("age-check-message").textContent = result.verified === true
      ? "Age check confirmed. You can continue."
      : "Your age check has not passed. Follow the website's next steps.";
  }
  // Your return page calls confirmReturnedAgeCheck().catch(showYourRecoveryMessage).
  // A closed browser tab or abandoned check may never visit the return page.
</script>

What you should see: after the check, customers can select Back to the website to return to your chosen page. Your website must then confirm their saved result directly with ID Before Entry. Information in the browser’s address bar is not proof of a pass, and a customer who closes the page may never return.

5. Confirm the result before allowing access

For your developer: this is the final check before a customer can continue. Your server asks ID Before Entry for the result of the check it saved earlier, confirms that it belongs to the same customer and records a successful result securely. It must not rely on a success message shown in the browser.

View example codeYour backend · confirm the saved session
Your backend · confirm the saved session
// 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://YOUR_ID_BEFORE_ENTRY_HOST";
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.
  • Let a customer continue only when ID Before Entry confirms a pass and the saved check and customer reference both match. Your developer must enforce this on your server.
  • If a customer refreshes or returns twice, your website should recognise the same result without creating duplicate access or orders.
  • If the result is missing, unclear, belongs to someone else or cannot be checked because of a connection problem, keep the customer unverified and explain their next step.

6. Check everything works

Walk through the journey with your developer before customers use it. Check what happens on a phone and computer, when a customer passes, and when they cannot finish. Your developer should also test the security checks behind the scenes. There is no separate test mode: these are real checks and use your available trial or check allowance.

CheckExpected behaviour
A successful checkThe backend confirms the saved session and matching reference before granting access.
Failed, pending or expired sessionThe customer remains unverified; your page explains the next step.
Customer closes the popupNo access is granted from the browser result. Your application can check the saved session’s actual status.
Changed status or session ID in the browserYour backend ignores browser proof and uses the session bound to the authenticated customer.
A different customer’s sessionIt cannot be used to verify the current customer, even if both belong to the same website.
API error or no check creditYour page handles the error without allowing access or repeatedly creating paid sessions.
Phone handover and returnThe same session is completed and your original application receives or retrieves the result.
Production configurationUse your published verification URL, correct site keys, HTTPS and the intended allowed website and return addresses.

API reference

For developers

These are the two public server endpoints. Send Authorization: Bearer sk_… using your secret key. JSON responses are not cached. There is no webhook or public REST cancellation endpoint in the current integration.

POST/api/public/v1/sessions

Creates a session for the website identified by your secret key. Returns HTTP 201. All body fields are optional to the API; supply a server-owned reference for your integration.

JSON fieldAccepted value
referenceString, trimmed, up to 200 characters. An empty value is returned as null.
return_urlAn allowed absolute URL, up to 1,000 characters. Needed for a hosted return; normally omitted for a popup.
expected_sexOptional "male" or "female" comparison. Omit for an ordinary age check; see the optional comparison section.
View example codeExample · create-session response
Example · create-session response
{
  "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"
}

The url and client_token are returned on creation only. Do not log them or share them with another customer. Creating a session can use a check credit; there is no idempotency-key support, so avoid automatic duplicate POST retries.

GET/api/public/v1/sessions/:id

Returns HTTP 200 with the session’s current result. Use the secret key for the same website that created the session. It returns 404 if the ID is invalid, missing or belongs to another website.

View example codeExample · retrieve-session response
Example · retrieve-session response
{
  "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
}
FieldMeaning
object / idverification_session and the session UUID.
status / passedCurrent status and boolean result. passed is true only for status passed.
reasonA known reason code for a failed session; otherwise null.
referenceThe reference assigned when the session was created, or null.
min_age / attemptsThe age requirement captured at session creation and the number of attempts used.
sourceapi for server-created sessions; widget or hosted for sessions started directly in those flows.
created_at / completed_at / expires_atISO timestamps. completed_at can be null. Pending sessions expire after 30 minutes; expiry does not invalidate an already passed result.
face_photoPhoto data URL when a shared face photo is still available; otherwise null. Available for 24 hours, then hidden and automatically removed.

Statuses and what to do next

For developers
StatusMeaningYour application
pendingNo final result yet.Keep access unverified. Check again after the customer finishes; if polling, use a bounded delay and stop at expiry.
passedThe configured verification rules were met.Confirm the saved session and reference, then record success.
failedThe check ended without passing.Keep access unverified and use the reason code to explain the outcome.
cancelledThe server session was cancelled.Do not grant access. Offer an explicit restart if appropriate.
expiredA pending session exceeded its 30-minute lifetime.Do not resume it. Let the customer choose to start a new session.

A closed browser popup can report cancellation before the server has a final result. The server response is authoritative.

API errors

For developers

Check the HTTP status before reading a success response. An API error is different from a completed verification with status: "failed".

View example codeError response shape
Error response shape
{ "error": { "code": "invalid_key", "message": "That secret key isn’t valid." } }
HTTPCodeAction
400invalid_json / invalid_requestCheck JSON syntax, field types and field lengths.
400return_url_not_allowedAdd the intended return address to this website’s allowed return URLs.
401missing_key / invalid_keyCheck the server Authorization header and the current secret key.
402checks_pausedCheck the business account’s trial, credit balance and availability. Do not keep retrying session creation.
403site_pausedThe website is paused. Enable it before creating another session.
404not_foundCheck the saved session ID and that you are using the matching website’s secret key.
500server_errorShow a temporary failure. A new create request may consume another credit; do not blindly retry it.

Verification reason codes

For developers

The widget handles photo guidance and retries within the configured attempt limit. Your application uses the final server result. “May retry” describes a fixable problem, not permission to accept a failed session.

CodeMeaningMay retry
underageThe date of birth on the ID is below the minimum age set for this widget.No
document_expiredThe photo ID has passed its expiry date.Yes, while attempts remain
document_unreadableThe date of birth or photo on the ID could not be read clearly.Yes, while attempts remain
document_not_acceptedThe image was not an accepted photo ID. Only passports and driving licences are accepted.Yes, while attempts remain
document_not_originalThe ID appeared to be a photocopy, a printout or a picture of a screen.Yes, while attempts remain
document_type_mismatchThe ID photographed was not the type the person chose (passport or driving licence).Yes, while attempts remain
face_mismatchThe selfie did not appear to match the photo on the ID.Yes, while attempts remain
selfie_unclearThe selfie was missing a clear, single face.Yes, while attempts remain
suspected_fraudThe ID or selfie showed signs of tampering or spoofing.No
face_unclearThe face scan did not show one clear, live face.Yes, while attempts remain
age_estimate_too_closeThe face scan could not confidently show the person is over the minimum age.No
sex_mismatchThe sex shown on the ID or face scan did not match the sex the website expected.No
sex_unclearThe sex the website expected could not be confirmed from the face scan or ID.Yes, while attempts remain

Two exceptions depend on the flow: age_estimate_too_close can switch to photo ID when both methods are offered and attempts remain. A face scan with sex_unclear can fall back to photo ID while attempts remain; on that ID fallback, an unclear result is final.

Photos and phone handover

For developers

What your application receives

ID document images are checked in memory and are not stored in ID Before Entry’s database or file storage. Document names, addresses, dates of birth and numbers are not returned to your application.

Face scan photos and optional ID selfies are shared for 24 hours. The server field is face_photo; the popup field is facePhoto. They are null when no shared photo is available. Do not log these fields. If your application saves a copy, manage that copy under your own data-handling policy; ID Before Entry’s expiry does not delete it from your system.

Let customers continue on a phone

When phone handover is enabled in website settings, a customer can scan a QR code and use their phone’s camera. The session ID stays the same. The original device checks for completion, so you do not need a separate mobile integration.

Optional: an additional comparison

For developers

Ordinary age checks do not need expected_sex. Leave it out unless your application explicitly requires this separate comparison.

Read the optional expected_sex behaviour

Set expected_sex to "male" or "female" when your server creates the session. For the server-created flow, the saved value is used; do not rely on a browser option to change it later.

  • A mismatch ends with sex_mismatch. An unclear ID value ends with sex_unclear.
  • An unclear face result can move to photo ID without a selfie, including on face-only websites, while attempts remain. The original shared face photo is retained until its expiry.
  • The observed value is not stored or returned. Face-based inference can be inaccurate and must not be described as a definitive finding.