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

# Recipe: KYC → INR Payout

> End-to-end pool_settlement flow: onboard a user, verify KYC (including foreign-ID users), check the escrow balance, and disburse INR.

A complete walk-through of the **pool settlement** integration: you fund a central pool, StablePay allocates INR to your escrow balance, and you disburse INR to your users. The request/response shapes are **identical** in sandbox and production — only the base URL and API key prefix change.

<Note>
  This recipe covers the **`pool_settlement`** flow. The **escrow balance** and **payout** steps require `pool_settlement` enabled on your account. The KYC steps apply to any flow.
</Note>

|                | Base URL                               | API key        |
| -------------- | -------------------------------------- | -------------- |
| **Sandbox**    | `https://sandbox-api.stablepay.global` | `pk_sandbox_…` |
| **Production** | `https://api.stablepay.global`         | `pk_live_…`    |

Every request carries your key: `Authorization: Bearer <API_KEY>`

## Onboard & verify KYC

<Steps>
  <Step title="Create the user">
    No OTP / mobile verification is required to proceed with KYC.

    ```bash theme={null}
    POST /v2/users
    ```

    ```json Request theme={null}
    {
      "name": "John Doe",
      "mobile": "+919876543210",
      "email": "john.doe@example.com",
      "externalId": "your-ref-123"
    }
    ```

    ```json Response · 201 theme={null}
    {
      "success": true,
      "data": { "userId": "usr_abc123", "kycStatus": "pending" }
    }
    ```

    Keep the `userId` — every KYC and payout call is scoped to it.
  </Step>

  <Step title="Verify PAN">
    The mandatory identity anchor. Real government-DB check in production; auto-verified on any valid-format PAN in sandbox.

    ```bash theme={null}
    POST /v2/users/{userId}/kyc/pan/verify
    ```

    ```json Request theme={null}
    { "panNumber": "ABCDE1234F", "name": "John Doe" }
    ```

    ```json Response theme={null}
    {
      "success": true,
      "data": { "verified": true, "name": "JOHN DOE", "maskedPan": "XXXXX1234X", "nameMatchScore": 100 }
    }
    ```
  </Step>

  <Step title="Submit KYC (foreign ID)">
    Share the identity document you collected — e.g. a foreign driving-license number for an NRI with no Aadhaar/passport. This satisfies the identity requirement and the declaration in one call.

    ```bash theme={null}
    POST /v2/users/{userId}/kyc/submit
    ```

    ```json Request theme={null}
    {
      "legalName": "John Doe",
      "country": "USA",
      "proofOfIdentity": {
        "docType": "driving_license",
        "number": "DL1234567",
        "issuingCountry": "USA"
      },
      "dob": "1990-05-15",
      "gender": "male",
      "incomeRange": "10l_to_25l",
      "isPep": false
    }
    ```

    ```json Response theme={null}
    {
      "success": true,
      "data": { "submitted": true, "kycStatus": "verified", "kycLevel": "basic" }
    }
    ```

    <Note>
      A `403 "Partner-attested KYC is not enabled"` means the endpoint is live but not enabled for your account — contact StablePay. **Indian users** can skip this step and verify with Aadhaar (DigiLocker) or passport instead.
    </Note>
  </Step>

  <Step title="Verify bank account">
    The payout destination — always required. Penny-drop in production; auto-verified in sandbox.

    ```bash theme={null}
    POST /v2/users/{userId}/kyc/bank/verify
    ```

    ```json Request theme={null}
    {
      "accountNumber": "1234567890",
      "ifsc": "HDFC0001234",
      "accountHolderName": "John Doe"
    }
    ```

    ```json Response theme={null}
    {
      "success": true,
      "data": { "verified": true, "bankName": "HDFC Bank",
                "maskedAccountNumber": "XXXXXX7890", "ifsc": "HDFC0001234" }
    }
    ```
  </Step>
</Steps>

<Check>
  **KYC is now verified.** Status flips to `verified` once all three identity legs are complete — order doesn't matter, each call recomputes the status:

  `verified` = **PAN** + **identity** (foreign DL via `/kyc/submit`, *or* Aadhaar / passport) + **bank**

  The user reaches `verified` only after **all three** — not just after `/kyc/submit`.
</Check>

### Check KYC status (poll until verified)

Poll to confirm the user reached `verified`. The `attestation` block reflects the foreign-ID submission.

```bash theme={null}
GET /v2/users/{userId}/kyc/status
```

```json Response theme={null}
{
  "success": true,
  "data": {
    "level": "basic",
    "status": "verified",
    "pan":  { "verified": true, "maskedNumber": "XXXXX1234X" },
    "bank": { "verified": true, "bankName": "HDFC Bank" },
    "attestation": {
      "verified": true, "docType": "driving_license",
      "country": "USA", "issuingCountry": "USA",
      "legalName": "John Doe", "verifiedAt": "2026-01-01T10:00:00Z"
    },
    "aadhaar": { "verified": false },
    "passport": { "verified": false }
  }
}
```

A method that isn't complete returns `{ "verified": false }`. Treat an absent `verified` key as `false`.

