Storemate KYC

API reference

Interactive OpenAPI: /api/v1/docs/ (json)  ·  engine SDK /api/v1/engine/docs/ (json).

Contents

1. Concepts

TermMeaning
ProjectA client application. Its own API credentials, branding, callback endpoint and verification engine.
external_user_idThe client's own user id (any string). Never parsed, only echoed. Unique per project.
Provider / enginesumsub or inhouse. Chosen per project, resolved at runtime on every request.
statusCoarse, provider-agnostic verification state — the field to branch on.

Status values

statusMeaningis_verifiedcan_start
not_startedNo attempt yetfalsetrue
pendingApplicant needs to submit / re-submitfalsetrue
in_reviewSubmitted, under reviewfalsefalse
approvedVerifiedtruefalse
retryRejected, applicant may try againfalsetrue
rejectedRejected, finalfalsetrue
on_holdManual review / on holdfalsetrue

2. Internal API

Consumed by a client backend (server-to-server). The client proxies its own user-facing /kyc/* routes to these.

Rate limits

Fixed-window, keyed by <project>:<external_user_id>. 429 {"detail": "Rate limit exceeded. Try again shortly."} when exceeded.

ScopeEndpointsDefault
tokenPOST /kyc/access-tokenKYC_TOKEN_RATE = 15/m
mutationPOST /kyc/refresh, POST /kyc/resetKYC_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": ""
}
FieldNotes
tokenHand to the SDK (Sumsub SDK, or the engine API as a Bearer token).
providersumsub → use the Sumsub Web/Mobile SDK. inhouse → call api_base (see §3).
api_baseOnly set when provider == "inhouse". Base URL for the engine SDK API.
ttl_secsToken lifetime. Re-call this endpoint for a fresh token.

Errors

CodeBodyWhen
409{"detail": "Identity verification is already approved."}user already approved
429rate 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

CodeBody
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.

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 200EngineStatusOut. 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 200EngineStatusOut. 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.

Conditionstatus
ID doc + selfie/liveness + first/last name + dob all present, ENGINE_AUTO_APPROVE=trueapproved
…all present, ENGINE_AUTO_APPROVE=falsein_review (human signs off in admin)
something missingretry with reject_labelsID_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)

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
CodeBodyMeaning
200{"detail": "accepted", "event_id": "…"}recorded (or duplicate)
400{"detail": "invalid signature"} / {"detail": "invalid body"}rejected, do not retry
500failed 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"}]}