Skip to content

KYC verification

LootBox Solutions can run identity verification (KYC) for your players end to end: you request a verification link over S2S, the player completes a hosted verification page, the platform’s compliance team reviews the submission, and you mirror the outcome from a webhook. You never handle documents — only statuses.

The flow

  1. Your backend mints a verification URL with POST /api/s2s/kyc/sessions, passing the player’s contact email and, optionally, a returnOrigin for the completion signal.
  2. Your page opens the URL in a popup window — window.open, not an iframe. The verification page refuses framing outright (it ships frame-ancestors 'none'), so an iframe embed renders nothing. A plain window.open(kycUrl) popup, PSP-checkout style, is the supported shape.
  3. The player completes the hosted wizard — entering their identity details (legal name, date of birth, tax/national id, address) and uploading their identity documents. The page also offers a QR hand-off so the player can continue on their phone mid-flow; no work on your side.
  4. The platform team reviews the submission — checking the entered details against the documents — and confirms or rejects it.
  5. You track every step via the kyc_status_changed webhook — including the moment the request is created.
// after your backend fetched { kycUrl } over S2S:
const popup = window.open(kycUrl, 'kyc', 'width=480,height=720');

The minted URL is valid for 30 minutes and single-use — mint a fresh one each time the player clicks “verify”.

Status lifecycle

PENDING ─▶ SUBMITTED ─▶ CONFIRMED
│ │
└───────────┴──────▶ REJECTED
StatusMeaning
PENDINGA verification request is open; the player hasn’t finished uploading.
SUBMITTEDThe player finished uploading; the submission awaits review.
CONFIRMEDReviewed and approved. Terminal.
REJECTEDReviewed and refused. Terminal.
  • A decision (CONFIRMED / REJECTED) can arrive from PENDING or SUBMITTED — the reviewer may decide before the player finishes uploading.
  • Terminal statuses are final for that request. A redo creates a new request with a new requestId: subsequent webhooks carry the new id, starting with its own PENDING event. Track per-player state as the latest request wins.
  • A confirmation is not forever. A previously-confirmed player can be sent back through verification — because you asked for it, because the platform’s compliance process re-opened it, or because the confirmation aged past the operator’s configured validity window. All three look identical from your side: a new request opens and its PENDING webhook arrives, outranking the old confirmation under latest request wins. Treat the player as unverified until the new request is confirmed; when they should act, mint a session as usual — the open request is reused.

Rejection reasons

A rejection always carries machine-readable reason codes on the webhook (payload.reasons), plus a single retryable flag classifying the whole refusal:

  • Retryable — something went wrong with the submission, not the player:

    CodeMeaning
    blurry_captureThe photos were too blurry to read
    glare_or_obstructedGlare, a shadow, or fingers covered part of the document
    cropped_documentDocument edges cut off
    missing_pageA side or page is missing
    doc_expiredThe document has expired
    unsupported_documentDocument type not accepted
    selfie_unusableThe selfie video is unusable
    details_do_not_match_documentEntered details differ from the document
    resubmit_otherOther — the player should try again
  • Final — the player cannot be verified, and trying again won’t change that: identity_mismatch, fraudulent_document, underage, duplicate_identity, prohibited_jurisdiction, ineligible_other.

One final reason makes the whole rejection final — that collapse is done platform-side, so take retryable as sent rather than re-classifying the codes yourself. Use the flag to pick your own UX: a retry CTA with guidance for a retryable refusal, a “contact support” dead end for a final one. Never surface a final rejection’s cause to the player — explaining why someone failed identity verification is a how-to-pass guide for whoever is actually on the other end, which is why the platform’s own rejection email does the same. New codes may be added over time; treat an unknown code as having no special copy and let the retryable flag drive the behaviour.

Enabling KYC

Everything lives in Integration → Settings, under the KYC section:

  • KYC verification — the master switch. Off by default; while off, the S2S session mint refuses with KYC_DISABLED and players cannot start a verification.
  • Success email — whether the player is emailed when their verification is confirmed. On by default.
  • Minimum age — 18 by default. A player whose date of birth is under it is rejected at once with the final underage reason, and cannot verify again unless an operator allows it.
  • Rejection email — whether the player is emailed when their verification is rejected. On by default. For a retryable rejection the email lists what to fix (per-reason guidance) and embeds a fresh restart link, so the player can re-verify without a round-trip through your site; the new request row is only created if they actually click it. For a final rejection the email contains no restart link and names no cause — see rejection reasons.

Emails go to the address you pass when minting the session — keep it current with POST /api/s2s/kyc/email if the player changes it mid-verification.

The completion signal

When the player reaches the success page, the popup posts a message to the window that opened it (window.opener), targeted at the returnOrigin you passed when minting — never broadcast:

window.addEventListener('message', (e) => {
if (e.data?.source !== 'lootbox-solutions') return;
if (e.data.type === 'kyc_flow_finished') refreshKycStatus();
});

Treat it as a UX hint, not as truth:

  • Also refresh on window focus — the player may close the popup before the success page, and a popup can’t signal you after it’s gone.
  • The webhook is the authoritative sync. kyc_flow_finished means “the player finished the wizard”, not “approved” — the decision arrives later via kyc_status_changed.

If you omit returnOrigin when minting, the popup stays silent and you rely on the webhook (and focus-refresh) alone.

Data handling

Identity documents are collected and stored privately by the platform — they never leave it, and neither does the contact email you pass at minting.

What you do receive, on a confirmation, is the verified identity: the legal name, date of birth, tax/national id and address the platform’s review checked against the documents, delivered once in the kyc_status_changed webhook’s identity block. That record is yours to persist — you are the operator of record for your player, and the point of the service is that you end up holding an identity someone has actually verified. Treat it accordingly: store it encrypted, show it only to staff who need it, and remember your side is now the system of record for it.

Everything else stays outcome-shaped: statuses, timestamps, verdicts and reason codes. Rejected and open requests never carry identity data.