# KYC sharing

If you verify your users with Sumsub yourself, you can share a verification with Yodl so the user does not go through KYC a second time. Your backend mints a single-use **share token** for an applicant you have already verified and submits it to us together with the user's email — before the user has a Yodl account, a wallet, or any contact with Yodl at all. When they later sign up with that email, the verification is already on their account.

Under the hood this is Sumsub's [Reusable KYC](https://docs.sumsub.com/docs/reusable-kyc-share): Sumsub copies the applicant's documents and liveness from your Sumsub account into ours and re-runs its checks against our verification level. No document capture UI is involved, and the user is not.

:::warning
KYC sharing needs setup on both sides: you must be a Sumsub client, and the sharing relationship between your Sumsub account and ours must be authorized in Sumsub before a share token can be minted. We exchange Sumsub client IDs during [onboarding](/integrations/onboarding) — ask for it there.
:::

## How it works

```mermaid
sequenceDiagram
    autonumber
    participant BE as Your backend
    participant S as Sumsub
    participant Y as Yodl

    BE->>S: Mint share token for a verified applicant
    S-->>BE: Single-use share token
    BE->>Y: POST /api/v1/integration/sumsub-share-token
    Y->>S: Redeem the token
    S-->>Y: Verification copied, checks re-run
    Y-->>BE: reviewStatus "init", reused true
    S--)Y: Webhook with the verdict, minutes later
```

The verdict is asynchronous. A successful submission normally answers `reviewStatus: "init"`; Sumsub's re-check completes on its own schedule and reaches us by webhook. There is no polling endpoint for integrations — the user sees their verification status in the app once they have signed up.

## Minting the share token

Your backend asks Sumsub for the token, authenticated with your own Sumsub credentials:

```text
POST https://api.sumsub.com/resources/accessTokens/shareToken?applicantId={applicantId}&forClientId={yodlClientId}
```

* `applicantId` is the applicant on **your** side, and must be verified — sharing an unapproved applicant fails at redemption.
* `forClientId` is our Sumsub client ID, which we give you during setup.
* The token is single use and expires after 20 minutes by default (`ttlInSecs` to change that). Mint it at the moment you submit, not in advance.

## The endpoint

```text
POST https://api.yodl.me/api/v1/integration/sumsub-share-token
```

HTTP Basic, with the same credential pair as the [Payments API](/integrations/payments-api#authentication): your integration address as username, your **API secret** as password. Every authentication failure is the same `401`, and body validation happens only after authentication.

```json
{ "email": "user@example.com", "kycShareToken": "..." }
```

| Field | Notes |
| --- | --- |
| `email` | The email you know the user by. The verification attaches to the Yodl account this address resolves to |
| `kycShareToken` | The share token from Sumsub, verbatim |

:::code-group
```bash [curl]
# $SHARE_TOKEN minted from Sumsub as above
curl -u "$YODL_INTEGRATION_ADDRESS:$YODL_API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{"email": "user@example.com", "kycShareToken": "'"$SHARE_TOKEN"'"}' \
  https://api.yodl.me/api/v1/integration/sumsub-share-token
```

```ts [TypeScript]
import { createHmac } from 'node:crypto';

// Mint a single-use share token from Sumsub, signed with YOUR Sumsub credentials.
async function mintShareToken(applicantId: string): Promise<string> {
  const path =
    `/resources/accessTokens/shareToken?applicantId=${applicantId}` +
    `&forClientId=${process.env.YODL_SUMSUB_CLIENT_ID}`;
  const ts = Math.floor(Date.now() / 1000);
  const signature = createHmac('sha256', process.env.SUMSUB_SECRET_KEY!)
    .update(`${ts}POST${path}`)
    .digest('hex');

  const res = await fetch(`https://api.sumsub.com${path}`, {
    method: 'POST',
    headers: {
      'X-App-Token': process.env.SUMSUB_APP_TOKEN!,
      'X-App-Access-Ts': String(ts),
      'X-App-Access-Sig': signature,
    },
  });
  if (!res.ok) throw new Error(`Sumsub ${res.status}`);
  const { token } = await res.json();
  return token;
}

