> ## Documentation Index
> Fetch the complete documentation index at: https://docs.frayme.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Sumsub

> The full-stack identity suite that Frayme uses for KYC, AML and pre-KYC fraud.

<Info>
  **Resold by Frayme.** No client API key required. Credentials and billing are managed end-to-end.
</Info>

Sumsub is the workhorse of Frayme's identity stack. It bundles three logical surfaces — KYC, AML and FRAUD — inside one applicant lifecycle, and it's the default identity orchestrator for natural-person KYC workflows.

## How Frayme calls Sumsub

The standard pattern across natural-person KYC workflows:

<Steps>
  <Step title="Create applicant">
    A POST to Sumsub creates the applicant skeleton and returns `applicant_id` + `sdk_token`.
  </Step>

  <Step title="Hand off to the client app">
    Frayme notifies the client app with the applicant id and an SDK launch hint. The client app opens the Sumsub SDK; selfie and document binaries flow **directly to Sumsub** — they never touch Frayme.
  </Step>

  <Step title="Wait for the terminal review">
    A long-poll dataSource node (timeout 600s, retry 0) waits for Sumsub to return `review_answer: GREEN | RED` plus reject labels.
  </Step>

  <Step title="Triage in Frayme">
    A rule node partitions the outcome: GREEN → proceed; RED + soft labels → AI false-positive synthesis; RED + hard labels → manual review with deterministic-fail recommendation.
  </Step>
</Steps>

## Real-time actions on a paused node

While a Sumsub `dataSource` node is paused awaiting `review_answer`, you can invoke named **pending actions** on it. Address each by the node's `pending_handle` — from the `node.notification` webhook or `GET /cases/{caseId}/pending` — and `POST /pending/{handle}/actions/{actionId}` with the `cases:callback` scope. The handle and invoke mechanics are covered in [Integration guide → Pending actions on a paused node](/integration/guide#pending-actions-on-a-paused-node).

All six Sumsub actions are **side effects**: they return data and leave the node paused — the review still completes through the `applicantReviewed` webhook. The two prefill actions are a matched pair — use `prefill_applicant_info` on a natural-person applicant and `prefill_company_info` on a company (KYB) applicant; each writes the half of `fixedInfo` that matches how the applicant was created. The two reset actions are a fine/coarse pair — `reset_level_step` resets specific steps, `reset_applicant` resets the whole applicant.

| `actionId`               | Purpose                                                                                                                               | Returns                                                                                              |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `refresh_sdk_token`      | Re-mint a WebSDK access token for the same applicant (SDK integration).                                                               | `sdk_token`, `sdk_token_expires_at`, `sdk_user_id`, `customization_name`                             |
| `regenerate_link`        | Re-mint the hosted verification link for the same applicant (direct-link integration).                                                | `verification_url`                                                                                   |
| `prefill_applicant_info` | Push identity data onto a natural-person applicant's Sumsub `fixedInfo` (a PATCH). Only the fields you send are written.              | `applicant_id`, `fixed_info` (the updated object Sumsub echoes back), `email` (echoed when supplied) |
| `prefill_company_info`   | Push company data onto a company (KYB) applicant's Sumsub `fixedInfo.companyInfo` (a PATCH). Only the fields you send are written.    | `applicant_id`, `company_info` (the updated object Sumsub echoes back)                               |
| `reset_level_step`       | Reset **one or more** verification steps of the applicant so the person must re-submit just those steps (not the whole verification). | `applicant_id`, `id_doc_set_types` (the steps that were reset)                                       |
| `reset_applicant`        | Reset the **entire** applicant (all steps + data) so the person must redo their whole verification. Takes no params.                  | `applicant_id`                                                                                       |

### Request bodies

<CodeGroup>
  ```json refresh_sdk_token theme={null}
  { "params": { "token_ttl_secs": 1200 } }
  ```

  ```json regenerate_link theme={null}
  { "params": { "token_ttl_secs": 1800 } }
  ```

  ```json prefill_applicant_info theme={null}
  {
    "params": {
      "first_name": "志强",
      "last_name": "王",
      "dob": "1990-05-15",
      "gender": "M",
      "country": "GBR",
      "nationality": "GBR",
      "addresses": [
        { "country": "GBR", "town": "London", "street": "5 Wimpole Street", "post_code": "W1G" }
      ]
    }
  }
  ```

  ```json prefill_company_info theme={null}
  {
    "params": {
      "company_name": "Acme Robotics Ltd",
      "registration_number": "09876543",
      "country": "GBR",
      "incorporated_on": "2015-03-22",
      "type": "Private Limited Company",
      "tax_id": "GB123456789",
      "email": "compliance@acme.example",
      "website": "https://acme.example",
      "address": {
        "country": "GBR",
        "town": "London",
        "street": "5 Wimpole Street",
        "post_code": "W1G 9SL"
      }
    }
  }
  ```

  ```json reset_level_step theme={null}
  { "params": { "id_doc_set_types": ["IDENTITY", "SELFIE"] } }
  ```

  ```json reset_applicant theme={null}
  {}
  ```
