Skip to content

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-info refuses 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 a lockedFields: [...] 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.