A complete, self-contained brief for implementing a ClashX partner integration. Follow it in order. Every request and response below is real; reproduce them rather than inferring shapes.
There are two documents, and this is the buildable one. It is ordered the way you will write the code. The Partner API reference is the complete surface — every endpoint, every field, every failure — and is where to look when you need a detail this brief does not carry. Neither supersedes the other.
Read before writing any code
Four behaviours where the obvious assumption is wrong. Each will pass a naive test and fail in production. They are listed first because you will write the code they affect within the next twenty minutes.
A rejected wallet transfer returns HTTP 200 — not 4xx. Insufficient balance is a business outcome, not an error. Branch on the kind field, never on the status code alone.
applied: false means replay, not failure. Wallet transfers are idempotent on your transfer_ref. A retry returns success describing the original movement. Both true and false are successes. Treating false as failure and retrying under a new reference is how you double-count.
Errors carry no machine-readable code. Every failure is {"error": ""}. Branch on HTTP status and on kind. The message wording is not a stable contract — log it, do not parse it.
Launch tokens live 60 seconds and every refusal is identical. Single-use, 60-second TTL. Mint at the moment of redirect, never in advance. Unknown, expired, and already-used return the same message by design, so you cannot diagnose from the response — log your own mint and exchange timestamps.
What you cannot build without ClashX
These must be provisioned by ClashX before any of the steps below will work. If you do not have them, stop and request them. Do not stub, mock, or work around a missing prerequisite — an integration built against a fabricated substitute will not work against the real platform.
Prerequisite
Why you cannot proceed without it
A provisioned casino (tenant)
Nothing exists to hold players, wallets, or matches.
An API key with the scopes you need
A key with no scopes can only call /ping. Request sessions:write, wallet:write, matches:read, and detection:read if you need flags.
Module entitlements
Games are gated on entitlement. Without them your lobby is empty and no match can form.
A resolvable hostname pointed at ClashX
The launch exchange and all player endpoints identify your casino by Host. ⚠ This is the clause that tells you ClashX hosts the player experience — your casino host resolves to ClashX infrastructure, and the lobby, the games and the exchange page are all served from it under your branding. See "what you may build yourself" in the reference.
A registered callback endpoint and its signing secret
The secret is shown once at registration. Without it you receive no deliveries at all.
Your base URL and casino hostname
Ask for both. Do not guess either — see the note below.
Which hostname to call — two different rules
The two families of endpoint identify your casino in different ways, and getting this wrong is a confusing early failure.
/partner/v1/* — identified by your API key. These routes do no hostname resolution at all; the key is the claim. Call them on whatever base URL ClashX gives you.
/api/v1/* — identified by the Host header. The launch exchange and every player-facing route resolve your casino from the hostname. They must be called on your casino's hostname, and a request with the wrong Host reaches a different casino or none at all.
Ownership boundary — do not invent one
ClashX owns: the wallet balance and ledger, settlement, game servers and matchmaking, match records, collusion detection.
You own: player accounts and authentication, card processing and real deposits/withdrawals, the decision to move value in or out, acting on detection flags, receiving and verifying callbacks.
Value moves by explicit transfer. ClashX will never call your system to ask for a balance mid-wager — a seamless wallet is deliberately not part of this product. Do not build a balance-callback endpoint; nothing will call it.
Authenticate
Send your key in X-Api-Key on every partner request. Keys are prefixed wlk_.
status is this call's outcome. tenant_status is the casino's, and they are independent — a casino that is not yet live answers "status": "ok" on every endpoint in this document.
onboarding is a working state, not a broken one. You can build and certify your entire integration in it: mint launch tokens, move real balances, play real matches, receive signed callbacks. Partner-launched players reach games in it, because in this model the players are yours and the game is what ClashX supplies.
What onboarding does not open is the ClashX-hosted player site — a separate surface, for casinos that let ClashX serve their players directly. If you are integrating this API you are not using it, and serves_hosted_players: false is the expected reading.
⚠ suspended and churned refuse both surfaces. Suspension is the platform's stop lever and it stops the integration too; treat a 403 naming either state as terminal for the key and escalate commercially rather than retrying.
Implement a startup check that calls /ping and asserts both the scopes you need and the tenant_status you expect. /ping requires no scope, so it succeeds even for a key that can do nothing else — which makes it the only reliable way to distinguish "bad key" from "key lacks permission" before you are deep in a money path.
Failure modes
Status
Meaning
Retry?
401
Key missing, unknown, or revoked
No — fix the key
403
Authenticated but not permitted — missing scope, or the casino is suspended/churned. Notonboarding, which is permitted
No — request access
Resolve a player
Before a player can hold value or play, they must exist on the ClashX side. One call does get-or-create and mints a launch token at the same time.
The launch_token is 64 lowercase hex characters with no prefix — do not expect or validate one. The pst_ session token returned later does carry its prefix; the two are different shapes.
external_ref is the identity key: the same value always returns the same player. Use your immutable primary key, never an email address, username, or anything a user can change. A changed external_ref silently creates a second player with a second wallet, and the balance does not follow.
Store the returned player_id against your user record. You will not need it for wallet calls — those take external_ref — but it appears in match results and makes reconciliation unambiguous.
display_name and email are write-once
They are read only when the player is created. Sending changed values on a later call has no effect and returns no error, and there is no endpoint to update them. Do not build a profile-sync feature; it will appear to work and do nothing.
POST /api/v1/queue — the request body, and who you match with
The body is {module_key, game_key, currency, entry_fee_cents}. Ask ClashX which module and game keys your account is entitled to; they are not derivable and are not listed here because they differ per account.
It answers three kinds, all at 200: matched, queued, and insufficient_balance (which carries currency, balance_cents, required_cents, shortfall_cents and a player-facing message). Write the switch for three.
⚠ A player matches only with others whose module_key, game_key, currency AND entry_fee_cents are all identical. The fee is matched on exact equality — there is no banding by stake, unlike rating, which does widen.
So if you let players name their own buy-in, or compute a per-player fee, each one queues alone. The symptom is not an error: waiting: 1 reads as thin liquidity, so it looks like an empty platform rather than a mismatch you created. Publish fixed stake tiers.
If fixed tiers do not suit your product, raise it — whether stake should band the way rating does is a platform decision, not something you can work around at your end.
Four refusals the tables do not give you
A stake ceiling applies to new accounts. A buy-in above $10.00 (1000 cents) is refused 422 until that player has played 10 paid lives of that game. Neither ceiling nor rule appears anywhere else, and the refusal copy is player-facing, so it reaches your end user. Make sure your stake tiers give a new player a legal entry point — a high-value player launched straight onto a $50 table is refused on day one.
A 403 whose message begins compliance_block: is about the PLAYER, not your key. KYC, sanctions, self-exclusion or the collusion gate has refused. The 403 row in the error table is about your KEY or the casino's STATE, which routes a regulated refusal to your integrations team as a credential problem. Route compliance_block: to compliance and do not retry. This is the one message worth matching on, notwithstanding the general rule against matching messages.
rank and score are nullable. A match that produced no ranking — abandoned, fully refunded, failed to start — has null for both on every participant. rank === 1 is not a safe test for "winner": on a refunded match nobody is rank 1. Sort with nulls handled explicitly.
A refused launch exchange returns 401, with one identical message for unknown, expired and already-spent tokens — deliberately, so a probe cannot tell them apart.
display_name is write-once, and omitting it brands the account permanently
display_name and email are read ONLY when the account is created, on the first POST /partner/v1/sessions for a given external_ref. There is no update endpoint.
If you omit them, ClashX synthesises values — an invented in-game name and a deliberately unresolvable address — and they are permanent. Wiring up the integration without display_name therefore brands every player created in that window with a name nobody chose, and if those are real players the casino cannot fix it afterwards. Send the real name on the very first call for each external_ref.
Credit the wallet
You take the real payment in your own system. Then move the corresponding value into the player's ClashX wallet.
Player must already exist. Unknown → 404, not auto-created.
transfer_ref
Required, ≤ 200 chars. Your ledger entry ID and the idempotency key. Unique across your ENTIRE casino — not per player, per currency, per day, or per direction. One reference names one movement, so a charge and its correction need different references — a ledger row id reused for both is refused 409.
currency
"sweeps" only, both directions. Real money moves in sweeps; gold is play money — a gold credit could never be debited back out — and is refused 422. Anything else → 422.
amount_cents
Positive integer. Zero or negative → 422.
Idempotency only protects you if a retry sends the sametransfer_ref. Generate it once, persist it with your ledger entry, and reuse it on every retry of that entry. A freshly generated ID per attempt defeats the mechanism entirely and will move money twice.
Handle both non-obvious responses
HTTP 200 with "kind": "insufficient_balance" — the transfer did not happen. Only possible on debit, but handle the shape on both paths.
"applied": false — this transfer_ref was already processed; the response describes the earlier movement and nothing moved now. This is a success. Reconcile against it; do not retry.
⚠ transfer_ref must be unique across your ENTIRE casino
The idempotency key is the transfer_refalone — there is no player, currency or amount in it. Its scope is the whole casino, not the player.
That matters because the obvious ways to mint one collide: a per-player sequence, a per-day sequence, a deposit id from a table that resets, or the same reference for the gold and sweeps legs of one deposit. Every example in this document (dep-2026-09-01-000912) reads like a per-ledger sequence, which is exactly the shape that collides.
A reference reused for a different movement — different player, currency, amount or direction — is refused 409. It is terminal: retrying cannot clear it, and only a new reference will work. A genuine retry of the same movement is unaffected and still returns applied: false with the original transaction.
⚠ A if (!response.ok) retry() loop spins forever on this. 409 is the one refusal on this endpoint that a generic retry rule cannot survive, because it is permanent and identical on every attempt. Branch on it explicitly.
(Before this was refused, a collision returned 200 / applied: false carrying the FIRST movement's transaction while echoing the second player's id — a real deposit was silently never credited and both sides' books still balanced. If you integrated against that behaviour, the 409 is the fix, not a regression.)
On HTTP 500, retry with the same transfer_ref
A 500 leaves you unsure whether the transfer applied. Retrying with the same reference resolves it safely: you will either apply it once or receive applied: false telling you it was already done. Never retry a wallet call under a new reference.
Launch the player into a game
The launch is a top-level navigation. Nothing here can be iframed.
This is an architecture decision, so it belongs before you design your lobby rather than after.
The launch itself is always a top-level navigation. Every ClashX app page — the exchange, the lobby — is served Content-Security-Policy: frame-ancestors 'none' and X-Frame-Options: DENY, so it can never be framed. Send the player to launch_url as a normal link, a redirect, or a new tab.
Game bundles under /games/* are a separate case and can be framed by arrangement. They carry frame-ancestors 'self' plus any origins ClashX has configured, and no X-Frame-Options. The configured list is empty in production by default, so your domain is not on it and the browser will refuse your frame with nothing but a console message inside a frame that never rendered.
If you want the game inside your own page, ask ClashX to add your origin before you build against it — it is applied at build time and is currently global rather than per-casino, so it is a deployment conversation rather than a setting. The reference's §15 is the client half once your origin is registered.
Two hops: mint a token, redirect the browser to it.
✅ ClashX HOSTS THE EXCHANGE. YOU BUILD NO PAGE.
Mint a token, redirect the player to it, and stop. ClashX serves the exchange page under your branding: no exchange page, no session handling, no error page on your side.
This document previously told you to build one, because the page was written but not deployed. It is deployed now. If you built a thin exchange page against the earlier instruction, you can delete it — the redirect URL and the token contract are unchanged, which is why that instruction said you would be able to.
Easier still: the session response carries launch_url, the complete redirect assembled for you. Send the player there and concatenate nothing.
ClashX hosts the exchange page. You build no exchange page, no session handling and no error page — send the player to launch_url and stop.
(This paragraph previously read "when it ships". It has shipped: GET /launch?token=… on the casino host answers today. If you already built an exchange against the earlier instruction, deleting it is safe — the redirect URL and the token contract are unchanged.)
https://{casino-host}/launch?token={launch_token}
external_ref is idempotent — do not build deduplication
Launching the same external_ref twice reaches the same account. Identity is stable, the credential is disposable. An integrator who does not know this builds a dedupe layer they do not need, and one that can only introduce bugs of its own.
4a — Mint at the moment of the click
Call POST /partner/v1/sessions again (step 02) to get a fresh launch_token. It expires in 60 seconds. Do not mint it when rendering a page the user may read for a minute first — mint it in the handler for the "Play" click.
This endpoint is unauthenticated — the caller is the player's browser, which holds nothing but the token. Your casino is identified by the Host header; the player by the token. The claim is atomic, so a token cannot be exchanged twice even under concurrent requests.
After four hours the pst_ session simply stops working. There is no refresh endpoint. Detect expiry and re-launch — mint a new token and repeat 4a–4c. Build this in now; discovering it as a four-hour production outage is the common failure.
Receive settlement callbacks
ClashX pushes settled matches to an HTTPS endpoint you register. One event type exists: match.settled.
Concatenate t, a literal ., and the raw body bytes — captured before any JSON parsing. Re-serialising the parsed object will produce a different byte string and the signature will not match.
HMAC-SHA256 with your secret, compare hex in constant time.
Reject if t is outside your freshness window.
ClashX does not mandate a tolerance, so choose one: 300 seconds. This is safe despite retries because the timestamp and signature are regenerated on every delivery attempt, not fixed when the match settled. A retry two hours later still arrives with a current t. Do not widen the window to accommodate retries — you would only be accepting older replays.
Any 2xx counts as delivered; anything else is a failure. The request times out at 10 seconds. Persist the raw payload, return 200, and process from your own queue. Doing the work inline risks a timeout that marks a delivered event as failed.
Retry policy
Behaviour
Value
Timeout
10 seconds
Max attempts
8
Backoff
30s, 60s, 120s, 240s, 480s, 960s, 1920s, then 3600s
Batch size
up to 50 matches per delivery
After 8 failures
Dead-lettered — delivery stops permanently
Dead-lettering is a full stop
A stream that exhausts its retries stops — it does not keep retrying quietly in the background. An operator must clear it before deliveries resume. This is why step 06 is not optional.
Silent endpoint? You are missing a signing secret
A registered endpoint with no stored signing secret receives nothing — deliveries are withheld rather than sent unsigned or signed with a fallback key. If you registered an endpoint and see no traffic, ask ClashX to re-save it to provision a secret.
Reconcile by polling
Poll the match feed on a schedule in addition to receiving callbacks. Push and pull run off the same underlying query and cannot disagree, so polling is the backstop that makes a dead-lettered stream survivable.
Store next_since and pass it as your next since — except when it is null, which is what an empty page returns. Keep your existing cursor then; storing the null drops since from the next request and replays the feed from the beginning, re-ingesting every settlement you have already seen. Never derive the cursor from your own clock — a match that started before your cursor and settled after it would be skipped forever, and nothing would report the gap. The cursor is settlement time for exactly this reason.
When truncated: true, more rows were waiting than your limit — fetch again immediately rather than waiting for the next tick. limit defaults to 50, maximum 200.
Two timestamps, two different questions
settled_at is when the platform recorded the settlement. It is this feed's cursor and its ordering key, which is why it cannot mean anything else — an ordering key has to be the key rows become visible on, or paging skips rows that appear behind your cursor.
engine_ended_at is when the game actually finished, reported by the game server.
On a healthy match they are milliseconds apart. They diverge on exactly the matches a casino investigates — one ended by something other than being played out. A session reaped by the platform's stuck-sweep once recorded a settlement 2m38s before the match really ended, so a timeline built from settled_at alone put the match in the wrong place with nothing saying so. Read them together with settlement_reason; that is the same population.
engine_ended_at is null when the game server never reported a terminal status for that session — an honest gap rather than a fabricated instant. Do not substitute settled_at for it; that is the substitution this field exists to stop.
A 500 is not the only permanent failure, and 422/404 here are terminal
A wallet call refuses with 422 when the player's wallet is not active (frozen, closed), and 404 when there is no wallet for that player at this casino — a money call never creates an account. Both are permanent: retrying cannot clear either.
These used to be 500s, which our own error table tells you to retry with backoff. A partner following that instruction retried a frozen wallet forever, the queue never drained, and a genuine outage was masked by the same signal. If you built that retry loop, it is now safe: these arrive as 4xx and should be escalated, not retried.
Free practice matches are in this feed
There is no money filter on it. Match-based practice settles through the same path as paid play, so free games against bots appear here with fiat: false and entry_fee_cents: 0 — and because their stakes are zero, the money identity still closes and nothing flags them. Only your counts are wrong.
Filter on fiat unless you mean to count practice. Note also that participants excludes bots, so a practice match typically shows one participant and reads like a one-player match.
Reading the results
participants is nested inside its match, so you never join two arrays. external_ref is null for players who did not come through your integration. An empty page means "nothing settled since your cursor", not "nothing happened" — matches in progress are absent until they settle.
Debit the wallet
Identical shape to credit. Moves value out, back to your system for a real withdrawal — in sweeps only. A gold debit is refused 422: gold is play money and does not leave the casino.
A player who does not hold the amount produces HTTP 200 with "kind": "insufficient_balance". A player cannot be overdrawn into debt. Code that reads only the status code will mark a withdrawal complete and pay out money that was never debited.
Order matters. Debit ClashX first; only on "kind": "settled" with applied: true (or a confirmed applied: false for a known retry) should you release funds on your side. Never pay out first and reconcile later.
Handle 429
Counted per API key, per sixty seconds. Defaults:
Class
Endpoints
Default
Writes
/sessions, /wallet/credit, /wallet/debit
6,000 / min
Reads
/matches, /detection/*
600 / min
Ping
/ping
60 / min
⚠ Built and verified, but not released yet
The numbers are final; the date is not set here. Respect them now and nothing changes when they take effect — but do not expect a 429 yet, and do not write a test that requires one.
Poll no faster than once per second. That is 60/min, ten times under the read class. 600/min is exactly 10 requests per second, so a 100 ms loop sits on the limit and anything tighter is refused.
On a 429, retry a wallet call with the SAME transfer_ref — exactly as for a 500. A refused request moved no money, and idempotency makes the retry safe either way. Never retry under a new reference.
You will not reach these with real traffic — 500 concurrent players is about 51 req/min, and 50,000 concurrent is about 5,000 req/min. They catch a retry loop with no backoff. Above roughly 5,000 concurrent your numbers are set when you onboard, per key and per class; the defaults are for a new integration, not a cap on growth.
Use a separate key per integration. The window is per key, so a runaway reporting job cannot exhaust the allowance your players are served through.
{ "error": "rate limit exceeded for this API key: more than 600 requests per minute. …" }
⚠ Branch on the 429 status code, never on the message — there is no machine-readable code (same rule as every other error here), and the number in the sentence is your provisioned limit rather than a constant.
Verify your integration actually works
Each item proves the step happened, rather than that a call returned 200. Work through them in order.
Authentication
/ping returns your expected tenant slug and every scope you need.
A deliberately wrong key returns 401, not a 500 or a hang.
Player resolution
Calling /sessions twice with the same external_ref returns the same player_id. If it differs, your external_ref is not stable — fix it before going further.
Two different external_ref values return two different player_id values.
Wallet
After a credit, balance_after_cents equals your expected running total — not just a 200.
Replaying the exact same transfer_ref returns applied: false and the same transaction_id, and the balance does not change.
Debiting more than the balance returns HTTP 200 with kind: "insufficient_balance", and your code takes the rejection branch. Assert the branch was taken, not just that no exception was thrown.
A debit for an unknown external_ref returns 404 and does not create a player — verify by calling /sessions afterwards and confirming a new player_id is minted at that point.
Launch
A launch token exchanges successfully and returns a pst_ session.
Exchanging the same token twice fails the second time — proves single-use.
A token held for 61+ seconds fails — proves expiry, and confirms you are reading the TTL correctly.
The pst_ session authenticates GET /api/v1/me — called on your casino hostname with Authorization: Bearer pst_… — and the player it returns matches the player_id from step 02.
Callbacks
A delivery arrives with both x-clashx-event and x-clashx-signature headers.
Your verifier accepts a genuine delivery.
Your verifier rejects a payload whose body you have altered by one byte. If it accepts, you are verifying re-serialised JSON rather than the raw body.
Your verifier rejects a delivery whose t you have moved outside your window.
Your endpoint returns 2xx in well under 10 seconds under realistic load.
Reconciliation
A match played end to end appears in GET /matches with your external_ref on the right participant.
stake_cents, payout_cents, and refund_cents reconcile against the wallet movements you observed. All three are independent — a refund is not a payout, and settlement accumulates them separately.
Polling with the returned next_since does not return the same match twice.
The same match is seen by both the callback and the poll, and your system stores it once.
The one end-to-end test worth automating
Credit a player, launch them, play a match to settlement, receive the callback, poll the feed, debit the winnings. Assert the wallet arithmetic closes: credited − staked + won + refunded − debited equals the final balance — once per currency, since a wallet holds two independent balances (gold and sweeps) and balance_after_cents is only the one your request named. refunded is the term integrations miss — refund_cents is a separate field from payout_cents, not a component of it, and it is non-zero whenever a match is abandoned. Omit it and every abandoned match reports a false discrepancy. Every trap in this document shows up as a discrepancy in that sum, which is why it is worth more than any individual endpoint test.
Getting a seated player back into their match
⚠ Re-POST /queue with the identical body. This is the reconnect path and it is the single most useful thing in this document.
When a roster fills, only the request that COMPLETED it receives ws coordinates in its response. Everyone else is seated, staked, and holding a queued reply. They get their coordinates by polling the same route again:
The same call recovers a player whose browser refreshed, crashed, or lost the connection mid-match. No money moves — the seat was bought at match formation, and re-polling does not stake again.
Why this is easy to get wrong
A queued response reads exactly like "you are not in a match". It is not: it means "no seat yet". There is no separate reconnect endpoint to discover, and nothing about queued suggests polling is how you progress — so an integration that treats it as terminal concludes its players are stranded with money staked. That has happened, to an experienced integrator, on this API.
⚠ The body must match. The lookup is keyed on module_key + game_key + player. A re-poll with a different or missing module_key misses the seat and falls through to matchmaking, which reads as a brand-new queued — the same symptom, with the seat sitting there the whole time.
⚠ Each seat's ws.token is its own. The JWT carries user_id. Reusing one seat's token for another fails; if you are driving several players, assert user_id matches the seat before connecting.
Two timers worth designing around
The launch token in /partner/v1/sessions lives ~60 seconds and is single-use. Mint it at redirect time, not at page-render time — no email link, no confirmation step in between.
A match that forms and that nobody joins is eventually swept and refunded. The window is generous by default, but it is why the reconnect poll should start immediately rather than after a delay.
Reading rake: it is on the MATCH, not the seats
rake_cents on a match object is the real figure. Every participant's rake_cents is 0 for a pot game, and that is correct rather than missing: the rake is booked against the house, not against any seat, so no player bears a share of it.
Read it from the match. Summing it across participants gives zero.
⚠ Rake never touches a player's wallet, so it does not belong in a balance reconciliation. It comes out of the pot before payout. In the worked example below, the winner staked 500 and received 2775; the 225 was taken from the pot, not from anyone's balance.
6 seats x 500 = 3000 pot
payout 2775 -> the winner
rake 225 -> the house (750 bps)
3000 - 2775 - 0 - 225 = 0
Revenue reconciliation and balance reconciliation are different books that happen to arrive in the same feed row. A player-balance identity that includes rake will not close.
When real-money play is refused: compliance_block
Everything above can be built, tested and working, and real-money play can still refuse. This section exists because that refusal was previously documented nowhere — an integrating casino reached "fiat does not work" with no route to a diagnosis.
What you will see
POST /api/v1/queue — the first money-moving call — returns 403:
{ "error": "compliance_block: kyc" }
The word after the colon names the check that refused. Practice play, session creation, the launch exchange and wallet credit all keep working, so the failure looks specific to real-money matchmaking. It is.
What it means
Your casino has a compliance gate, and one of its checks could not be satisfied. The checks you may see named:
value
what it means
who can fix it
kyc
the player is not identity-verified, or your casino has no KYC rail configured
ClashX + your KYC vendor
age
no verified date of birth is on file for this player
your KYC vendor, via ClashX
sanctions
sanctions screening could not be performed — an unscreened player is never cleared
ClashX + your screening vendor
geo
the player's jurisdiction could not be established or is not permitted
ClashX + your geo vendor
self_exclusion, account_ban, cash_ineligible
this specific player is barred
you, in your portal
⚠ The first four are not bugs in your integration and you cannot fix them from the partner API. They mean a compliance rail your casino depends on is not configured or not operational. There is no request field, header or parameter that satisfies them.
⚠ Date of birth cannot be supplied by you
POST /partner/v1/sessions currently accepts and silently ignoresdate_of_birth, dob and age in the request body. It returns 200 and the player record is unchanged.
Do not rely on those fields. A date of birth is only ever accepted from your KYC vendor, attested and encrypted at rest — supplying one yourself would be an unverified claim about a person's age, which is the thing the check exists to prevent. That the fields are accepted at all is a defect on our side, tracked; treat them as absent.
What to ask ClashX for
Quote the exact compliance_block: value you received, and ask:
Which rail is behind this check, and is it configured and operational for my casino? (kyc and sanctions in particular are per-casino rails that must be granted before any real-money play is possible.)
If it is configured, is the vendor actually reachable? A rail that exists but cannot answer refuses exactly like one that is missing — for sanctions, deliberately so.
Is this check blocked platform-side? Geo screening in particular may be unavailable for reasons that have nothing to do with your configuration.
You can see which rails your casino has under Payments & Identity → Credentials in your portal. A rail listed as "not set up" will refuse every gated action for every player until it is configured.
Deliberately out of scope here
This brief covers the money-and-play path end to end. Two capabilities exist on the API and are not covered above — build them only once the path above works.
Detection — GET /partner/v1/detection/flags and /detection/accounts, scope detection:read. Collusion review and enforcement are yours in this model. Both responses carry a coverage object beside the list. scanned: false means no scan has run — an empty list then means "nothing was examined", not "nothing was found". scanned: true means a scan ran, not that every detector ran: some decline at small scale, and the boolean cannot tell that apart from a clean result. coverage.summary names any that declined. Never render an empty list as "all clear" without reading it.
The hosted white-label model — where ClashX runs the entire player experience on your domain and you write no code at all. If that is what you were sold, this brief is the wrong document.