</CodeGroup>

`token_ttl_secs` is optional on the token and link actions (defaults: 600s for the SDK token, 1800s for the link). Every field on both prefill actions is optional — send only what you want written; empty values are skipped, and re-sending the same body is idempotent. `reset_level_step` takes a **required** `id_doc_set_types` array.

#### `prefill_applicant_info` fields

All fields are optional; send only what you want written. Every field is a `string` except `addresses`, which is an array of address objects (`object[]`). Each param maps to a Sumsub `fixedInfo` key (except `email`, which is a top-level applicant attribute set via `change-profile-data`). Country fields are ISO 3166-1 alpha-3.

| Param              | Type       | Sumsub field                               | Notes                                                                     |
| ------------------ | ---------- | ------------------------------------------ | ------------------------------------------------------------------------- |
| `first_name`       | `string`   | `fixedInfo.firstName`                      | Original-language scripts accepted; Sumsub derives a Latin `firstNameEn`. |
| `last_name`        | `string`   | `fixedInfo.lastName`                       | Original-language scripts accepted; Sumsub derives a Latin `lastNameEn`.  |
| `middle_name`      | `string`   | `fixedInfo.middleName`                     |                                                                           |
| `legal_name`       | `string`   | `fixedInfo.legalName`                      |                                                                           |
| `gender`           | `string`   | `fixedInfo.gender`                         | Enum: `M` \| `F` \| `X`.                                                  |
| `dob`              | `string`   | `fixedInfo.dob`                            | `YYYY-MM-DD`. More than 150 years in the past is rejected.                |
| `place_of_birth`   | `string`   | `fixedInfo.placeOfBirth`                   |                                                                           |
| `country_of_birth` | `string`   | `fixedInfo.countryOfBirth`                 | Alpha-3.                                                                  |
| `country`          | `string`   | `fixedInfo.country`                        | Alpha-3.                                                                  |
| `nationality`      | `string`   | `fixedInfo.nationality`                    | Alpha-3.                                                                  |
| `tin`              | `string`   | `fixedInfo.tin`                            | Taxpayer ID number.                                                       |
| `phone`            | `string`   | `fixedInfo.phone`                          |                                                                           |
| `email`            | `string`   | applicant root (via `change-profile-data`) | Not part of `fixedInfo`; echoed back in the response.                     |
| `addresses`        | `object[]` | `fixedInfo.addresses[]`                    | Array of address objects — object fields below.                           |

Each `addresses` entry is an object with these fields (all `string`):

| Address field     | Type     | Sumsub field     | Notes    |
| ----------------- | -------- | ---------------- | -------- |
| `country`         | `string` | `country`        | Alpha-3. |
| `post_code`       | `string` | `postCode`       |          |
| `town`            | `string` | `town`           |          |
| `street`          | `string` | `street`         |          |
| `sub_street`      | `string` | `subStreet`      |          |
| `state`           | `string` | `state`          |          |
| `building_number` | `string` | `buildingNumber` |          |
| `flat_number`     | `string` | `flatNumber`     |          |

#### `prefill_company_info` fields

All fields are optional; send only what you want written. Every field is a `string` except `address`, which is a structured object (`object`). Each param maps to a Sumsub `fixedInfo.companyInfo` key. Values are forwarded to Sumsub as-is and validated there.

Sumsub's `companyInfo` carries **both** free-text address strings (`legal_address`, `postal_address`) **and** a structured `address` object — they are independent fields, so you may send either, both, or neither.

| Param                   | Type     | Sumsub `companyInfo` field | Notes                                                          |
| ----------------------- | -------- | -------------------------- | -------------------------------------------------------------- |
| `company_name`          | `string` | `companyName`              |                                                                |
| `registration_number`   | `string` | `registrationNumber`       |                                                                |
| `country`               | `string` | `country`                  | ISO 3166-1 alpha-3 (e.g. `GBR`).                               |
| `registration_location` | `string` | `registrationLocation`     |                                                                |
| `legal_address`         | `string` | `legalAddress`             | Free-text address string; for structured fields use `address`. |
| `incorporated_on`       | `string` | `incorporatedOn`           | `YYYY-MM-DD`.                                                  |
| `type`                  | `string` | `type`                     | The company's legal form, e.g. `Private Limited Company`.      |
| `email`                 | `string` | `email`                    |                                                                |
| `phone`                 | `string` | `phone`                    |                                                                |
| `tax_id`                | `string` | `taxId`                    |                                                                |
| `lei_code`              | `string` | `leiCode`                  | Legal Entity Identifier.                                       |
| `website`               | `string` | `website`                  |                                                                |
| `postal_address`        | `string` | `postalAddress`            | Free-text address string; for structured fields use `address`. |
| `control_scheme`        | `string` | `controlScheme`            | Ownership/control structure.                                   |
| `applicant_position`    | `string` | `applicantPosition`        | Role of the person acting for the company.                     |
| `address`               | `object` | `companyInfo.address`      | Structured company address — object fields below.              |

