Verification levels¶
Verification levels are the operator-configured recipes that decide which
steps an applicant must complete (ID document, selfie, questionnaire,
AML screening, …) and which identity fields the SDK collects on each
step. Each level has a human-readable name in the shape KYC_01 —
that name is the wire identifier you use everywhere.
You don't have to list levels at runtime. Hard-code the name your
integration cares about (KYC_BASIC, KYC_PREMIUM, KYB_STANDARD,
…) and pass it as-is. The level builder enforces the identifier
shape (^[A-Z][A-Z0-9_]{1,40}$) and locks rename as soon as any
applicant references the level — so once you ship a name in
production, it won't move under you.
GET /v1/levels¶
Returns every active level scoped to the calling credential's product. Operator-only fields (usage counts, internal description, read-only flags, SDK chips) are stripped.
curl -X GET 'https://api.compliance.example/v1/levels?type=individual' \
-H 'X-Api-Key: pk_live_...' \
-H 'X-Api-Secret: ...' \
-H 'X-Environment: production'
{
"items": [
{
"name": "KYC_01",
"applicantType": "individual",
"isActive": true,
"isDefault": true,
"requiredSteps": ["identity_doc", "selfie", "questionnaire"]
}
],
"totalItems": 1
}
Query parameters¶
| Param | Values | Default | Effect |
|---|---|---|---|
type |
individual | company | all |
all |
Filter by applicant type |
includeInactive |
true | false |
false |
Include disabled levels |
GET /v1/levels/{levelName}¶
Single level detail with per-step config + the field-level
enabled/required lists for the level's personal-info / company-data /
company-documents steps. Use this to know exactly what your integration
can prefill on POST /v1/applicants for this level.
{
"id": "a1b2c3d4-...",
"name": "KYB_STANDARD",
"applicantType": "company",
"isActive": true,
"isDefault": false,
"steps": [ /* per-step config; one entry per step the level requires */ ],
"attachedQuestionnaireId": null,
// Individual personal-info form. Empty arrays = level has no
// personal_information step OR didn't customise the field list (SDK
// falls back to its baseline: firstName + lastName + dateOfBirth +
// nationality + email + phone).
"personalInfoFields": ["firstName", "lastName", "email", "phone"],
"personalInfoRequiredFields": ["firstName", "lastName"],
// Company KYB form. Empty arrays = level has no company_data step OR
// didn't customise the field list (SDK falls back to its baseline:
// companyName + registrationCountry + registrationNumber +
// legalAddress + email + phone). Required ⊆ enabled.
"companyDataFields": ["companyName", "registrationCountry", "registrationNumber",
"incorporatedOn", "legalForm", "legalAddress", "email", "phone"],
"companyRequiredFields": ["companyName", "registrationNumber", "registrationCountry"],
// Company documents step. Empty list ⇒ every enabled doc is required.
// companyDocumentsMinRequired null ⇒ SDK uses the required-doc count.
"companyRequiredDocuments": ["certificate_of_incorporation"],
"companyDocumentsMinRequired": 1,
// Per-level legal-form whitelist. Empty ⇒ SDK shows its full catalog.
"companyLegalForms": ["LLC", "JSC", "Ltd"]
}
Returns 404 when the level either doesn't exist or belongs to another tenant — no distinction (would leak existence).
Field-name mapping¶
The level's companyDataFields / personalInfoFields use SDK form keys
(incorporatedOn, legalAddress, registrationCountry). On
POST /v1/applicants the wire names mirror our entity:
| API request field | SDK / level field name |
|---|---|
incorporationDate |
incorporatedOn |
registeredAddress |
legalAddress |
country |
registrationCountry |
The backend translates automatically. Use the entity names on the wire; use the level's keys when reading the enabled / required lists.
Using the level name¶
Pass it as verificationLevelName when you start a session:
curl -X POST 'https://api.compliance.example/v1/applicants/user-12345/verification-sessions?verificationLevelName=KYC_01&lang=ru&theme=dark&returnUrl=https://acme.com/done' \
-H 'X-Api-Key: pk_live_...' \
-H 'X-Api-Secret: ...' \
-H 'X-Environment: production'
The response contains a url you redirect the user to. See
Quick Start for the full session
flow and Session link customization for
the optional lang / theme / returnUrl / successUrl / errorUrl
query params.
What happens to fields the level doesn't enable¶
If your POST /v1/applicants payload includes fields the resolved
level doesn't enable (e.g. you send incorporationDate to a level
whose company_data step doesn't list incorporatedOn), the backend
silently drops them. The applicant is created with the rest. This
lets a partner ship one superset DTO across multiple levels — the
backend keeps only what each level wants.
Anything the partner did send + the level enables → SDK form will show the value as disabled (the user cannot overwrite it). Fields the partner didn't send → SDK form collects them as usual. Required fields the partner didn't send are blocking; optional ones the user can skip.
Field lock — partner-claimed values are immutable¶
Every identity field a partner commits via POST /v1/applicants or
PATCH /v1/applicants/{externalUserId}/company-info is recorded on
the applicant and becomes immutable from the SDK side. The contract:
- In the SDK form the corresponding input renders as
disabled. The user cannot edit a value the partner already owns. - On the backend
POST /api/v1/sdk/personal-inforefuses to mutate any field on the lock list whose submitted value differs from what's already stored — defence-in-depth in case the form-side disabled state is bypassed by a malicious or buggy client. The SDK response returns the dropped field names in alockedFields: [...]array so an integrator can debug. - Reopening a claim is partner-controlled: PATCH the field back to its current value (or to a new value) via the public API. There is no SDK path to unlock a field.
Field-name vocabulary in the lock list matches the level's
personalInfoFields / companyDataFields (SDK form keys —
incorporatedOn / legalAddress / registrationCountry, not the
POST /v1/applicants wire names). Same direction the backend
translates on create.
Practical implication for KYC where the partner only knows a subset: send what you know on create. The user fills the rest in the SDK; the partner-known fields stay verbatim. If you don't want that lock on a field, simply omit it from the create payload — the SDK will collect it editable.