## Disburse INR (pool settlement)

<Info>
  The steps below require **`pool_settlement`** enabled on your account. Your INR escrow is funded when StablePay allocates INR against your settled USDT.
</Info>

<Steps>
  <Step title="Check escrow balance">
    Your available INR to disburse — the same balance shown in the Client Portal.

    ```bash theme={null}
    GET /v2/escrow/balance
    ```

    ```json Response theme={null}
    { "success": true, "data": { "usdt": "0.00", "inr": "500000.00" } }
    ```

    `inr` is what you can pay out. A larger payout fails with `INSUFFICIENT_BALANCE`.
  </Step>

  <Step title="Trigger the payout">
    A single call — debits your escrow and pays the user's verified bank over IMPS. The `Idempotency-Key` header is **required**, and the user must be KYC-verified with a verified bank.

    ```bash theme={null}
    POST /v2/transactions
    ```

    ```json Request theme={null}
    // Header: Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
    {
      "type": "pool_settlement",
      "userId": "usr_abc123",
      "amountInr": "100000",
      "partnerReference": "disbursal_789"
    }
    ```

    ```json Response theme={null}
    {
      "success": true,
      "data": {
        "transactionId": "tx-uuid",
        "status": "payout_processing",
        "type": "pool_settlement",
        "amountInr": "100000.00",
        "settlementInr": "99000.00",
        "tds": { "applicable": true, "percent": "1", "amountInr": "1000.00" },
        "recipient": { "bank": "HDFC Bank", "account": "XXXXXX7890" }
      }
    }
    ```

    <Warning>
      A `200` means the bank rail **accepted** the transfer, not that it settled. Confirm the final outcome via the status endpoint or webhook below. When TDS is off, `settlementInr` equals `amountInr`.
    </Warning>
  </Step>

  <Step title="Check the payout status">
    Poll the transaction for its final state and UTR. The path accepts **either** the StablePay `transactionId` **or** your own `partnerReference` — so if a create call timed out and you never received the `transactionId`, look it up by the reference you sent.

    ```bash theme={null}
    GET /v2/transactions/{transactionId}      # or /v2/transactions/{partnerReference}
    ```

    ```json Response theme={null}
    {
      "success": true,
      "data": {
        "transaction": { "status": "completed" },
        "payout": {
          "status": "SUCCESS",         // SUCCESS | DEEMED | FAILED | PROCESSING
          "amountInr": "99000.00",
          "utr": "601813168575",
          "rail": "imps"
        }
      }
    }
    ```

    `transaction.status` is `payout_processing` until settled, then `completed` (or `failed`). `payout.status` carries the rail result — `SUCCESS` (with a UTR), `DEEMED` (bank-accepted, no UTR yet), or `FAILED` (escrow debit reversed).
  </Step>
</Steps>

## Webhooks (recommended over polling)

Instead of polling, register a webhook URL to receive the payout outcome in real time. See the [Webhooks guide](/guides/webhooks) for setup and signature verification.

You'll receive, in order:

| Event                          | Meaning                                        |
| ------------------------------ | ---------------------------------------------- |
| `transaction.payout_initiated` | The bank rail accepted the transfer            |
| `transaction.payout_completed` | INR delivered — carries the UTR                |
| `transaction.payout_failed`    | Transfer failed — the escrow debit is reversed |

```json transaction.payout_completed theme={null}
{
  "event": "transaction.payout_completed",
  "timestamp": "2026-01-01T10:30:00.000Z",
  "partnerId": "your_partner_id",
  "data": {
    "transactionId": "tx-uuid",
    "amount": 99000,
    "currency": "INR",
    "status": "SUCCESS",
    "utr": "601813168575",
    "receiver_name": "JOHN DOE",
    "bank_account_masked": "XXXXXX7890",
    "completedAt": "2026-01-01T10:30:00.000Z"
  }
}
```

## Sandbox vs Production

| Aspect                    | Sandbox                                                      | Production                                      |
| ------------------------- | ------------------------------------------------------------ | ----------------------------------------------- |
| PAN / bank / KYC          | Auto-verified on valid-format input                          | Real verification via provider                  |
| Escrow INR                | Pre-funded by StablePay for testing                          | Allocated by StablePay ops against settled USDT |
| Payout                    | Settles instantly, synthetic UTR; no real money              | Real IMPS transfer to the user's bank           |
| Request / response shapes | Identical — build on sandbox, then switch the base URL + key |                                                 |

## Sandbox test cues

* Drive payout outcomes by the **cents** of `amountInr`: normal cents settle; ending in `.11` forces a **failure**; `.22` forces a **DEEMED** (bank-accepted, no UTR).
* OTP, where used elsewhere, is always `123456` in sandbox. It is **not** required for this flow.
* To test a payout, your sandbox escrow must hold INR — ask StablePay to seed it (there is no self-serve funding endpoint).
* **TDS (§194S)** is a per-account setting. When on, it's withheld from the gross; the user receives `settlementInr` and your escrow is debited the full `amountInr`.