`address` is an object with these fields (all `string`):

| Address field     | Type     | Sumsub field     | Notes    |
| ----------------- | -------- | ---------------- | -------- |
| `country`         | `string` | `country`        | Alpha-3. |
| `post_code`       | `string` | `postCode`       |          |
| `town`            | `string` | `town`           |          |
| `street`          | `string` | `street`         |          |
| `sub_street`      | `string` | `subStreet`      |          |
| `state`           | `string` | `state`          |          |
| `building_number` | `string` | `buildingNumber` |          |
| `flat_number`     | `string` | `flatNumber`     |          |

`prefill_company_info` targets an applicant created as a **company** (Sumsub `type: company`) — it writes `fixedInfo.companyInfo`, not the top-level person `fixedInfo` that `prefill_applicant_info` writes. Pick the prefill action that matches the node's applicant type.

#### `reset_level_step` fields

Resets one or more verification steps of the applicant — the person then re-submits just those steps, and the review completes through the `applicantReviewed` webhook as usual. The rest of their verification is untouched (this is not a full applicant reset).

| Param              | Type       | Notes                                                                                                          |
| ------------------ | ---------- | -------------------------------------------------------------------------------------------------------------- |
| `id_doc_set_types` | `string[]` | **Required.** A non-empty array of the verification steps to reset, each a Sumsub `IdDocSetType` (upper-case). |

Common values: `IDENTITY`, `SELFIE`, `PROOF_OF_RESIDENCE`, `PROOF_OF_PAYMENT`, `APPLICANT_DATA`, `QUESTIONNAIRE`, `PHONE_VERIFICATION`, `EMAIL_VERIFICATION`, `E_SIGN`, and the company steps `COMPANY`, `COMPANY_DATA`, `COMPANY_DOCUMENTS`, `COMPANY_BENEFICIARIES`.