// Submit it to Yodl together with the user's email.
async function shareKycWithYodl(email: string, applicantId: string) {
  const credentials = Buffer.from(
    `${process.env.YODL_INTEGRATION_ADDRESS}:${process.env.YODL_API_SECRET}`,
  ).toString('base64');

  const res = await fetch('https://api.yodl.me/api/v1/integration/sumsub-share-token', {
    method: 'POST',
    headers: { Authorization: `Basic ${credentials}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, kycShareToken: await mintShareToken(applicantId) }),
  });

  if (!res.ok) throw new Error(`Yodl API ${res.status}: ${await res.text()}`);
  return res.json();
}
```
:::

## Response

```json
{
  "reviewStatus": "init",
  "reviewAnswer": null,
  "rejectType": null,
  "rejectLabels": [],
  "reused": true
}
```

| Field | Values | Notes |
| --- | --- | --- |
| `reviewStatus` | `not_started`, `init`, `pending`, `prechecked`, `onHold`, `completed` | Where Sumsub's re-check stands |
| `reviewAnswer` | `GREEN`, `RED`, `null` | The verdict. Only meaningful once `reviewStatus` is `completed` |
| `rejectType` | `RETRY`, `FINAL`, `null` | On `RED`: whether resubmission can help |
| `rejectLabels` | array of strings | Sumsub's reject reasons, verbatim |
| `reused` | boolean | Whether **this request** redeemed the token. `false` means nothing was redeemed and the token was not consumed |

Responses are sent with `Cache-Control: private, no-store`. Do not put them in a shared cache.

## Which account it attaches to

The verification lands on the account the email resolves to:

1. the account that has verified ownership of the email, if one exists,
2. else an existing account carrying that email unverified,
3. else a placeholder account we create on the spot.

The placeholder is claimed by the normal registration flow once the user signs up and proves the email — by OTP, or instantly via your [email attestation](/integrations/email-attestation). Either way the KYC state is sitting on the account when they arrive.

The response is identical whether the email already had an account or not. This endpoint is deliberately not an account-existence oracle.

## You only see what you shared

Redeemed verifications record your integration as the donor, and the endpoint only acts on — and reports — state you donated. KYC that the user completed with Yodl directly, or that another integration shared, gets one neutral `409` whatever its actual status, approved or rejected alike. You cannot probe verification state you did not produce.

## Retries and races

The endpoint is safe to retry — a resubmission never damages settled or in-flight state:

* An already-approved verification you shared answers its status with `reused: false`. The token stays unconsumed.
* A verification still in flight also answers `reused: false`, and the pending applicant is not displaced.
* A completed `RED` you shared redeems again — sharing a fresh token is the retry path after a rejection.
* Two concurrent submissions for the same email redeem exactly once. The loser answers `reused: false`, possibly with `reviewStatus: "not_started"` while the winner is still processing, and its token stays unconsumed.
* After a `422`, retry with a freshly minted token — the failed one may already be spent or expired. The retry lands on the same account.

## Errors

| Status | When |
| --- | --- |
| `400` | Malformed body: `email` must be a valid address and `kycShareToken` non-empty |
| `401` | Missing or invalid credentials — same neutral response as the [Payments API](/integrations/payments-api#authentication) |
| `404` | KYC sharing is not enabled on this environment |
| `409` | KYC state exists for this user but you did not share it. Body: `{"error": "KYC unavailable for this user"}` |
| `422` | Sumsub rejected the redemption: token expired or already used, the applicant is not approved, or your verification level and ours have no overlapping steps. The body's `message` carries Sumsub's description |

Error bodies are `{"error": "..."}`, plus `message` on a `422`.
