KYC / Identity Verification — Integration Guide
Identity verification for Storemate runs on a standalone microservice
(kyc-service, hosted at https://kyc.clistech.com) and, through it,
Sumsub (https://sumsub.com) as the verification provider.
The Storemate backend is a thin proxy: the mobile app keeps calling
/api/v1/kyc/* on the Storemate API exactly as before; each handler forwards to
the microservice, which talks to Sumsub and owns all verification data
(documents, selfies, PII, provider ids). Storemate keeps only a small
denormalized KycProfile mirror for fast feature-gating.
Provider =
sumsubin both environments today. The microservice also has a built-in "in-house" engine behind the same interface — we will switch to it later with zero mobile-app changes (see §6).
Contents:
- Architecture & data flow
- Backend / DevOps setup
- Sumsub dashboard setup
- React Native developer guide
- Local end-to-end test
- Future: in-house engine
See also KYC_API_REFERENCE.md for the exact request/response of every endpoint.
1. Architecture & data flow
┌──────────────┐ 1. POST /api/v1/kyc/access-token/ ┌────────────────────┐
│ React Native │ ─────────────────────────────────────▶ │ Storemate backend │
│ app │ ◀──── { token, provider:"sumsub" } ──── │ (thin proxy) │
│ │ └─────────┬──────────┘
│ Sumsub RN │ │ X-Api-Key / X-Api-Secret
│ MobileSDK │ 2. SDK streams docs + selfie + liveness ▼
│ │ ─────────────────────────────────────▶ ┌────────────────────┐ ┌─────────┐
│ │ ◀──────── SDK "submitted" ──────────── │ kyc-service │ ─────▶ │ Sumsub │
└──────┬───────┘ │ kyc.clistech.com │ ◀──── │ │
│ 3. GET /api/v1/kyc/status/ (poll) │ │ 3. POST /webhook/sumsub/
│ or wait for push notification ◀───────────┤ re-pulls the │ ◀──────┘
▼ signed │ AUTHORITATIVE │
shows Verified / Retry / Rejected callback │ status from Sumsub│
Storemate ◀──────────┤ on every webhook │
updates mirror + └────────────────────┘
sends a notification
Key points
- The mobile app talks to two hosts:
- Storemate API (
https://storemate.clistech.com) foraccess-token,status,refresh,reset,branding— always with the user's normal Storemate bearer token. - Sumsub, via the Sumsub Mobile SDK, using the short-lived
tokenfrom theaccess-tokenresponse. Documents never pass through any of our servers. - The verification decision is never trusted from the client or even from
the webhook body. On every Sumsub webhook,
kyc-servicecalls Sumsub'sGET /resources/applicants/{id}/statusand uses that as the source of truth. - When the coarse
statuschanges,kyc-servicepushes a signed callback to Storemate, which updatesKycProfile.statusand sends the user a notification (WebSocket + FCM, automatically).
2. Backend / DevOps setup
2.1 Register Storemate as a project on the microservice
On the kyc-service host:
python manage.py kyc_project storemate \
--name "Storemate" \
--provider sumsub \
--level-name id-and-liveness \
--primary-color "#0B5FFF" \
--support-email support@storemate.clistech.com \
--callback-url https://storemate.clistech.com/api/v1/internal/kyc/status-callback/ \
--callback-secret "<random string>"
It prints api_key (pk_…) and, once, api_secret (sk_…).
Provisioned:
| Environment | api_key | provider | callback_url |
|---|---|---|---|
Server (kyc.clistech.com) |
pk_hzY-As0nm0j9DecPxrQOtM2J9mJfFK53 |
sumsub |
https://storemate.clistech.com/api/v1/internal/kyc/status-callback/ |
| Local | pk_XK8yc0FbTqFVowW2STAbeM6SvMAE8_f9 |
sumsub |
http://localhost:8000/api/v1/internal/kyc/status-callback/ |
✅ Done: the server project is
provider=sumsub, levelid-and-liveness, wired to the Sumsub sandbox, and the full chain (proxy → Sumsub → webhook → callback → mirror + notification) was verified end-to-end on 2026-09-03.
Secrets were shown once at creation — they live in the respective .env files,
never in git.
2.2 Storemate .env
KYC_SERVICE_URL=https://kyc.clistech.com # http://localhost:8001 for local dev
KYC_API_KEY=pk_...
KYC_API_SECRET=sk_...
KYC_CALLBACK_SECRET=<same value as --callback-secret above>
KYC_HTTP_TIMEOUT=20
2.3 kyc-service .env (Sumsub credentials)
KYC_PROVIDER=sumsub
SUMSUB_BASE_URL=https://api.sumsub.com # same host for sandbox & production
SUMSUB_APP_TOKEN=sbx:... # prd:... in production
SUMSUB_SECRET_KEY=... # shown once when the app token is made
SUMSUB_WEBHOOK_SECRET=... # from Dev space → Webhooks → Secret key
SUMSUB_LEVEL_NAME=id-and-liveness # must match the level created in §3
SUMSUB_ACCESS_TOKEN_TTL=600
Get these from the Sumsub dashboard — see §3.
2.4 What was added to this repo
| File | Purpose |
|---|---|
kyc/client.py |
Async httpx client to the microservice (X-Api-Key / X-Api-Secret). |
kyc/views.py |
kyc_router — the 5 user-facing endpoints, each forwards to the service. |
kyc/callbacks.py |
kyc_status_callback — HMAC-verified inbound callback receiver. |
kyc/models.py |
KycProfile — local denormalized mirror (one row per user). |
kyc/admin.py |
Read-only admin. |
storage_mate/urls.py |
Mounts /api/v1/internal/kyc/status-callback/ before the Ninja catch-all. |
inventory/views.py |
api.add_router("/", kyc_router). |
storage_mate/settings.py |
KYC_* settings. |
tests/test_kyc_proxy.py |
Proxy + signed-callback tests (no live service needed). |
Run migrations: python manage.py migrate kyc.
Run tests: python manage.py test tests.test_kyc_proxy.
2.5 The inbound callback
kyc-service POSTs the full status snapshot to
/api/v1/internal/kyc/status-callback/ whenever a user's coarse status
changes, with headers:
X-Kyc-Event: status.changed
X-Kyc-Signature: hex(hmac_sha256(KYC_CALLBACK_SECRET, raw_body))
kyc/callbacks.py verifies the signature in constant time (fails closed if
KYC_CALLBACK_SECRET is unset), updates KycProfile, creates a
support.Notification (which support/signals.py already pushes over WebSocket
and FCM), and returns 200. Any non-2xx response is retried by the
service (durable CallbackDelivery rows + a beat sweep), so a brief Storemate
outage never loses a status update.
2.6 Feature-gating in backend code
# Cheap: reads the local mirror, no network call.
verified = getattr(user, "kyc_profile", None) and user.kyc_profile.is_verified
The mirror is refreshed on every status / refresh call and on every
callback.
3. Sumsub dashboard setup
Do this once per environment (sandbox first, then production). Dashboard: https://cockpit.sumsub.com.
3.1 App token (API credentials)
- Dev space → App tokens → Create app token.
- Permissions: at least Applicant management (read + write) and Generate access tokens.
- Copy the App token →
SUMSUB_APP_TOKEN(sbx:…sandbox,prd:…prod). - Copy the Secret key (shown once) →
SUMSUB_SECRET_KEY. SUMSUB_BASE_URLstayshttps://api.sumsub.comin both cases.
3.2 Verification level (flow)
- Verification flow → Levels → Create level (or edit the default).
- Name it exactly what you put in
SUMSUB_LEVEL_NAME(defaultid-and-liveness). - Enable all of:
- Identity document (passport / national ID / driving licence)
- Selfie / Liveness (3D liveness, not just a photo)
- Face match (selfie ↔ document photo)
- optionally Proof of address if compliance needs it
- Turn on Duplicate applicant check so one person can't verify twice under different accounts.
3.3 Webhook
- Dev space → Webhooks → Add webhook.
- Target URL:
| Environment | URL |
|---|---|
| Production | https://kyc.clistech.com/api/v1/kyc/webhook/sumsub/ |
| Local dev | expose your machine (ngrok http 8001) → https://<id>.ngrok.io/api/v1/kyc/webhook/sumsub/ |
(trailing slash matters)
3. Secret key: generate, copy → SUMSUB_WEBHOOK_SECRET.
4. Payload digest algorithm: HMAC_SHA256_HEX (the service also accepts SHA1
/ SHA512).
5. Event types to send (the service handles all of them; unknown ones are
safely re-pulled):
| Type | Why |
|---|---|
applicantReviewed |
the important one — final GREEN/RED decision |
applicantPending |
user finished uploading → now under review |
applicantOnHold |
compliance / manual hold |
applicantActionPending / applicantActionReviewed |
extra action requested / reviewed |
applicantReset |
applicant reset (our reset endpoint or an admin) |
applicantDeleted |
applicant deleted (GDPR) |
applicantWorkflowCompleted |
if you use Sumsub Workflows |
- Save → "Send test webhook" → the service should return
200. A400means the secret in.envdoesn't match.
3.4 Going to production
Repeat §3.1–3.3 with production app token + webhook, swap the SUMSUB_*
values in the production kyc-service .env, restart the service + its Celery
worker. In sandbox use Sumsub's
test documents to force
GREEN / RED / RETRY.
4. React Native developer guide
4.1 TL;DR
POST /api/v1/kyc/access-token/(Storemate bearer token) →{ token, provider: "sumsub", ttl_secs }.- Launch the Sumsub Mobile SDK with
tokenand a refresh handler that calls the same endpoint again. - On SDK completion, show a "submitted" screen, then poll
GET /api/v1/kyc/status/(or wait for the push notification) and branch onstatus.
4.2 Install the SDK
npm install @sumsub/react-native-mobilesdk-module
# iOS
cd ios && pod install && cd ..
iOS — add to Info.plist:
<key>NSCameraUsageDescription</key><string>To verify your identity</string>
<key>NSMicrophoneUsageDescription</key><string>To record a liveness check</string>
<key>NSPhotoLibraryUsageDescription</key><string>To upload your ID document</string>
Android — minSdkVersion ≥ 21; the SDK pulls its own camera permissions.
If you use Proguard, add the Sumsub rules from their docs.
Docs: https://docs.sumsub.com/docs/react-native-module
4.3 Storemate endpoints (base: https://storemate.clistech.com)
| Method & path | Auth | Rate limit | Purpose |
|---|---|---|---|
POST /api/v1/kyc/access-token/ |
user | 15/min | Create/link the applicant (first call) + mint a short-lived Sumsub SDK token. Also used to refresh the token mid-session. 409 if already approved. |
GET /api/v1/kyc/status/ |
user | — | Current verification snapshot. Safe to poll. |
POST /api/v1/kyc/refresh/ |
user | 6/min | Force an authoritative re-pull from Sumsub. Fallback when the webhook is slow. |
POST /api/v1/kyc/reset/ |
user | 6/min | Reset a failed / stuck attempt so the user can start over. 409 if approved. |
GET /api/v1/kyc/branding/ |
user | — | Project name / logo / colour for theming your intro screen. Cache it. |
Exceeding a rate limit → 429. Full field-by-field reference:
KYC_API_REFERENCE.md.
access-token response (Sumsub):
{
"token": "_act-sbx-...", // give to the Sumsub SDK
"user_id": "3f9a....", // Storemate user id
"provider": "sumsub",
"level_name": "id-and-liveness",
"ttl_secs": 600,
"status": "pending",
"is_verified": false,
"api_base": "" // empty for sumsub
}
4.4 status values — what the app should do
status |
Meaning | App action |
|---|---|---|
not_started |
No attempt yet | Show "Verify identity" CTA |
pending |
Token issued, awaiting the user's documents | Launch / resume the SDK |
in_review |
Submitted, Sumsub reviewing | Show "under review", poll / wait for push |
approved |
Verified ✅ | Unlock the gated feature |
retry |
Rejected, user may resubmit (reject_type == "RETRY") |
Show moderation_comment; button "Try again" → POST /api/v1/kyc/reset/ → back to step 1 |
rejected |
Final rejection (reject_type == "FINAL") |
Dead end; show support contact |
on_hold |
Manual / compliance review | Show "under review" |
Treat a 409 from access-token as success (approved), not an error.
4.5 Launch the SDK
import SNSMobileSDK from "@sumsub/react-native-mobilesdk-module";
import { api } from "../api"; // your authed Storemate client
async function startKyc() {
const first = await api.post("/api/v1/kyc/access-token/");
if (first.status === "approved") return showApproved(); // 409 path
const snsSdk = SNSMobileSDK.init(
first.token,
// token-expiration handler — called by the SDK; must return a fresh token
async () => {
const r = await api.post("/api/v1/kyc/access-token/");
return r.token;
},
)
.withHandlers({
onStatusChanged: (e) => console.log("KYC SDK status:", e.prevStatus, "→", e.newStatus),
onLog: (m) => console.log("KYC SDK:", m),
})
.withDebug(__DEV__)
.build();
const result = await snsSdk.launch();
// result.status is the SDK's view ("Approved" here only means "submitted").
// Confirm the REAL result via the backend:
await confirmKycResult();
}
4.6 After the SDK — confirming the result
The SDK only knows "submitted", not "approved". Then:
- Show a "Verification submitted — we'll let you know" screen.
GET /api/v1/kyc/status/. If stillpending/in_review, either:- poll every ~5–10 s for up to a minute, or
- wait for the push notification (title "Identity verified" / "…needs another try" / "…declined" / "…under review") and refresh on open.
- Optionally call
POST /api/v1/kyc/refresh/once to force an immediate pull. - Branch on
statusper §4.4.
The backend also emits a realtime WebSocket event on the existing
/ws/notifications/ channel — if the app already listens there, refresh
GET /api/v1/kyc/status/ when a KYC notification arrives.
4.7 Gotchas
- Two hosts, two tokens. Storemate endpoints use the user's Storemate bearer
token; the Sumsub SDK uses the
tokenfromaccess-token. Don't mix them. 409onaccess-token= already approved → treat as success.429= rate limited → exponential backoff; never tight-loop the SDK refresh handler.- The SDK token is short-lived (~10 min) — always wire the refresh handler.
- One applicant per user; calling
access-tokenrepeatedly is safe and cheap. - No document images / PII ever reach the Storemate API — only Sumsub via the SDK.
providerin the response will be"inhouse"after we switch engines — see §6; keep a branch for it now and you won't need an app release.
5. Local end-to-end test
You need Sumsub sandbox credentials and a tunnel for the webhook.
# terminal 1 — kyc-service (sandbox Sumsub creds in its .env)
cd kyc-service
python manage.py runserver 8001
celery -A config worker -l info # terminal 1b
# terminal 2 — webhook tunnel
ngrok http 8001
# put https://<id>.ngrok.io/api/v1/kyc/webhook/sumsub/ in the Sumsub dashboard
# terminal 3 — Storemate backend
cd Storemate_Backend
KYC_SERVICE_URL=http://localhost:8001 \
KYC_API_KEY=pk_XK8yc0FbTqFVowW2STAbeM6SvMAE8_f9 \
KYC_API_SECRET=sk_... \
KYC_CALLBACK_SECRET=dev-storemate-secret \
python manage.py runserver 8000
Then, as a logged-in Storemate user:
TOK=<storemate access token>
curl -X POST -H "Authorization: Bearer $TOK" localhost:8000/api/v1/kyc/access-token/
# -> { token: "_act-sbx-...", provider: "sumsub", ttl_secs: 600, ... }
Run the RN app (sandbox), complete verification with a Sumsub test document, then:
curl -H "Authorization: Bearer $TOK" localhost:8000/api/v1/kyc/status/
# -> status transitions pending -> in_review -> approved
# and Storemate's KycProfile mirror + an "Identity verified" notification appear.
The proxy ↔ callback wiring is covered by
tests/test_kyc_proxy.py(no live Sumsub needed). The full chain was verified end-to-end in production against real Sumsub (sandbox) on 2026-09-03: proxyaccess-token→ Sumsub applicant → Sumsub review (GREEN) →applicantReviewedwebhook → authoritative re-pull → signed callback → StoremateKycProfile = approved+ "Identity verified" notification. Zero manual steps.
For the mobile developer: hand them
KYC_APP_DEVELOPER_GUIDE.md — a self-contained
implementation guide (SDK install, code, status handling, testing, checklist).
6. Future: in-house engine
kyc-service ships a built-in verification engine (provider=inhouse) behind
the same KycProvider interface. Switching Storemate to it is one command
on the microservice:
python manage.py kyc_project storemate --provider inhouse
What changes:
access-tokenreturnsprovider: "inhouse"and a non-emptyapi_base.- Instead of the Sumsub SDK, the app uploads to
api_basewithAuthorization: Bearer <token>:POST {api_base}/applicant/documents,/applicant/data,/applicant/submit. - Everything else is identical — the same 5 Storemate endpoints, the same
statusvalues, the same callback, the same notifications.
If the RN app already branches on response.provider, the switch needs no app
release. The engine's decision rules live in
kyc-service/kyc/engine/review.py (today: document + selfie + identity-data
presence check; real biometric / OCR / sanctions checks plug in there later).