> ## 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.

# Pods Hosted KYC

> Verify an applicant on a page Pods hosts, then poll one status until Pix is unlocked

Use this flow when you do not operate a Sumsub account. Pods creates the verification, returns a link, and the applicant completes document, selfie, and personal data in the browser. When the verification is approved, Pods finishes the onboarding with the local KYC/AML provider and the profile unlocks Brazil Pix (`brlaEnabled`).

You never handle documents or selfies, and you never generate a share token. You make two kinds of calls: one to start (or restart) the verification, and one to read the result.

> [!NOTE]
>
> Pods Hosted KYC is enabled per account. Ask Pods to enable it before you integrate; until then `POST /api/v1/kyc/hosted/sessions` returns `403 HOSTED_KYC_NOT_ENABLED`.

## Before you start

| | |
| - | - |
| API key | Server-to-server, sent as `x-api-key`. Never expose it in a browser or mobile client. |
| Pods Hosted KYC enabled | Pods turns it on for your account. |
| Redirect URLs | Send Pods the exact HTTPS URLs the applicant may return to. Each `redirectUrl` you send must match one of them by origin and path. |
| Applicant data | The CPF, an email, and the wallet address the user's funds will move from. |

## The flow

**1. Create the session**

Your backend sends the CPF, email, and wallet. You get back a `kycUserId` and a `verificationLink`.

**2. Open the link**

Open `verificationLink` in the applicant's browser or a web view. The applicant verifies on the Pods-hosted page.

**3. Poll the status**

While the applicant verifies, and after they return, poll `GET /api/v1/kyc/status` until the status is terminal.

**4. Handle the outcome**

`approved` unlocks Pix. `rejected_retryable` can be retried. `rejected` and `blocked` are final.

```mermaid theme={null}
sequenceDiagram
    participant U as Applicant
    participant A as Your app
    participant B as Your backend
    participant P as Pods API
    participant H as Hosted page
    A->>B: Start KYC
    B->>P: POST /api/v1/kyc/hosted/sessions
    P-->>B: kycUserId + verificationLink
    B-->>A: verificationLink
    A->>U: Open the link
    U->>H: Verify (document, selfie, data)
    H-->>U: Redirect to your redirectUrl
    loop every ~5 s
        B->>P: GET /api/v1/kyc/status
        P-->>B: status
    end
    P-->>B: approved, brlaEnabled true
```

## 1. Create a session

`POST /api/v1/kyc/hosted/sessions`

```bash theme={null}
curl -X POST https://api.pods.finance/api/v1/kyc/hosted/sessions \
  -H "x-api-key: $PODS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "cpf": "52998224725",
    "email": "user@example.com",
    "walletAddress": "0x0000000000000000000000000000000000000001",
    "externalUserId": "app-user-001",
    "redirectUrl": "https://app.example.com/kyc/return"
  }'
```

| Field | Required | Notes |
| - | - | - |
| `cpf` | Yes | Digits, with or without punctuation. Validated before anything else happens. |
| `email` | Yes | The applicant's email. |
| `walletAddress` | Yes | Checksummed EVM address or Solana public key. One wallet belongs to one KYC profile of your account. |
| `externalUserId` | No | Your own id, kept for support correlation. Nothing is looked up by it. |
| `redirectUrl` | No | Where the applicant returns after a successful verification. Must be HTTPS, carry no credentials, and match an entry of your allowlist. The query string is kept; the fragment is dropped. When omitted, the first allowlisted URL is used. |

```json theme={null}
{
  "kycUserId": "550e8400-e29b-41d4-a716-446655440000",
  "status": "created",
  "requiresWebView": true,
  "verificationLink": "https://in.sumsub.com/websdk/p/example",
  "verificationLinkExpiresAt": "2026-10-19T18:30:00.000Z"
}
```

Store `kycUserId`: it is the only handle you need for status polling and retries. The CPF is never returned.

**The call is idempotent by CPF.** Calling it again for the same CPF returns the same `kycUserId`. You get the same link while it still has more than five minutes left, and a fresh link after that. It is safe to call whenever your app needs the link again.

> [!WARNING]
>
> `verificationLink` is a bearer link: anyone who has it can use the verification. Do not log it, and hand it only to the applicant.

If you already verified this person, through this flow or another Pods KYC flow, the response is that profile, with `status: "approved"` and `verificationLink: null`: there is nothing to open. To add a wallet, call `POST /api/v1/kyc/{kycUserId}/wallets` with the `kycUserId` returned. A person verified only by another Pods customer goes through the verification like anyone else. A profile under analysis, rejected, or blocked also returns no link.

## 2. Open the link and return to the app

Open `verificationLink` for the applicant. It works until `verificationLinkExpiresAt`. After a successful verification the applicant is redirected to your `redirectUrl`.

> [!WARNING]
>
> The redirect is only a hint that the applicant finished. It is not a verdict, and it may never happen (the applicant closes the tab, loses connection, or finishes on a page opened after the link expired). Always read the result from `GET /api/v1/kyc/status`.

A link that expired does not end the verification. The applicant stays the same: call the session endpoint again for a new link, and keep polling the status.

## 3. Poll the status

`GET /api/v1/kyc/status?kycUserId=550e8400-e29b-41d4-a716-446655440000`

Start polling as soon as you show the link, not after the applicant clicks, and poll about every 5 seconds while they are on the hosted page. That way you notice the result even when the redirect never reaches your app.

