Customers & onboarding

This guide walks the first week of an integration: the customer record, what its status means, how identity (KYC) or business (KYB) verification is started and followed, and the two traps that catch most new integrations — a read that changes state, and a list that does not.

All paths below are on the v1 surface (/api/v1/partner/..., snake_case bodies, the {success, status_code, message, data} envelope). Reference: Customer API v1.

1. The customer record

A customer is a person (user_type: Personal, verified by KYC) or a company (user_type: Business, verified by KYB). Every other resource — fiat account, wallet, withdrawal, conversion — belongs to exactly one customer, and every customer-scoped route takes its id in the path as customerId.

Your first call should be GET /api/v1/partner/me (no scope needed beyond a valid key): a customer-bound key learns its own customer_id there. A tenant-wide key acts for every customer on the account and lists them with GET /api/v1/partner/customers (onboarding:read).

The record you get back:

{
  "id": "0198f3a0-1234-7000-8000-0000000000c1",
  "external_reference": "jane.doe@example.com",
  "display_name": "Jane Doe",
  "user_type": "Personal",
  "jurisdiction": "GB",
  "status": "Onboarding",
  "record_type": "Customer",
  "created_at": "2026-09-01T09:30:00+00:00",
  "updated_at": "2026-09-02T10:15:30+00:00"
}

external_reference is the customer's email as supplied at creation. It is unique within your account and is the key the email-keyed onboarding routes resolve on.

Creating a customer

