KYC flow
KYC has four ownership models — pick once per tenant; the model dictates which sides the contract supports.
Model: platform_owns
Most common for casinos. The platform handles all of KYC; CRM mirrors state via webhooks.
You implement: webhook senders for the lifecycle below.
You DON'T implement: outbound KYC endpoints (CRM never calls you for KYC).
Webhook events
POST /v1/adapters/<slug>/webhooks/kyc-events
// kyc.submitted
{
"event": "kyc.submitted",
"event_id": "evt-uuid",
"ts": "2026-05-08T...",
"player_external_id": "casino_player_001",
"level": 1,
"data": { "documents": ["passport", "selfie"] }
}
// kyc.approved
{
"event": "kyc.approved",
"level": 2,
"data": { "expires_at": "2027-05-08T..." } // optional TTL
}
// kyc.rejected
{
"event": "kyc.rejected",
"level": 1,
"data": { "reason": "document_expired" }
}
// kyc.expired
{ "event": "kyc.expired", "level": 2, "data": {} }
// kyc.level_upgraded — player started higher-level submission
{ "event": "kyc.level_upgraded", "level": 3, "data": {} }
State transitions enforced server-side: none → pending → {verified, rejected}, verified → expired, etc.
Model: crm_owns
Less common — typical for operators with custom KYC workflows. CRM admin initiates; platform listens for verdicts.
You implement: outbound KYC endpoints.
You DON'T implement: webhook senders. UCRM rejects inbound kyc.* webhooks with 403 for crm_owns tenants.
POST <your-base>/v1/kyc/initiate
{
"tenant_id": "...",
"player_external_id": "casino_player_001",
"level": 1,
"requirements": ["passport", "address_proof"]
}
Response: { ok: true, provider_ref?: "..." } or { ok: false, code, message }.
POST <your-base>/v1/kyc/update
CRM admin approves / rejects on the CRM side; UCRM informs your platform:
{
"tenant_id": "...",
"player_external_id": "casino_player_001",
"status": "verified", // or "rejected"
"level": 1,
"reason": "documents validated"
}
GET <your-base>/v1/kyc/:player_external_id
Reconciliation lookup:
{
"status": "verified",
"level": 2,
"last_updated": "..."
}
Model: dual
Both flows simultaneously — typical for MGA / UK where:
- Platform does ID verification (
level 1,level 2) - CRM does affordability checks (
level 3)
You implement BOTH the outbound endpoints AND the webhook senders. Each KYC level falls naturally to one side or the other; UCRM doesn't enforce which side handles which level.
Model: none
No KYC tracked at this tenant. UCRM rejects all KYC webhooks AND doesn't expose KYC admin actions. Useful for unregulated test envs only.
State storage
Regardless of model, CRM stores:
players.kyc_status -- none | pending | verified | rejected | expired
players.kyc_level -- 0..10
players.kyc_history -- append-only audit JSONB
players.kyc_last_event_at
players.kyc_expires_at
Admin UI renders the timeline directly off kyc_history.