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

# Sandbox KYC

> Test shared-token and hosted KYC with explicit outcomes and read-only status polling

The sandbox supports two Sumsub KYC integrations. In the **shared-token** flow, your backend sends a share token and applicant ID to Pods. In the **hosted** flow, Pods creates the applicant and returns a verification link. Both flows let your backend choose a simulated outcome with an explicit sandbox request. Repeated status reads do not change the result.

> [!NOTE]
>
> Use your staging customer API key with the staging API URL, sent as `x-api-key` from your backend. Applicant email does **not** choose the result. Sandbox transition routes are absent in production.

## 1. Shared token

Use the same `POST /api/v1/kyc/sumsub-share-token` request as the production [Sumsub reusable KYC flow](https://docs.pods.finance/guides/kyc/setup.md). Pods does not call Sumsub for this sandbox submission, and Avenia operations are mocked. The share token and applicant ID must be non-empty, but Pods does not validate their contents.

### Before you start

| Item | Requirement |
| - | - |
| Pods base URL | The staging URL provided by Pods. The sandbox routes are absent in production. |
| API key | Your staging customer API key, sent as `x-api-key`. |
| Applicant | A valid CPF, email, and wallet address. Use a fresh identity for each independent test. |
| Sumsub fields | Non-empty share token and applicant ID. You can use placeholders to test only Pods, or real Sumsub sandbox values to test your own token-generation code as well. |

If you want real Sumsub sandbox values, follow [Sumsub setup](https://docs.pods.finance/guides/kyc/setup.md) with your sandbox account: create an applicant, complete its sandbox review, and generate a share token for the Pods recipient. The Pods sandbox does not call Sumsub or inspect the token. The [Node example](https://docs.pods.finance/examples/javascript/kyc-sandbox.md) supports both real Sumsub sandbox values and placeholders.

### 1. Submit a share token

```bash theme={null}
curl -X POST "$PODS_API_BASE_URL/api/v1/kyc/sumsub-share-token" \
  -H "x-api-key: $PODS_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "cpf": "52998224725",
    "shareToken": "sandbox-token-1",
    "sumsubApplicantId": "sandbox-applicant-1",
    "email": "applicant@example.com",
    "walletAddress": "0x0000000000000000000000000000000000000001"
  }'
```

```json theme={null}
{ "kycUserId": "550e8400-e29b-41d4-a716-446655440000", "status": "provider_pending" }
```

Save `kycUserId`. The sandbox submission is now waiting for an explicit outcome. The email can be any valid address; strings such as `approve` or `retry` have no special meaning.

The request also accepts `taxId` and optional `taxIdCountry` instead of `cpf` for the [passport path](https://docs.pods.finance/guides/kyc/setup.md). At least one of `cpf` or `taxId` is required. Use the same applicant identity for a later retry.

### 2. Read status without changing it

```bash theme={null}
curl "$PODS_API_BASE_URL/api/v1/kyc/status?kycUserId=550e8400-e29b-41d4-a716-446655440000" \
  -H "x-api-key: $PODS_API_KEY"
```

The response includes the stored `status` and other KYC fields. Calling this endpoint again leaves `provider_pending` unchanged until you set an outcome. Your application can poll exactly as it will in production, while your test controls when the result arrives.

### 3. Set an outcome explicitly

Call one of these staging sandbox endpoints with the `kycUserId` returned by submission:

| Endpoint | Public status | Retry behavior |
| - | - | - |
| `POST /api/v1/kyc/status/sandbox/approved` | `approved` | Final for this attempt. |
| `POST /api/v1/kyc/status/sandbox/rejected` | `rejected` | Final rejection. |
| `POST /api/v1/kyc/status/sandbox/rejected_retryable` | `rejected_retryable` | Resubmit with a fresh share token. |

For example, choose a retryable rejection:

```bash theme={null}
curl -X POST "$PODS_API_BASE_URL/api/v1/kyc/status/sandbox/rejected_retryable" \
  -H "x-api-key: $PODS_API_KEY" \
  -H "content-type: application/json" \
  -d '{ "kycUserId": "550e8400-e29b-41d4-a716-446655440000" }'
```

The transition returns the full KYC status response. Relevant fields are:

```json theme={null}
{
  "kycUserId": "550e8400-e29b-41d4-a716-446655440000",
  "provider": "sumsub",
  "status": "rejected_retryable",
  "providerStatus": "COMPLETED",
  "providerResult": "REJECTED",
  "rejectReason": "VERIFICATION_RETRY_REQUIRED",
  "brlaEnabled": false,
  "reason": {
    "code": "VERIFICATION_RETRY_REQUIRED",
    "message": "The verification can be retried.",
    "retryable": true
  }
}
```

`GET /api/v1/kyc/status` now returns that result on every read. An `approved` transition returns `providerResult: "APPROVED"` and `brlaEnabled: true` in the mock. A `rejected` transition returns `reason.code: "VERIFICATION_REJECTED"` and `reason.retryable: false`.

> [!NOTE]
>
> The share-token submission already starts in `provider_pending`. You may still call `POST /api/v1/kyc/status/sandbox/provider_pending` with the same `{ "kycUserId": "…" }` body. It returns the current status without starting another attempt or changing the profile. You can then call an outcome route when ready.

### 4. Retry a retryable rejection

Keep the same `sumsubApplicantId`, email, wallet address, and CPF. Generate a **new share token** for that applicant, then submit it to the same endpoint:

```bash theme={null}
curl -X POST "$PODS_API_BASE_URL/api/v1/kyc/sumsub-share-token" \
  -H "x-api-key: $PODS_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "cpf": "52998224725",
    "shareToken": "sandbox-token-2",
    "sumsubApplicantId": "sandbox-applicant-1",
    "email": "applicant@example.com",
    "walletAddress": "0x0000000000000000000000000000000000000001"
  }'
```

```json theme={null}
{ "kycUserId": "550e8400-e29b-41d4-a716-446655440000", "status": "provider_pending" }
```

The profile keeps its `kycUserId`. Now call a sandbox outcome route again, for example `/api/v1/kyc/status/sandbox/approved`, and read the final status. Reusing the previous token leaves the result `rejected_retryable`. Changing the applicant identity returns `409 KYC_SUBMISSION_BINDING_MISMATCH`.

### State and access rules

```mermaid theme={null}
stateDiagram-v2
    [*] --> provider_pending: submit share token
    provider_pending --> provider_pending: optional POST sandbox/provider_pending
    provider_pending --> approved: POST approved
    provider_pending --> rejected: POST rejected
    provider_pending --> rejected_retryable: POST rejected_retryable
    rejected_retryable --> provider_pending: resubmit same applicant with new token
```

| Result | Meaning |
| - | - |
| `404` on a sandbox route in production | The route is not registered there. |
| `404 KYC_PROFILE_NOT_FOUND` | The profile is absent, belongs to another customer, or is not an eligible sandbox Sumsub profile. |
| `409 KYC_SANDBOX_INVALID_TRANSITION` | The current status does not allow the selected next status. |
| `409 KYC_PROFILE_CHANGED` | A concurrent request changed the profile; read its status before retrying. |
| `409 KYC_SUBMISSION_BINDING_MISMATCH` | A retry changed the applicant identity. |

> [!WARNING]
>
> Sandbox outcomes are simulated. They do not perform real identity verification or grant production access.

## 2. Hosted

Use the same `POST /api/v1/kyc/hosted/sessions` request as [Pods Hosted KYC](https://docs.pods.finance/guides/kyc/pods-hosted.md). The sandbox creates a mock Sumsub applicant and returns a URL on `not-a-real-verification-link.invalid`. This link is a placeholder: it does not collect documents or liveness evidence, and opening it does not advance KYC. Your backend controls each state change with the sandbox routes.

### Before you start

| Item | Requirement |
| - | - |
| Pods base URL and API key | Use the staging Pods URL and your staging customer API key in `x-api-key`. |
| Hosted KYC access | Pods must enable hosted KYC for your customer; otherwise session creation returns `403 HOSTED_KYC_NOT_ENABLED`. |
| Applicant | A valid CPF, email, and wallet address. Use a fresh identity for each independent test. |
| Redirect URL | Optional in the request. If supplied, it must match a URL allowlisted for your customer. The customer must have a usable default or allowlisted URL. |

You do not need Sumsub credentials or a share token for this flow. The [hosted Node example](https://docs.pods.finance/examples/javascript/kyc-sandbox.md#2-hosted) exercises the steps below.

### 1. Create a hosted session

```bash theme={null}
curl -X POST "$PODS_API_BASE_URL/api/v1/kyc/hosted/sessions" \
  -H "x-api-key: $PODS_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "cpf": "52998224725",
    "email": "hosted-applicant@example.com",
    "walletAddress": "0x0000000000000000000000000000000000000001"
  }'
```

```json theme={null}
{
  "kycUserId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "created",
  "requiresWebView": true,
  "verificationLink": "https://not-a-real-verification-link.invalid/hosted/…",
  "verificationLinkExpiresAt": "<ISO timestamp>"
}
```

Save `kycUserId`. The sandbox link is deliberately unusable as a verification page. `requiresWebView` has the same shape as the production response, but your sandbox test should simulate completion through the transition endpoint instead of opening this link. Repeating session creation for the same CPF may return the existing profile and link.

### 2. Read status without changing it

```bash theme={null}
curl "$PODS_API_BASE_URL/api/v1/kyc/status?kycUserId=550e8400-e29b-41d4-a716-446655440000" \
  -H "x-api-key: $PODS_API_KEY"
```

Repeated reads remain `created` until your backend starts the simulated review. Later reads remain `provider_pending` until your backend selects an outcome.

### 3. Start the simulated review

```bash theme={null}
curl -X POST "$PODS_API_BASE_URL/api/v1/kyc/status/sandbox/provider_pending" \
  -H "x-api-key: $PODS_API_KEY" \
  -H "content-type: application/json" \
  -d '{ "kycUserId": "550e8400-e29b-41d4-a716-446655440000" }'
```

The transition returns the full status response with `status: "provider_pending"`. A later `GET /api/v1/kyc/status` still returns `provider_pending`. Starting the review twice is invalid; this route is only for `created → provider_pending`.

### 4. Set an outcome explicitly

From `provider_pending`, call one of `/api/v1/kyc/status/sandbox/approved`, `/rejected`, or `/rejected_retryable` with the same `{ "kycUserId": "…" }` body. For example:

```bash theme={null}
curl -X POST "$PODS_API_BASE_URL/api/v1/kyc/status/sandbox/rejected_retryable" \
  -H "x-api-key: $PODS_API_KEY" \
  -H "content-type: application/json" \
  -d '{ "kycUserId": "550e8400-e29b-41d4-a716-446655440000" }'
```

The response and subsequent status reads report `rejected_retryable` with `reason.retryable: true`. `approved` sets `brlaEnabled: true` in the mock. `rejected` ends the simulated attempt.

### 5. Reset a retryable hosted attempt

If the hosted profile reached `rejected_retryable` before Avenia import, call the normal hosted reset endpoint. This returns the **same `kycUserId`** to `created` and issues another dummy link:

```bash theme={null}
curl -X POST "$PODS_API_BASE_URL/api/v1/kyc/hosted/reset" \
  -H "x-api-key: $PODS_API_KEY" \
  -H "content-type: application/json" \
  -d '{ "kycUserId": "550e8400-e29b-41d4-a716-446655440000" }'
```

Call `/api/v1/kyc/status/sandbox/provider_pending` again, then choose the next outcome. The sandbox's placeholder link may have the same URL text after reset; its value does not drive state. Reset is limited to three attempts per profile and returns `409 KYC_RESET_NOT_ALLOWED` when the profile is not eligible. An Avenia-side `rejected_retryable` result after import **cannot** use this reset route because the profile already has an Avenia account link.

### State and access rules

```mermaid theme={null}
stateDiagram-v2
    [*] --> created: POST hosted/sessions
    created --> provider_pending: POST sandbox/provider_pending
    provider_pending --> approved: POST sandbox/approved
    provider_pending --> rejected: POST sandbox/rejected
    provider_pending --> rejected_retryable: POST sandbox/rejected_retryable
    rejected_retryable --> created: POST hosted/reset when eligible
```

The transition endpoint accepts only mock hosted profiles owned by the API-key customer. A real Sumsub applicant or another customer's `kycUserId` returns `404 KYC_PROFILE_NOT_FOUND`. Invalid state changes return `409 KYC_SANDBOX_INVALID_TRANSITION`; concurrent updates return `409 KYC_PROFILE_CHANGED`. In production the sandbox transition routes return `404` because they are not registered. The normal hosted session and reset endpoints remain available there, subject to the real provider and customer configuration.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.