# One-time codes (OTP)

> Docs index: https://wahasend.com/llms.txt · Everything in one file: https://wahasend.com/docs.md · API base URL: https://app.wahasend.com/api/v1

Send a verification code by WhatsApp and check what the person typed. Codes are random, stored hashed, valid 5 minutes, 3 tries. Comes with **Developer** (feature `otp`).

<a id="otp-send"></a>

### Send a code

`POST /api/v1/otp/send`

Generates a code and sends it right away (no queue). Keep `requestId` for the check.

- Unlocked by: Developer (`otp`)
- Per-number key: yes

> Limits, so nobody can flood a phone through your app (see [OTP limits](https://wahasend.com/docs/errors.md#otp-limits)): to one phone, one code every 30 seconds, 3 per 10 minutes and 10 per 24 hours; 60 codes a minute for your whole organization. Over a limit: 429 `OTP_RATE_LIMITED` with `Retry-After` and `details` `{ scope: "phone" | "organization", reason: "cooldown" | "window" | "day" | "organization", limit, retryAfterSeconds }`. A code counts when it is sent, even if WhatsApp then refuses it.

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phone` | string | yes | International phone number, 8–20 characters (`+22212345678`). `@lid` ids are not accepted here. |
| `template` | string | no | Your text, up to 500 characters. Must contain `{{code}}` (else 400 `OTP_TEMPLATE_MISSING_CODE`): every `{{code}}` becomes the code and every `{{expiry}}` the minutes. Default: "Your verification code is: *{{code}}* …". |
| `length` | integer | no | Digits, 4–8. Default 6. |
| `containerId` | uuid | no | Which of your WhatsApp numbers to use; it always wins. Default: your default number (else any connected one), or a number of the group when [several numbers](https://wahasend.com/docs/phone-numbers.md#several-numbers) is on. Ignored with a per-number key, which always uses its own number. |

**Example**

curl:

```bash
curl -X POST https://app.wahasend.com/api/v1/otp/send \
  -H "X-API-Key: $WAHASEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "phone": "+22212345678",
  "template": "Your Kaba Shop code: {{code}}. It expires in {{expiry}} minutes.",
  "length": 6
}'
```

JavaScript:

```js
const res = await fetch('https://app.wahasend.com/api/v1/otp/send', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "phone": "+22212345678",
    "template": "Your Kaba Shop code: {{code}}. It expires in {{expiry}} minutes.",
    "length": 6
  }),
});
const data = await res.json();
```

Python:

```python
import os
import requests

r = requests.post(
    "https://app.wahasend.com/api/v1/otp/send",
    headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
    json={
        "phone": "+22212345678",
        "template": "Your Kaba Shop code: {{code}}. It expires in {{expiry}} minutes.",
        "length": 6,
    },
)
print(r.json())
```

**Response 201**

```json
{
  "success": true,
  "requestId": "0b8e4d2a-7c1f-4a95-b3e6-9d2c5f8a1e47",
  "phone": "+22212345678",
  "expiresAt": "2026-09-30T10:05:00.000Z",
  "expiresInSeconds": 300
}
```

**Errors to expect**

- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`OTP_TEMPLATE_MISSING_CODE`](https://wahasend.com/docs/errors.md#otp_template_missing_code) (400): The message template must contain {{code}}, where the code goes.
- [`OTP_RATE_LIMITED`](https://wahasend.com/docs/errors.md#otp_rate_limited) (429): Too many verification codes for this number or this account. Wait before sending another one.
- [`NO_SESSION_CONFIGURED`](https://wahasend.com/docs/errors.md#no_session_configured) (400): This account has no connected WhatsApp number.
- [`WAHA_ERROR`](https://wahasend.com/docs/errors.md#waha_error) (502): WhatsApp could not complete the request. Try again.
- [`WAHA_SESSION_ERROR`](https://wahasend.com/docs/errors.md#waha_session_error) (503): WhatsApp is not connected.

<a id="otp-verify"></a>

### Check a code

`POST /api/v1/otp/verify`

Answers `verified: true` once. A wrong code uses one of the 3 tries.

- Unlocked by: Developer (`otp`)
- Per-number key: yes

> A wrong code answers 400 `OTP_INVALID_CODE` with `details.attemptsRemaining`.

**Request body**

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `requestId` | uuid | yes | From the send. |
| `code` | string | yes | What the person typed, 4–8 characters. |

**Example**

curl:

```bash
curl -X POST https://app.wahasend.com/api/v1/otp/verify \
  -H "X-API-Key: $WAHASEND_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "requestId": "0b8e4d2a-7c1f-4a95-b3e6-9d2c5f8a1e47",
  "code": "482913"
}'
```

JavaScript:

```js
const res = await fetch('https://app.wahasend.com/api/v1/otp/verify', {
  method: 'POST',
  headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    "requestId": "0b8e4d2a-7c1f-4a95-b3e6-9d2c5f8a1e47",
    "code": "482913"
  }),
});
const data = await res.json();
```

Python:

```python
import os
import requests

r = requests.post(
    "https://app.wahasend.com/api/v1/otp/verify",
    headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
    json={
        "requestId": "0b8e4d2a-7c1f-4a95-b3e6-9d2c5f8a1e47",
        "code": "482913",
    },
)
print(r.json())
```

**Response 200**

```json
{
  "success": true,
  "verified": true,
  "phone": "+22212345678"
}
```

**Errors to expect**

- [`OTP_INVALID_CODE`](https://wahasend.com/docs/errors.md#otp_invalid_code) (400): Invalid code.
- [`OTP_EXPIRED`](https://wahasend.com/docs/errors.md#otp_expired) (410): OTP has expired. Request a new code.
- [`OTP_MAX_ATTEMPTS`](https://wahasend.com/docs/errors.md#otp_max_attempts) (429): Maximum verification attempts exceeded. Request a new code.
- [`OTP_ALREADY_VERIFIED`](https://wahasend.com/docs/errors.md#otp_already_verified) (400): OTP has already been verified.
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
