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
- Your backend mints a verification URL with
POST /api/s2s/kyc/sessions, passing the player’s contact email and, optionally, areturnOriginfor the completion signal. - Your page opens the URL in a popup window —
window.open, not an iframe. The verification page refuses framing outright (it shipsframe-ancestors 'none'), so an iframe embed renders nothing. A plainwindow.open(kycUrl)popup, PSP-checkout style, is the supported shape. - 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.
- The platform team reviews the submission — checking the entered details against the documents — and confirms or rejects it.
- You track every step via the
kyc_status_changedwebhook — 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| Status | Meaning |
|---|---|
PENDING | A verification request is open; the player hasn’t finished uploading. |
SUBMITTED | The player finished uploading; the submission awaits review. |
CONFIRMED | Reviewed and approved. Terminal. |
REJECTED | Reviewed and refused. Terminal. |
- A decision (
CONFIRMED/REJECTED) can arrive fromPENDINGorSUBMITTED— 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 ownPENDINGevent. 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
PENDINGwebhook 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:
Code Meaning 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_DISABLEDand 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
underagereason, 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_finishedmeans “the player finished the wizard”, not “approved” — the decision arrives later viakyc_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.