| `status` | What it means | What your app does |
| - | - | - |
| `created` | The applicant has not finished. While `verificationLink` is present it still opens. | Show the link and keep polling. If `verificationLink` is `null`, the link expired: call `POST /api/v1/kyc/hosted/sessions` again. |
| `provider_pending` | The verification is under analysis. | Stop offering the link, show "under analysis", keep polling. |
| `approved` | Verified. `brlaEnabled` is `true` once Pix is unlocked. | Continue to [Pods Ramp](https://docs.pods.finance/guides/ramp/quotes.md). |
| `rejected_retryable` | The applicant can try again. `reason` says why. | Offer a retry (see [Retry or reset](#4-retry-or-reset)). |
| `rejected` | Final. `reason` says why. | Show the message. Do not retry. |
| `blocked` | Final. | Show the message. Do not retry. |

A `provider_pending` profile with `providerResult: KYC_PROVIDER_IMPORT_FAILED` passed the verification, but Pods could not finish the onboarding yet. Keep showing "under analysis": Pods retries transient failures and takes the rest to support. Include the `kycUserId` if you contact support.

While `created` or `rejected_retryable`, the status carries `verificationLink` and `verificationLinkExpiresAt`. They are `null` otherwise.

`reason` is present for `rejected` and `rejected_retryable`:

```json theme={null}
{
  "status": "rejected_retryable",
  "brlaEnabled": false,
  "reason": {
    "code": "IMAGE_QUALITY_INSUFFICIENT",
    "retryable": true,
    "retryFrom": "documents",
    "message": "The images were not clear enough. Retake them in even lighting, in focus and uncropped."
  }
}
```

Program against `reason.code` and `reason.retryable`; show `reason.message` to the applicant. The codes are the same Pods vocabulary used by the other KYC flows: see [rejection reasons](https://docs.pods.finance/guides/kyc/external-evidence.md#handling-rejections).

## 4. Retry or reset

There are two ways to restart a `rejected_retryable` verification. Choose by what the applicant has to fix.

| You want | Call | What the applicant sees |
| - | - | - |
| Let the applicant fix **only the rejected step** (a blurry photo, a new selfie) | `POST /api/v1/kyc/hosted/sessions` again with the same CPF | A new link that reopens only the step that was rejected. |
| Let the applicant **correct data they already submitted** (for example the address) | `POST /api/v1/kyc/hosted/reset` with the `kycUserId` | A new link that starts over from the first step. Data already given stays filled in and editable. |

```bash theme={null}
curl -X POST https://api.pods.finance/api/v1/kyc/hosted/reset \
  -H "x-api-key: $PODS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kycUserId": "550e8400-e29b-41d4-a716-446655440000",
    "redirectUrl": "https://app.example.com/kyc/return"
  }'
```

Both return the same shape as the session response, with a new `verificationLink`. A reset keeps the same `kycUserId`. Reset is allowed only while the status is `rejected_retryable`, and only a limited number of times per profile.

After either call, keep polling the status as before.

## 5. Errors

Errors use `{ "error": { "code", "message", "details?" } }`. Full list with resolutions: [Error codes](https://docs.pods.finance/error-codes.md#kyc-hosted-session-errors).

| Code | HTTP | When |
| - | - | - |
| `HOSTED_KYC_NOT_ENABLED` | 403 | Hosted KYC is not enabled for your account |
| `INVALID_CPF` | 422 | The CPF is not valid |
| `KYC_REDIRECT_URL_NOT_ALLOWED` | 422 | `redirectUrl` is not on your allowlist, is not HTTPS, carries credentials, or is longer than 1024 characters |
| `KYC_WALLET_ALREADY_LINKED` | 409 | The wallet belongs to another KYC profile of your account |
| `KYC_PROFILE_CHANGED` | 409 | The profile changed while the request ran. Retry |
| `KYC_RESET_NOT_ALLOWED` | 409 | The profile is not `rejected_retryable`, or the rejection cannot be reset |
| `KYC_RESET_LIMIT_REACHED` | 409 | The profile used all its resets |
| `HOSTED_KYC_REQUEST_FAILED` | 502 | The verification provider did not answer. Retry after a short delay |
| `HOSTED_KYC_PROVIDER_CREDITS_EXHAUSTED` | 503 | The verification provider is temporarily unavailable. Retry later, or contact support |

A `502` or `503` means the provider could not complete the call: it is safe to call again.

## Data handling

* **Documents and selfie** are captured and held by the verification provider on the hosted page. They never pass through your servers, and Pods does not store them.
* **CPF:** Pods keeps a salted hash and the last four digits. The full CPF is not returned by any route.
* **Link:** a short-lived bearer link. Treat it as a secret.
* **Result:** Pods keeps the status, the normalized rejection reason, and the identifiers needed for audit and retry.

## Next steps

> [card] [KYC overview](https://docs.pods.finance/guides/kyc/overview.md)
>
> All KYC flows and the shared status model

> [card] [Move money with Pods Ramp](https://docs.pods.finance/guides/ramp/quotes.md)
>
> Quote and execute Pix on-ramp and off-ramp once approved

> [card] [Backend example](https://docs.pods.finance/examples/javascript/kyc-backend.md)
>
> Runnable script: create a session, print the link, poll the status

> [card] [Error codes](https://docs.pods.finance/error-codes.md#kyc-hosted-session-errors)
>
> Every code with its resolution


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