Back to API Docs

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 = sumsub in 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:

  1. Architecture & data flow
  2. Backend / DevOps setup
  3. Sumsub dashboard setup
  4. React Native developer guide
  5. Local end-to-end test
  6. 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


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, level id-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)

  1. Dev space → App tokens → Create app token.
  2. Permissions: at least Applicant management (read + write) and Generate access tokens.
  3. Copy the App token → SUMSUB_APP_TOKEN (sbx:… sandbox, prd:… prod).
  4. Copy the Secret key (shown once) → SUMSUB_SECRET_KEY.
  5. SUMSUB_BASE_URL stays https://api.sumsub.com in both cases.

3.2 Verification level (flow)

  1. Verification flow → Levels → Create level (or edit the default).
  2. Name it exactly what you put in SUMSUB_LEVEL_NAME (default id-and-liveness).
  3. Enable all of:
  4. Identity document (passport / national ID / driving licence)
  5. Selfie / Liveness (3D liveness, not just a photo)
  6. Face match (selfie ↔ document photo)
  7. optionally Proof of address if compliance needs it
  8. Turn on Duplicate applicant check so one person can't verify twice under different accounts.

3.3 Webhook

  1. Dev space → Webhooks → Add webhook.
  2. 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
  1. Save → "Send test webhook" → the service should return 200. A 400 means the secret in .env doesn'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

  1. POST /api/v1/kyc/access-token/ (Storemate bearer token) → { token, provider: "sumsub", ttl_secs }.
  2. Launch the Sumsub Mobile SDK with token and a refresh handler that calls the same endpoint again.
  3. On SDK completion, show a "submitted" screen, then poll GET /api/v1/kyc/status/ (or wait for the push notification) and branch on status.

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:

  1. Show a "Verification submitted — we'll let you know" screen.
  2. GET /api/v1/kyc/status/. If still pending / in_review, either:
  3. poll every ~5–10 s for up to a minute, or
  4. wait for the push notification (title "Identity verified" / "…needs another try" / "…declined" / "…under review") and refresh on open.
  5. Optionally call POST /api/v1/kyc/refresh/ once to force an immediate pull.
  6. Branch on status per §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


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: proxy access-token → Sumsub applicant → Sumsub review (GREEN) → applicantReviewed webhook → authoritative re-pull → signed callback → Storemate KycProfile = 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:

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