API reference
Interactive OpenAPI: /api/v1/docs/ (json) · engine SDK /api/v1/engine/docs/ (json).
- Base URL
https://kyc.clistech.com - All bodies JSON (
Content-Type: application/json) - All timestamps ISO-8601 UTC
- 1. Concepts & status values
- 2. Internal API —
/api/v1/kyc/* - 3. In-house engine SDK API —
/api/v1/engine/* - 4. Provider webhook (Sumsub → service)
- 5. Status callback (service → client)
- 6. Error shape
1. Concepts
| Term | Meaning |
|---|---|
| Project | A client application. Its own API credentials, branding, callback endpoint and verification engine. |
external_user_id | The client's own user id (any string). Never parsed, only echoed. Unique per project. |
| Provider / engine | sumsub or inhouse. Chosen per project, resolved at runtime on every request. |
status | Coarse, provider-agnostic verification state — the field to branch on. |
Status values
status | Meaning | is_verified | can_start |
|---|---|---|---|
not_started | No attempt yet | false | true |
pending | Applicant needs to submit / re-submit | false | true |
in_review | Submitted, under review | false | false |
approved | Verified | true | false |
retry | Rejected, applicant may try again | false | true |
rejected | Rejected, final | false | true |
on_hold | Manual review / on hold | false | true |
2. Internal API
Consumed by a client backend (server-to-server). The client proxies its
own user-facing /kyc/* routes to these.
- Base path:
/api/v1 - Auth: two headers on every request —
X-Api-Key: pk_…andX-Api-Secret: sk_…. Unknown key, wrong secret, or a disabled project → 401{"detail": "Unauthorized"}.
Rate limits
Fixed-window, keyed by <project>:<external_user_id>.
429 {"detail": "Rate limit exceeded. Try again shortly."} when exceeded.
| Scope | Endpoints | Default |
|---|---|---|
token | POST /kyc/access-token | KYC_TOKEN_RATE = 15/m |
mutation | POST /kyc/refresh, POST /kyc/reset | KYC_MUTATION_RATE = 6/m |
get /api/v1/kyc/branding
Branding for the calling project — so the verification SDK / UI can theme itself.
Response 200
{
"project": "acme",
"name": "Acme App",
"logo_url": "https://acme.example/logo.png",
"primary_color": "#0B5FFF",
"support_email": "help@acme.example",
"provider": "sumsub"
}
logo_url is either the configured URL or, if a logo was uploaded in
the admin, a data: URI.
curl https://kyc.clistech.com/api/v1/kyc/branding \
-H "X-Api-Key: $KYC_API_KEY" -H "X-Api-Secret: $KYC_API_SECRET"
post /api/v1/kyc/access-token
Create/link the applicant (first call only) and mint a short-lived SDK token. The engine is chosen from the project.
Request
{
"external_user_id": "42",
"level_name": "id-and-liveness" // optional; defaults to the project level
}
Response 200
{
"token": "_act_sbx_a1b2c3…",
"user_id": "42",
"project": "acme",
"provider": "sumsub",
"level_name": "id-and-liveness",
"ttl_secs": 600,
"status": "pending",
"is_verified": false,
"api_base": ""
}
| Field | Notes |
|---|---|
token | Hand to the SDK (Sumsub SDK, or the engine API as a Bearer token). |
provider | sumsub → use the Sumsub Web/Mobile SDK. inhouse → call api_base (see §3). |
api_base | Only set when provider == "inhouse". Base URL for the engine SDK API. |
ttl_secs | Token lifetime. Re-call this endpoint for a fresh token. |
Errors
| Code | Body | When |
|---|---|---|
409 | {"detail": "Identity verification is already approved."} | user already approved |
429 | rate limit | > KYC_TOKEN_RATE |
502 / 503 | {"detail": "Verification provider unavailable."} | provider/engine error |
curl -X POST https://kyc.clistech.com/api/v1/kyc/access-token \
-H "X-Api-Key: $KYC_API_KEY" -H "X-Api-Secret: $KYC_API_SECRET" \
-H "Content-Type: application/json" \
-d '{"external_user_id": "42"}'
get /api/v1/kyc/status/{external_user_id}
Current verification snapshot. Bootstraps a not_started record if
none exists (never 404s for a valid user id).
Response 200
{
"external_user_id": "42",
"project": "acme",
"provider": "sumsub",
"status": "approved",
"is_verified": true,
"can_start": false,
"level_name": "id-and-liveness",
"review_status": "completed",
"review_answer": "GREEN",
"reject_type": "",
"reject_labels": [],
"moderation_comment": "",
"applicant_id": "6579…",
"verified_at": "2026-09-01T12:34:56+00:00",
"last_synced_at": "2026-09-01T12:34:57+00:00",
"updated_at": "2026-09-01T12:34:57+00:00"
}
post /api/v1/kyc/refresh
Force an authoritative re-pull from the engine (don't wait for a webhook). If the status changed, the status callback fires.
Request { "external_user_id": "42" } ·
Response 200 — same shape as GET /kyc/status/… ·
Rate: KYC_MUTATION_RATE.
post /api/v1/kyc/reset
Reset a failed / stuck attempt so the user can start over.
Request { "external_user_id": "42" }
Response 200
{ "detail": "Verification reset. The user can start again.", "status": "pending" }
Errors
| Code | Body |
|---|---|
404 | {"detail": "No verification record for this user."} |
409 | {"detail": "Identity verification is already approved."} |
400 | {"detail": "There is no verification attempt to reset."} |
Rate: KYC_MUTATION_RATE.
3. In-house engine SDK API
Only relevant when a project's provider == "inhouse". The
verification frontend calls these directly.
- Base path:
/api/v1/engine(value ofapi_basein the access-token response) - Auth:
Authorization: Bearer <token>— thetokenfromPOST /api/v1/kyc/access-token. Expired / invalid → 401. - Token TTL:
ENGINE_ACCESS_TOKEN_TTL(default1800s).
Document types
passport, id_card, drivers_license,
residence_permit, selfie, liveness
EngineStatusOut (returned by every endpoint)
{
"applicant_id": "0b4e…",
"external_user_id": "42",
"status": "pending",
"review_answer": "",
"reject_type": "",
"reject_labels": [],
"moderation_comment": "",
"documents": ["passport", "selfie"],
"submitted_at": null
}
get /api/v1/engine/applicant
Current applicant state (EngineStatusOut).
post /api/v1/engine/applicant/documents
Attach one document. Repeat per document.
{
"type": "passport",
"images": ["data:image/jpeg;base64,...", "..."],
"country": "IN",
"number": "A1234567"
}
images entries are opaque to the service (data URIs or client refs).
Response 200 — EngineStatusOut.
409 {"detail": "This verification can no longer be edited."}
once approved / in_review.
post /api/v1/engine/applicant/data
Set / merge the declared identity data.
{
"first_name": "Asha",
"last_name": "Rao",
"dob": "1990-04-15",
"country": "IN",
"address": "…"
}
Response 200 — EngineStatusOut. 409 as above.
post /api/v1/engine/applicant/submit
Finalise. Runs the review rules, sets the final status, and pushes
the result onto the KycProfile (which fires the status callback).
No body.
| Condition | status |
|---|---|
ID doc + selfie/liveness + first/last name + dob all present, ENGINE_AUTO_APPROVE=true | approved |
…all present, ENGINE_AUTO_APPROVE=false | in_review (human signs off in admin) |
| something missing | retry with reject_labels ⊆ ID_DOCUMENT_MISSING, SELFIE_MISSING, IDENTITY_DATA_INCOMPLETE |
409 {"detail": "Already submitted."} if already approved / in_review.
BASE=https://kyc.clistech.com/api/v1/engine
H="-H Authorization:Bearer $TOKEN -H Content-Type:application/json"
curl -X POST $BASE/applicant/documents $H -d '{"type":"passport","images":["..."]}'
curl -X POST $BASE/applicant/documents $H -d '{"type":"selfie","images":["..."]}'
curl -X POST $BASE/applicant/data $H -d '{"first_name":"Asha","last_name":"Rao","dob":"1990-04-15"}'
curl -X POST $BASE/applicant/submit $H
4. Provider webhook (Sumsub → this service)
POST /api/v1/kyc/webhook/— default provider (KYC_PROVIDER)POST /api/v1/kyc/webhook/sumsub/— explicit
Unauthenticated; HMAC-verified. Set the URL in the Sumsub dashboard.
x-payload-digest: <hex hmac(SUMSUB_WEBHOOK_SECRET, raw_body)>
x-payload-digest-alg: HMAC_SHA256_HEX # or HMAC_SHA1_HEX / HMAC_SHA512_HEX
| Code | Body | Meaning |
|---|---|---|
200 | {"detail": "accepted", "event_id": "…"} | recorded (or duplicate) |
400 | {"detail": "invalid signature"} / {"detail": "invalid body"} | rejected, do not retry |
500 | – | failed before recording; provider retries |
The body is never trusted for the decision — a background task re-pulls the authoritative status from Sumsub. Events are deduped on the payload digest. The in-house engine needs no webhook.
5. Status callback (this service → client backend)
On every coarse status change, this service POSTs the
status snapshot to the project's callback_url (fallback:
KYC_CALLBACK_URL).
Content-Type: application/json
X-Kyc-Event: status.changed
X-Kyc-Signature: <hex hmac_sha256(project.callback_secret, raw_body)>
Body — the same object as GET /api/v1/kyc/status/….
The client must: (1) verify X-Kyc-Signature constant-time against
its callback_secret, (2) update its denormalized
user.kyc_status / kyc_verified_at, (3) return
2xx — anything else is retried (durable
CallbackDelivery + retrying task + retry_failed_callbacks
beat sweep).
Test it from the admin: open the project → “Send test callback”.
6. Error shape
All errors are {"detail": "<message>"} (django-ninja default).
Validation errors on a request body are 422:
{"detail": [{"type": "missing", "loc": ["body", "payload", "external_user_id"], "msg": "Field required"}]}