> For the complete documentation index, see [llms.txt](https://docs.ur.app/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ur.app/api-reference/kyc-and-kyb/shared-token-kyc-reuse.md).

# Shared-token KYC reuse

This page documents the **shared-token** onboarding path: your platform runs a clone of UR's KYC workflow in **your own** Sumsub tenant, and once a user is approved you share that applicant with UR using a Sumsub **share token**. UR imports the applicant into UR's Sumsub tenant, validates the shared data, and takes the user to a `Live` UR Account.

Use this path when you already operate a Sumsub tenant and want to avoid making the user redo KYC. If you do **not** run a Sumsub tenant, use [Managed Custody SDK KYC](/api-reference/kyc-and-kyb/managed-custody-sdk-kyc.md) instead (UR hosts the Sumsub relationship and the SDK runs KYC on the user's device).

{% hint style="info" %}
Shared-token reuse requires your `partnerId` to be provisioned for `shared-token` mode and paired with UR's Sumsub tenant. Coordinate with your dedicated integration channel before pointing production traffic at it. See [§8 Prerequisites](#8-prerequisites).
{% endhint %}

{% hint style="warning" %}
This supersedes the share-token section of [API reference: Managed Custody Mode](/api-reference/account/managed-custody-mode.md) §3. The current implementation uses a **dedicated handoff endpoint** (`/kyc/reuse-share-token`), not an `applicantId` on `/create-account`, and UR **does not re-run** the KYC checks; see [§1](#1-how-it-works).
{% endhint %}

## 1. How it works

The compliance-grade KYC (including NFC document authentication) happens in **your** Sumsub tenant, running a workflow that UR clones from its own KYC level definitions during onboarding. That is the point at which the regulatory checks are performed.

When you hand the approved applicant to UR:

1. Your backend mints a **single-use share token** scoped to UR's Sumsub `clientId`, valid for a short TTL (about 20 minutes).
2. Your backend calls UR's **handoff endpoint** with that token.
3. UR **copies** the applicant into UR's own Sumsub tenant (Sumsub "Copy Applicant") and performs **data-level validation** on the copied profile: completeness (all required fields present), document images and NFC data present, and eligibility (nationality / residency / age / document expiry). UR **does not re-run** AML/PEP or re-verify the documents; your cloned workflow already did that.
4. UR returns a conclusive verdict for the attempt: `passed`, `incomplete` (remediable; the user must supply what's missing), or a non-remediable terminal rejection.
5. On `passed`, the user signs Form A and your backend submits; UR provisions the account and activates it. On `incomplete`, the user completes the missing level in **your** Sumsub workflow and you hand off again.

Shared-token reuse is available for both account models:

|                  | Managed Custody (this page's primary flow)                                   | External Wallet Access                                  |
| ---------------- | ---------------------------------------------------------------------------- | ------------------------------------------------------- |
| Signing          | Partner Auth (EIP-191), identity in `X-Ur-Id` header                         | Partner Auth (EIP-191), `urId` in the request body      |
| Handoff endpoint | `POST /api/fma/v1/kyc/reuse-share-token`                                     | `POST /api/v1/sumsub/reuse-share-token`                 |
| Poll / submit    | `/api/fma/v1/kyc/check`, `/kyc/form-a-info`, `/kyc/sign-form`, `/kyc/submit` | `/api/v1/sumsub/kyc-check`, `/api/v1/sumsub/kyc-submit` |
| Form A signer    | UR-managed custodial wallet                                                  | The user's own wallet                                   |

The rest of this page describes the **Managed Custody** flow. See [§7 External Wallet variant](#7-external-wallet-variant) for the differences.

## 2. End-to-end sequence

```mermaid
sequenceDiagram
    autonumber
    participant U as End user
    participant PB as Partner backend
    participant PS as Partner Sumsub tenant
    participant UR as UR OpenAPI
    participant US as UR Sumsub tenant

    Note over U,PS: User completes KYC in your cloned UR workflow
    PS-->>PB: applicantReviewed = GREEN
    PB->>UR: POST /create-account (email, X-External-User-Id)
    UR-->>PB: urId, sessionId, evmAddress, state=PartnerDataIngestion
    PB->>PS: Generate share token (forClientId = UR clientId, single-use)
    PS-->>PB: shareToken
    PB->>UR: POST /kyc/reuse-share-token (shareToken, X-Ur-Id)
    UR->>US: Preview + Copy Applicant + data validation
    US-->>UR: imported applicant
    alt verdict = passed
        UR-->>PB: { status: passed }
        UR-->>PB: webhook fma.kyc.reuse_check.result (passed)
        PB->>UR: GET /kyc/form-a-info → text + textHash
        PB->>UR: POST /kyc/sign-form (textHash)
        PB->>UR: POST /kyc/submit
        UR-->>PB: webhook fma.account.result (activated)
    else verdict = incomplete
        UR-->>PB: { status: incomplete, missingFields }
        UR-->>PB: webhook fma.kyc.reuse_check.result (incomplete, requiredLevels)
        Note over U,PS: User completes the missing level in your workflow
        PB->>UR: POST /kyc/reuse-share-token (new shareToken)  %% same session, next attempt
    end
```

## 3. Integration walkthrough

### 3.1 Complete KYC in your Sumsub workflow

Run the user through the UR-cloned workflow in your Sumsub tenant. Present the required **data-sharing declaration** before identity verification and record the user's agreement; this is the user-facing legal basis for sharing the applicant with UR (see [the required KYC disclosure](https://docs.ur.app/getting-started/integration-guide#kyc-data-sharing-disclosure)). Only hand off applicants whose review answer is **GREEN**.

### 3.2 Create the UR Account

Call `POST /api/fma/v1/create-account` with `X-External-User-Id` and the user's email. UR mints (or reuses) the URID, provisions the UR-managed wallet, and returns `sessionId`, `urId`, `evmAddress`, and `state = PartnerDataIngestion`. You do **not** pass a Sumsub `applicantId` here; the applicant is delivered later through the handoff endpoint.

### 3.3 Mint a share token

After the applicant is GREEN, mint a **single-use** Sumsub share token scoped to UR's `clientId`. Mint a fresh token for every handoff attempt; Sumsub invalidates a token once it is used.

### 3.4 Hand off the applicant

Call `POST /api/fma/v1/kyc/reuse-share-token` with the `shareToken` in the body and `X-Ur-Id` set to the returned `urId`. UR previews the token (does not consume it), copies the applicant, validates the data, and returns a conclusive verdict synchronously:

* `status = passed`: proceed to Form A.
* `status = incomplete` with `missingFields`: the user must complete the missing data in your workflow, then hand off again with a **new** token.
* A terminal rejection (e.g. `AGE_UNDER_18`, `NATIONALITY_RESTRICTED`): the session cannot proceed; this is non-remediable.

You may either read this synchronous response or consume the `fma.kyc.reuse_check.result` webhook (see [§6.1](#6-1-fma-kyc-reuse-check-result)); both carry the same verdict.

### 3.5 Poll completeness (optional)

`POST /api/fma/v1/kyc/check` with `sessionId` returns the last handoff attempt's verdict as a pure read (`complete`, `state`, `missingFields`). Use it as a fallback to the webhook. Poll every 30 seconds to 2 minutes; stop after `/kyc/submit` succeeds.

### 3.6 Render Form A and capture consent

`GET /api/fma/v1/kyc/form-a-info?sessionId=…` returns the exact Form A `text`, its `formAVersion`, and a `textHash`. Display the text to the user and capture consent. If `missingFields` is non-empty, do not proceed.

### 3.7 Sign Form A

`POST /api/fma/v1/kyc/sign-form` with `sessionId` and the `textHash` from the previous step. UR signs Form A with the user's UR-managed custodial wallet and advances the session toward `Register`.

### 3.8 Submit

`POST /api/fma/v1/kyc/submit` with `sessionId`. UR runs the final register against the banking core asynchronously. When the account activates, UR sends the `fma.account.result` webhook with `status = activated`, and the UR Account reaches `Live`.

## 4. API reference

All endpoints use Partner Auth (EIP-191) as described in [Managed Custody Mode §2.2](/api-reference/account/managed-custody-mode.md#2-2-authentication-partner-auth-eip-191). User-scoped endpoints identify the user with `X-Ur-Id` (or `X-External-User-Id`).

### 4.1 Create account

`POST /api/fma/v1/create-account`

| Field            | Required | Description       |
| ---------------- | -------- | ----------------- |
| `email`          | Yes      | User email.       |
| `nationality`    | No       | ISO-3166 alpha-3. |
| `residency`      | No       | ISO-3166 alpha-3. |
| `dob`            | No       | `YYYY-MM-DD`.     |
| `documentExpiry` | No       | `YYYY-MM-DD`.     |

Only `email` (plus the `X-External-User-Id` header) is required at this endpoint. `nationality` / `residency` / `dob` / `documentExpiry` are optional here; partners may submit them later via `/kyc/sync-data`, and the eligibility gates are deferred to `/kyc/check`, which re-validates over the merged staging snapshot before `/kyc/submit`.

Identity: send `X-External-User-Id`. Response `data`: `sessionId`, `urId`, `evmAddress`, `state` (`PartnerDataIngestion` for shared-token).

### 4.2 Hand off share token

`POST /api/fma/v1/kyc/reuse-share-token` · identity: `X-Ur-Id`

| Field        | Required | Description                                                   |
| ------------ | -------- | ------------------------------------------------------------- |
| `shareToken` | Yes      | Single-use Sumsub share token, `forClientId` = UR's clientId. |

Response `data`:

| Field           | Description                                                     |
| --------------- | --------------------------------------------------------------- |
| `status`        | `passed`, `incomplete`, or `terminal`.                          |
| `applicantId`   | The imported applicant ID in UR's tenant (present once copied). |
| `attempt`       | 1-indexed handoff attempt within this session.                  |
| `missingFields` | Present on `incomplete`; machine paths of the gaps.             |

The top-level envelope `code` is the authoritative outcome; `data.status` mirrors it: `passed` → `code = 0`; `incomplete` → `code = 20004`; `terminal` (non-remediable) → a numeric eligibility code (`30010` / `30011` / `30012`, see [§9](#9-error-codes)). On both `incomplete` and `terminal` the envelope carries the non-zero business `code` **and** `data.status` set to the matching discriminator; branch on whichever your integration prefers.

### 4.3 Check

`POST /api/fma/v1/kyc/check` · identity: `X-Ur-Id` · body `{ "sessionId": "…" }`

Pure read of the last handoff verdict. Response `data`: `state`, `complete` (bool), `missingFields`.

### 4.4 Get Form A

`GET /api/fma/v1/kyc/form-a-info?sessionId=…` · identity: `X-Ur-Id`

Response `data`: `formAVersion`, `text`, `textHash` (echo `text`/`textHash` into sign-form). This endpoint returns only these three fields.

### 4.5 Sign Form A

`POST /api/fma/v1/kyc/sign-form` · identity: `X-Ur-Id` · body `{ "sessionId": "…", "textHash": "0x…" }`

Response `data`: `state`, `signature`, `signerAddress` (UR-managed wallet).

### 4.6 Submit

`POST /api/fma/v1/kyc/submit` · identity: `X-Ur-Id` · body `{ "sessionId": "…" }`

Response `data`: `state`, `queued`. Register proceeds asynchronously; wait for the `fma.account.result` webhook.

## 5. Verdict & session state model

One handoff call = **one attempt** on one share token. Remediable failures are attempts within a **living session**; only non-remediable verdicts end the session.

| Verdict                   | Session                                               | What the user does next                                                       |
| ------------------------- | ----------------------------------------------------- | ----------------------------------------------------------------------------- |
| `passed`                  | Advances to `SignFormA`                               | Sign Form A, submit                                                           |
| `incomplete`              | Unchanged; attempt recorded                           | Complete the missing level in your workflow, mint a new token, hand off again |
| terminal (non-remediable) | Ends (subsequent handoffs return `NO_ACTIVE_SESSION`) | Nothing; the user is not eligible                                             |

## 6. Webhook contract

Webhooks are delivered per [Webhooks](https://docs.ur.app/developer-resources/webhook) (envelope `{event, data, timestamp}`, EIP-191 signed, `X-Webhook-Request-Id` idempotency). Subscribe your `partnerId` to both event types below.

### 6.1 fma.kyc.reuse\_check.result

The async mirror of a handoff attempt's verdict. Sent for `passed` and `incomplete` (terminal verdicts are not webhooked in v1). Idempotency business key: `{urId}:kyc:reuse_check:{attempt}`.

```json
{
  "event": "fma.kyc.reuse_check.result",
  "data": {
    "urId": 6233490772,
    "partnerId": "8208",
    "sessionId": "1c1f3904-…",
    "attempt": 1,
    "status": "incomplete",
    "missingFields": ["nfc_data", "registerRequest.address.addressProof"],
    "requiredLevels": [
      { "levelName": "<your NFC level>", "action": "RERUN_LEVEL", "fields": ["nfc_data"] }
    ],
    "occurredAt": 1784218179,
    "correlationId": "c514d9ea-…"
  },
  "timestamp": 1784218179
}
```

On `status = passed`, `missingFields` / `requiredLevels` are omitted. Partners that only act on remediation may ignore `passed`.

### 6.2 fma.account.result

Sent when the account activates after submit. Payload:

```json
{
  "event": "fma.account.result",
  "data": {
    "urId": 6233490772,
    "status": "activated",
    "partnerId": "8208",
    "sessionId": "9abf50d4-…",
    "occurredAt": 1784217340
  },
  "timestamp": 1784217340
}
```

## 7. External Wallet variant

For External Wallet Access, the user holds their own wallet. The flow is the same, with these differences:

* **Auth / identity:** Partner Auth signs the raw body; the `urId` travels in the request body, not the `X-Ur-Id` header.
* **Handoff:** `POST /api/v1/sumsub/reuse-share-token` with `{ shareToken, urId }`. The first handoff lazily creates the onboarding session.
* **Poll:** `POST /api/v1/sumsub/kyc-check`.
* **Submit / Form A:** `POST /api/v1/sumsub/kyc-submit`; the user signs Form A with their own wallet (there is no custodial signer).

The `fma.kyc.reuse_check.result` and `fma.account.result` webhooks are identical.

## 8. Prerequisites

One-time setup, coordinated with your UR integration channel:

1. **Partner pairing.** Your platform and UR are configured as Donor / Recipient in Sumsub so your share tokens are accepted by UR's tenant.
2. **Cloned workflow.** UR shares its KYC level definitions and Sumsub clones UR's KYC workflow (GPS / questionnaire / passport + NFC / liveness) into your tenant. Run users through this workflow so the shared applicant carries every field UR validates.
3. **Provisioning.** Your `partnerId` is set to `fmaMode = shared-token` with a per-partner `sourceKey`, your signer address is registered, and your webhook subscriptions include `fma.kyc.reuse_check.result` and `fma.account.result`.
4. **Declaration.** Your KYC flow presents the required data-sharing declaration and records the user's agreement before identity verification.
5. **Sandbox.** Validate end to end against the testnet base URL before production.

## 9. Error codes

Business errors are returned with HTTP 200 and a non-zero `code` in the response envelope.

| Code    | Meaning                                                    | Action                                                                                                |
| ------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `20003` | Invalid / missing parameter (e.g. `shareToken`, `X-Ur-Id`) | Fix the request.                                                                                      |
| `20004` | Session incomplete                                         | Read `missingFields`; remediate and hand off again.                                                   |
| `20011` | Share token invalid                                        | Mint a fresh single-use token (tokens invalidate on use).                                             |
| `30031` | No active KYC session                                      | Call `/create-account` first, or the session already ended (terminal).                                |
| `30032` | Session on the wrong channel                               | Confirm the partner is in `shared-token` mode.                                                        |
| `30008` | Identity already active in another session                 | The same identity is active in another session; resolve the existing one before reusing the identity. |
| `30010` | Nationality restricted (message: `NATIONALITY_RESTRICTED`) | Non-remediable eligibility rejection; the user is not eligible.                                       |
| `30011` | Residency unsupported (message: `RESIDENCY_UNSUPPORTED`)   | Non-remediable eligibility rejection; the user is not eligible.                                       |
| `30012` | Age under 18 (message: `AGE_UNDER_18`)                     | Non-remediable eligibility rejection; the user is not eligible.                                       |

> The eligibility codes are numeric in the envelope `code`; the string labels (`NATIONALITY_RESTRICTED` etc.) appear only in the human-readable `message`.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.ur.app/api-reference/kyc-and-kyb/shared-token-kyc-reuse.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
