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

# WhatsApp channel and PIN confirmation

> Let your users deposit, register bank accounts and withdraw from a WhatsApp conversation, with their own session, OTP and PIN.

This guide is for companies that want their own users to deposit, register bank accounts and withdraw from the company's WhatsApp line. You build and run the WhatsApp bot; Kravata provides these endpoints and keeps the user's OTP and PIN checks.

Your WhatsApp server can act **on behalf of one end user at a time**, with that user's own session. Kravata never trusts the server to say who the user is:

* Your server only says which WhatsApp number is writing (`waId`). Kravata finds the user with that phone in your company and sends an **OTP to the user's own phone (SMS) and email**. Only the code the user types back opens a session.
* The **PIN never goes through your server**. The user types it in a WhatsApp Flow whose data endpoint is Kravata.
* With the user's token, your server calls the same endpoints as the Kravata widget, so the same rules apply: your company's OTP and PIN settings, KYC, tiers and limits.

| Environment | Channel endpoints (`/api/v1/channel/*`) | User endpoints (user token) |
| - | - | - |
| Test | `https://test-api-kore.kravata.co` | `https://test-api-kore.kravata.co` |
| Production | `https://partners-api.kravata.co`, with your **mTLS client certificate** | `https://api-kore.kravata.co` |

In production, the channel token and session calls go through the partners domain and need the same client certificate as the rest of your partner API: see [Production access](https://docs-api.kravata.co/stack/authentication.md#production-access-mtls-and-ip-allowlist). The calls made with the user token (PIN, deposit, accounts, withdrawal) go to `api-kore`, like the Kravata widget. The examples below use the test domain.

> [!NOTE]
>
> The channel is enabled per company. Ask your Kravata point of contact to enable it and to issue your **channel credential**.

## Credentials and tokens

You work with two kinds of token:

| Token | How you get it | What it is for |
| - | - | - |
| **Channel token** | `POST /api/v1/channel/token` with your channel credential (`apiKey` that starts with `kmsk-channel-`, and `secretKey`) | Only the `/api/v1/channel/whatsapp/session/*` endpoints. |
| **User token** | Returned when the user verifies the OTP (step 2 below) | Everything the user does: PIN, deposit, accounts, withdrawal. |

```bash theme={"dark"}
curl -X POST https://test-api-kore.kravata.co/api/v1/channel/token \
  -H "Content-Type: application/json" \
  -d '{"apiKey": "kmsk-channel-whatsapp-...", "secretKey": "YOUR_SECRET_KEY"}'
```

```json theme={"dark"}
{ "accessToken": "eyJ...", "expiresIn": 3600, "tokenType": "Bearer" }
```

> [!WARNING]
>
> Send the **user token** in both headers on every user call: `Authorization: Bearer <token>` **and** `Cookie: accessToken=<token>`. Some routes check one header at the gateway and some check the other.

## 1. Open a session for the user

**1. Request the OTP**

When a number writes to you and you have no valid session for it, call `POST /api/v1/channel/whatsapp/session/request` with your **channel token**. `waId` is the number as WhatsApp sends it (country code, no `+`).

```bash theme={"dark"}
curl -X POST https://test-api-kore.kravata.co/api/v1/channel/whatsapp/session/request \
  -H "Authorization: Bearer CHANNEL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"waId": "573001234567"}'
```

```json theme={"dark"}
{ "challengeId": "7d3c2c4e-...", "channels": ["sms", "email"], "expiresIn": 300 }
```

Tell the user that a code was sent by SMS and email. Kravata never sends this code through WhatsApp.

**2. Verify the code the user types**

```bash theme={"dark"}
curl -X POST https://test-api-kore.kravata.co/api/v1/channel/whatsapp/session/verify \
  -H "Authorization: Bearer CHANNEL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"waId": "573001234567", "challengeId": "7d3c2c4e-...", "otpCode": "482915"}'
```

```json theme={"dark"}
{
  "accessToken": "eyJ...",
  "refreshToken": "eyJ...",
  "expiresIn": 300,
  "refreshExpiresIn": 1800,
  "userId": "01c972c9-0447-4159-976a-23a4eda81972"
}
```

Store the tokens for that `waId`. A new session for a number ends the previous one.

**3. Keep the session alive**

Before the access token expires, call `POST /api/v1/channel/whatsapp/session/refresh` with `{"waId", "refreshToken"}`. A session can be refreshed for at most **7 days** after it was opened; after that, repeat step 1.

**4. End the session**

When the user leaves (for example, writes "salir"), call `POST /api/v1/channel/whatsapp/session/revoke` with `{"waId"}`. It ends every channel session of that number.

Errors of these endpoints have the shape `{"error": {"code": "...", "message": "..."}}`:

| Status | Code | What to do |
| - | - | - |
| 400 | `INVALID_WA_ID` | The `waId` is not a phone number. |
| 403 | `CHANNEL_DISABLED` | The channel is not enabled for your company. |
| 404 | `USER_NOT_FOUND` | No user has this phone in your company: start the registration (section 2). |
| 409 | `AMBIGUOUS_PHONE` | More than one user has this phone. Contact Kravata support; the number can't be used until it is fixed. |
| 429 | `OTP_RESEND_COOLDOWN` / `TOO_MANY_OTP_REQUESTS` | At most one code every 30 seconds and 5 per hour per number. |
| 502 | `OTP_DELIVERY_FAILED` | The code could not be sent by SMS nor email. |
| 401 | `OTP_INVALID` | Wrong code; the message says how many attempts are left (3 per code). |
| 401 | `OTP_EXPIRED` | The code expired (5 minutes): request a new one. |
| 429 | `OTP_LOCKED` | Too many wrong codes: request a new one. |
| 404 | `CHALLENGE_NOT_FOUND` | Unknown or already used `challengeId`. |
| 401 | `SESSION_NOT_FOUND` / `SESSION_EXPIRED` | The session was revoked, belongs to another number or is older than 7 days: repeat step 1. |

## 2. Registration of new users

> [!WARNING]
>
> **To be defined.** When `session/request` answers `USER_NOT_FOUND`, the user must register and complete KYC outside the chat. The registration link for this channel is still being defined with each company; today users are registered through your own integration ([Users](https://docs-api.kravata.co/stack/users.md)).

## 3. Terms and conditions

Users accept Kravata's terms and data policy on Kravata's terms page. Your integration gets the link with `POST /api/v1/client/terms-acceptance/generate-link` (`{"clientId", "userId"}`, with your partner API token) and sends it to the user.

## 4. PIN setup

The PIN is created and recovered **only in the Kravata widget**, never in the chat. With the user token you can check whether the user already has one:

```bash theme={"dark"}
curl https://test-api-kore.kravata.co/api/v1/pin/status \
  -H "Authorization: Bearer USER_TOKEN" -H "Cookie: accessToken=USER_TOKEN"
```

```json theme={"dark"}
{ "pinSet": true, "pinAttemptsLeft": 5, "pinLockedUntil": null,
  "securityQuestionsAttemptsLeft": 3, "securityQuestionsLockedUntil": null }
```

If your company requires the PIN and `pinSet` is `false`, send the user to the widget to create it.

## 5. Deposit

**1. Get the user's wallet**

`GET /api/v1/custodian/kravata-wallets` returns the user's wallets (`items`). Use `items[0].walletId` as `accountId` in the deposit; its `balances` show the user's balance.

**2. (PSE) Get the banks**

`GET /api/v1/banks/get-banks-cashin` returns `[{"id", "name", "achcode", "status", ...}]`.

**3. Create the deposit**

```bash theme={"dark"}
curl -X POST https://test-api-kore.kravata.co/api/v1/operations/deposit \
  -H "Authorization: Bearer USER_TOKEN" -H "Cookie: accessToken=USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 100000,
    "accountId": "WALLET_ID",
    "paymentMethod": "PSE",
    "currency": "COP",
    "bankCode": "ACHCODE_OF_THE_BANK",
    "bankName": "BANCOLOMBIA",
    "executionMode": "NORMAL"
  }'
```

`bankCode` is the bank's `achcode`. With `executionMode: "NORMAL"`, `amount` is the COP amount the user pays; with `"INVERSE"`, it is the amount of the wallet's token the user wants to receive.

The response is `{"operation": {...}, "tierEscalation": null, ...}`. For PSE, send the user `operation.depositInstructions.pseLink`. If `tierEscalation` is present and `operation` is null, the user must complete a tier upgrade first (`tierEscalation.actionType` / `actionPayload`).

## 6. Register a bank account

**1. Get the banks**

`GET /api/v1/banks/get-banks` returns `[{"id", "code", "name", "status"}]`.

**2. Confirm with the PIN (if your company requires it)**

See [PIN confirmation](#pin-confirmation) with `purpose: "account_creation"`.

**3. Create the account**

Use the **same user token** within 120 seconds of the PIN confirmation.

```bash theme={"dark"}
curl -X POST https://test-api-kore.kravata.co/api/v1/accounts \
  -H "Authorization: Bearer USER_TOKEN" -H "Cookie: accessToken=USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Mi cuenta",
    "type": "SAVING",
    "number": "123456789",
    "currency": "COP",
    "bankId": 7,
    "accountOwnerType": "OWN"
  }'
```

`type` is `SAVING` or `CHECKING`. For a third party's account use `accountOwnerType: "PERSON"` with `holderIdentificationType`, `holderIdentificationNro`, `holderFirstName` and `holderFirstSurname` (or `"COMPANY"` with `holderBusinessName`). For a Bre-B key: `{"name", "type": "SAVING", "number": "<key>", "currency": "COP", "paymentMethod": "BREB"}`.

`GET /api/v1/accounts` lists the user's accounts (`items`). Editing an account (`PATCH /api/v1/accounts/{id}`) also asks for the PIN, with `purpose: "account_update"`.

## 7. Withdrawal

**1. Choose the destination account**

`GET /api/v1/accounts` → the `id` of the account.

**2. Confirm with the PIN (if your company requires it)**

See [PIN confirmation](#pin-confirmation) with `purpose: "withdrawal"`. Each withdrawal needs its own confirmation.

**3. Create the withdrawal**

```bash theme={"dark"}
curl -X POST https://test-api-kore.kravata.co/api/v1/operations/withdrawal \
  -H "Authorization: Bearer USER_TOKEN" -H "Cookie: accessToken=USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 25,
    "originId": "WALLET_ID",
    "destinationId": "ACCOUNT_ID",
    "paymentMethod": "BANK_TRANSFER",
    "currency": "COP",
    "executionMode": "NORMAL"
  }'
```

With `executionMode: "NORMAL"`, `amount` is the amount of the wallet's token (for example USDC) the user sells; with `"INVERSE"`, it is the COP amount the user wants to receive. Use `paymentMethod: "BREB"` for a Bre-B account.

The response has the same shape as the deposit; `operation.prefix` + `operation.numberCode` identify the withdrawal for the user.

## Operation OTP in the channel

If your company requires an OTP for deposits or withdrawals, the OTP the user typed to open the channel session covers it **while the user is active**: their last OTP or PIN in that session must be at most **30 minutes** old. Your server's own calls (refresh, reads) don't count as activity. After 30 minutes without activity, deposits and withdrawals are rejected with `403`: open a new session (section 1) and retry.

## PIN confirmation

When your company requires the PIN for an operation, the user confirms it in a **WhatsApp Flow** whose data endpoint is Kravata. Your server never receives the PIN.

**1. Create the PIN challenge**

With the **user token**, call `POST /api/v1/pin/challenge`. `summary` is the text the user sees in the Flow (up to 160 characters). `purpose` is `withdrawal`, `account_creation` or `account_update`.

```bash theme={"dark"}
curl -X POST https://test-api-kore.kravata.co/api/v1/pin/challenge \
  -H "Authorization: Bearer USER_TOKEN" -H "Cookie: accessToken=USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"purpose": "withdrawal", "summary": "Retiro de 100.000 COP a Bancolombia ****4321"}'
```

```json theme={"dark"}
{ "flowToken": "Yq3...", "expiresIn": 300 }
```

If the user has no PIN, the answer is `404`: send them to the widget to create it.

**2. Send the WhatsApp Flow**

Send your PIN Flow with `flow_token` = `flowToken`. Its data endpoint is `POST /api/v1/channel/whatsapp/flows/pin/{clientId}` (your `clientId`), on `https://api-kore.kravata.co` in production: Meta calls it, so it doesn't use mTLS.

**3. Wait for the Flow to finish**

When the PIN is right, the Flow ends with `pin_validated: true`. Then send the operation **with the same user token within 120 seconds**. A confirmation authorizes one operation only.

**Flow contract.** Build your Flow with:

* One screen with id `PIN`. Kravata sends it `data.summary` and, after a wrong PIN, `data.error_message` (for example "PIN incorrecto. Te quedan 4 intento(s).").
* A `pin` field submitted with `data_exchange`.
* Completion on `SUCCESS` with `params`: `flow_token`, `pin_validated` and `purpose`.

**Keys.** Every WhatsApp Business Account uses its own key pair for Flows. Kravata keeps the private key and gives you the public key to upload to your WABA; you give Kravata the app secret Meta uses to sign the calls to the data endpoint. Kravata answers `432` to a bad signature, `421` when it can't decrypt (Meta then re-fetches the public key) and `427` when the `flow_token` is unknown, used or expired.

## When an operation is rejected

Deposits, withdrawals and account changes rejected by the gateway answer `403` without a reason. Check, in this order:

1. The PIN confirmation was done with **this same user token**, less than 120 seconds ago, and was not already used.
2. The user has a PIN (`GET /api/v1/pin/status`).
3. The user was active in the last 30 minutes (operation OTP); otherwise open a new session.


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