**All-or-nothing:** every requested step must be configured on the applicant's level. Duplicates are collapsed, and up to 32 steps may be reset per call. Before resetting anything, the request is checked against the applicant's steps — if **any** requested step is not on the level, the whole call is rejected with `400` and **nothing is reset** (so a mistaken step can't leave the applicant half-reset). Each valid step is then reset in turn.

This action is available **only while the node is paused** (it addresses the applicant by the paused node's handle) — it cannot reset a step after the case has been decided.

#### `reset_applicant`

Resets the **entire** applicant — every step and all collected data — so the person redoes their whole verification. Use it when a step-level reset isn't enough (e.g. the wrong person was verified). Takes **no params**; the request body is `{}`. Returns `applicant_id`. Like `reset_level_step`, it is a side effect (the node stays paused and the review completes through `applicantReviewed`), and is available **only while the node is paused** — not after the case is decided.

## Resubmission retries

When Sumsub rejects an applicant with a **fixable** problem — a blurry selfie, a glare-covered document — it returns `review_answer: RED` with `reviewRejectType: RETRY` and reopens the applicant so they can try again. If the node is configured to allow retries (see [Configuration](#configuration)), Frayme treats this as a **retry round** rather than a terminal decline: it keeps the node paused, re-mints the verification artifact, and notifies you so you can prompt the end user to resubmit. A hard rejection (`reviewRejectType: FINAL`) is always terminal and is never retried.

### Detecting a retry

A retry round arrives as a `node.notification` webhook (see [Integration guide → Webhooks](/integration/guide#webhooks)) with `payloadType` set to **`sumsub-retry-requested`** — that `payloadType` is your signal that the applicant must resubmit. Detection is purely the `payloadType`; you do not poll or compute anything.

```json theme={null}
{
  "webhookId": "9b8a7c6d-5e4f-3a2b-1c0d-9e8f7a6b5c4d",
  "event_type": "node.notification",
  "payloadType": "sumsub-retry-requested",
  "tenantId": "tenant_01HABCXYZ...",
  "caseId": "case_01HABCXYZ...",
  "nodeId": "node_sumsub",
  "webhookKey": "kyc-links",
  "output": {
    "applicant_id": "6a3d4ca539de5a064a06f245",
    "external_user_id": "tenant_01HABCXYZ:cust-00481:node_sumsub",
    "review_answer": "RED",
    "review_reject_type": "RETRY",
    "reject_labels": ["SELFIE_MISMATCH"],
    "moderation_comment": "Your selfie did not match your document photo. Please retake it in good lighting.",
    "sdk_token": "_act-sbx-jwt-...",
    "sdk_token_expires_at": "2026-05-19T15:00:00Z",
    "retry_count": 1,
    "max_retries": 3
  }
}
```

### Output fields

| Field                               | Meaning                                                                                                                                                                |
| ----------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `moderation_comment`                | The **applicant-safe** rejection reason — written for the end user, show it to them verbatim. The internal `clientComment` is **never** included in this payload.      |
| `review_reject_type`                | Always `RETRY` for this payload type.                                                                                                                                  |
| `reject_labels`                     | Machine-readable reason codes for the rejection.                                                                                                                       |
| `retry_count`                       | Which resubmission attempt this is (`1` for the first retry, `2` for the second, …).                                                                                   |
| `max_retries`                       | The configured retry budget for the node. `0` means **unlimited**.                                                                                                     |
| `sdk_token`, `sdk_token_expires_at` | A **freshly re-minted** WebSDK access token for the same applicant (SDK integration).                                                                                  |
| `verification_url`                  | A **freshly re-minted** hosted verification link for the same applicant (direct-link integration) — present instead of `sdk_token` when the node uses the direct link. |
| `applicant_id`, `external_user_id`  | The same applicant identifiers as the original `sumsub-application-start` notification.                                                                                |

A fresh artifact is re-minted on every retry round because the previous token or link may have expired by the time the applicant returns. Re-minting is best-effort: if the vendor call fails, the node still stays paused and you still receive the notification — just without a fresh `sdk_token` / `verification_url` that round.

### What to do on a retry

1. Show `moderation_comment` to the end user.
2. Hand them the fresh `sdk_token` (or `verification_url`) and have them resubmit against the **same** applicant — no new case.
3. Do nothing else: the node stays paused and re-emits a `sumsub-retry-requested` notification for each subsequent failed round, then resumes the workflow automatically on the first `GREEN` (or terminal `RED`).

### When the budget runs out

If `max_retries` is greater than `0` and the applicant exhausts it, the next failed round is **not** sent as a retry notification. Instead the node resumes with that round's terminal `RED` verdict and the workflow decides the case (typically `declined`) — you receive a normal [`case.decided`](/integration/guide#webhooks) webhook. The re-minted artifact is deliberately omitted from that terminal output, so a `sumsub-retry-requested` webhook is your only signal to re-prompt; once it stops, the case is moving to a decision.

### Configuration

Retries are a per-node setting, **off by default**:

| Setting       | Effect                                                                                                                                                                                           |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `allow_retry` | When off (default), a `RED` + `RETRY` review is treated as a terminal decline, exactly as before — no retry notification is sent. Turn it on to enable the flow above.                           |
| `max_retries` | The number of resubmissions allowed before the node gives up and lets the workflow decide. `0` = unlimited (the node waits for a `GREEN` or a `FINAL` rejection however many rounds that takes). |

## Regenerate a share token

A Sumsub **share token** lets a partner client (a `forClientId` in Sumsub) access this applicant. When the dataSource node is configured to generate one, Frayme mints it automatically on review completion and surfaces it as `share_token` on the node output. But a partner may need access **long after the case is decided** — so re-issuing a share token is a [durable case action](/integration/guide#durable-case-actions), not a paused-node pending action.

Unlike the three actions above, it is addressed by `caseId` and works from the moment the node ran onward — including after the case is approved or declined:

`POST /cases/{caseId}/actions/sumsub/regenerate_share_token` — requires scope `cases:callback`.

```json theme={null}
{
  "params": {
    "for_client_id": "partner-co",
    "share_token_ttl_secs": 3600
  }
}
```

| Param                  | Required | Purpose                                                     |
| ---------------------- | -------- | ----------------------------------------------------------- |
| `for_client_id`        | yes      | The partner Sumsub `clientId` the applicant is shared with. |
| `share_token_ttl_secs` | no       | Token lifetime in seconds. Omit to use Sumsub's default.    |

Returns `share_token`. The applicant is re-resolved from the case's `external_customer_id`, so the case must carry one (required on every case type). Each call mints a **fresh** token — there is no idempotency.

<Info>
  There is no persisted default for `for_client_id` — supply it on every call. This lets you share the same applicant with a different partner later without reconfiguring the workflow.
</Info>