The customer record itself is normally created before your API key is issued — in the KwiikPay dashboard or by your account manager — which is why the v1 reference lists no creation call. A POST customers route does exist on this surface, but it belongs to the end-customer / whitelabel programme (creating and verifying your customers under KwiikPay's registration), which must be enabled for your account first. If you need it, ask your account manager; until it is enabled, a key bound to your customer account answers 403 sub_customer_onboarding_disabled (a tenant-wide partner key is not gated on the programme flag — it creates direct customers, as it always has). When usable it is a find-or-create keyed on email: posting an email that already exists returns that customer (and the message still says "created"), a new one starts as Draft, jurisdiction is required and never defaulted, password is refused with 422 password_not_supported (no identity account exists, so a credential would be discarded), and email_verified and send_otp are accepted and ignored.

2. Lifecycle status

status on the customer is the lifecycle status. It is the value every money-movement route checks, and only one value passes:

status Meaning Money movement
Draft Record exists, verification never started Blocked
Onboarding A verification session has been opened Blocked (all)
PendingCompliance Submitted, awaiting a decision or a manual review Blocked
Active Verified Allowed
Rejected Verification refused Blocked
Suspended / Blocked Set by you (PATCH) or by compliance Blocked
Closed Set by you (PATCH) or by compliance Blocked

Two rules to design around:

The same PATCH updates name, jurisdiction (unrecognised values are refused, never coerced) and email/external_reference (one field, external_reference wins; a value another customer already holds is 409 external_reference_in_use). Every field is optional and omitted fields are unchanged.

3. Starting verification

Verification runs in Sumsub. Your job is to open a session, hand it to the customer, and poll.

For a person:

curl -X POST https://api.kwiikpay.io/api/v1/partner/customers/{customerId}/onboarding/kyc/initiate \
  -H "X-Api-Key: kp_live_..." -H "Idempotency-Key: 4f7c1c2e-..."

For a company use POST /api/v1/partner/customers/{customerId}/onboarding/kyb/initiate. Both need onboarding:write. Calling the KYC route for a business (or KYB for a person) is 422 invalid_onboarding_type.

{
  "success": true, "status_code": 200, "message": "KYC verification session created",
  "data": {
    "verification_link": "https://in.sumsub.com/websdk/p/...",
    "token": "_act-sbx-jwt-...",
    "applicant_id": "66b3f2a1c0ffee0001a1b2c3",
    "onboarding_case_id": "0198f3a0-1234-7000-8000-0000000000aa",
    "status": "pending"
  }
}

Things the reference states that are easy to miss:

The four token states

The 200 response has four shapes, and two of them carry a null token on purpose:

  1. First call — a real token and link. Launch the SDK or send the link.
  2. Replay — a re-minted token for the same case. Identical handling.
  3. Already verifiedtoken and verification_link are both null and status is approved. Do not launch the SDK; the customer is done.
  4. Held for review — both null because the case was parked for manual review before Sumsub was called. Nothing for the customer to do yet; poll the status route.

Never launch the Sumsub SDK on a null token. If your integration only handles state 1, states 3 and 4 look like a broken response.

Token lifetime: 14 days

A Web SDK token is valid for 14 days from minting. Expiry does not surface as an API error — the failure happens inside the Sumsub SDK when it initialises with the stale token. Mint a fresh token (call the initiate route again) whenever the customer returns after a gap; do not cache one across sessions.

4. Following verification

Poll GET /api/v1/partner/customers/{customerId}/onboarding/status (onboarding:read):

{
  "current_step": "kyc",
  "steps": { "kyc": "in_progress", "kyb": "not_applicable" },
  "kyc_status": "in_progress",
  "kyb_status": null,
  "can_proceed_to_kyb": false,
  "customer_status": "Onboarding"
}

Three vocabularies sit on this one response — read the right one:

Field Vocabulary Values
kyc_status, kyb_status, steps.* verification pending, in_progress, approved, rejected, blocked, closed (+ not_applicable under steps)
current_step step kyc, kyb, completed
customer_status lifecycle Draft, Onboarding, PendingCompliance, Active, Rejected, Suspended, Blocked, Closed

The verification vocabulary is a mapping of the lifecycle status (Draftpending, Onboarding/PendingCompliancein_progress, Activeapproved, Rejectedrejected, Suspended/Blockedblocked, Closedclosed); an unmapped lifecycle value passes through raw, so treat anything you do not recognise as not-yet-approved rather than failing. Exactly one of kyc_status/kyb_status is non-null, decided by user_type. can_proceed_to_kyb is true only for an Active person — it is always false for a business, so it cannot gate KYB.

The single-kind routes GET .../onboarding/kyc/status and GET .../onboarding/kyb/status return the same mapped value with customer_status; asking the KYC route about a business is a 200 with kyc_status: null, not an error.

"Approved" for money purposes means customer_status: Active (equivalently kyc_status/kyb_status: approved). Until then, fiat account creation, withdrawals and wallet provisioning refuse the customer.

5. The read that refreshes — and the list that does not

Activation is not pushed to your record by Sumsub's decision alone. It happens when one of these is called for the customer:

Each of these re-checks the latest Sumsub case first and, if Sumsub has approved it, writes status: Active and lifts the money-movement block during that call. If Sumsub cannot be reached the read still succeeds with the stored, possibly stale, status.

GET /api/v1/partner/customers does not do this. So:

A customer that is Suspended, Blocked, Closed or Rejected is never re-activated by a read.

6. Manual submission (API-supplied data)

Instead of a hosted session, you can file the applicant yourself from data and base64 documents you already hold. Start with GET /api/v1/partner/onboarding/requirements (onboarding:read, no customer id): pass level, or let it resolve — type=kyb for the business level, anything else for the individual level.

{
  "level": "individual-onboarding",
  "applicant_type": "individual",
  "required_fields": ["person.first_name", "person.last_name", "person.dob", "person.email", "person.phone"],
  "required_documents": ["identity (PASSPORT/ID_CARD/RESIDENCE_PERMIT/DRIVERS, FRONT_SIDE+BACK_SIDE)", "SELFIE", "UTILITY_BILL"],
  "questionnaire_required": true,
  "business_sectors": null
}

required_fields are the exact JSON paths on the submit body. required_documents is prose for building an upload form, not a machine list. For the business level, business_sectors is the banking provider's sector vocabulary that the KYB submit validates against.

The submit routes themselves — POST onboarding/kyc and POST onboarding/kyb under the v1 prefix, with onboarding:write and an Idempotency-Key — are, like customer creation, part of the end-customer programme: they resolve the subject by email and create the customer if the email is new, so they answer 403 sub_customer_onboarding_disabled until that is enabled for your account. When enabled: email (or person.email / company.email) and jurisdiction (or person.country / company.country) are required; the response carries the customer_id, applicant_id, onboarding_case_id and a first status in a third, PascalCase vocabulary (Pending, Approved, ManualReview, ...) — poll the status route for the ongoing state.

One wrinkle to handle: a 422 missing_required_fields names the missing fields in an internal camelCase spelling (person.firstName, person.dateOfBirth for dob, company.address.postCode). Map them back to the snake_case paths from the requirements route.

7. End customers (summary)

If end-customer onboarding is enabled for your account, a customer of yours can create end customers under it — POST customers/{customerId}/end-customers with subject_type Personal or Business, an external_reference (a stable business reference: posting it again for the same parent and subject type returns the same record; an HTTP Idempotency-Key is also required), country_code, and for a company legal_name and registration_number. Verification is started with POST .../end-customers/{endCustomerId}/onboarding (end-customers:write; contact_email is required for everyone, business_profile for a company) and returns the same session shape as section 3, with status narrowed to verified or pending. The end customer's verification documents can be listed and downloaded (end-customers:read): each document has a category of IDENTITY, SELFIE, COMPANY_DOC, FILE_ATTACHMENT or UNKNOWN, and the download streams the provider's raw bytes with its own content type — not JSON. These routes appear under Sub-customers in the API reference. See Sub-customers & parent access for eligibility, parent authentication, account setup, beneficiaries and fiat withdrawals.

8. Two utility routes, and what they are not

9. Checklist

  1. GET /api/v1/partner/me to learn your customer_id.
  2. POST .../onboarding/kyc/initiate (or /kyb/initiate); handle all four token states.
  3. Hand the customer the link or the token; mint a fresh token after 14 days.
  4. Poll GET .../onboarding/status by id — never the list — until customer_status is Active.
  5. Only then create accounts and move money.
  6. Treat PATCH status as permanent.