# WahaSend API docs
> WahaSend sends and receives WhatsApp messages from your own number: single and batch sends, campaigns, reminders, OTP codes, media, signed webhooks, an inbox, and an MCP server for Claude and other AI agents. HTTPS + JSON API at https://app.wahasend.com/api/v1, key in the `X-API-Key` header.
> 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
## Getting started
WahaSend sends and receives WhatsApp messages from **your own WhatsApp number**. The API is plain HTTPS and JSON. Five minutes from zero to a first message:
1. [Create an account](https://wahasend.com/register) and pick a product. It starts with a free trial, and every product comes with an API key ([which routes each one reaches](https://wahasend.com/docs/authentication.md#auth-products)).
2. Link your WhatsApp number: dashboard → **WhatsApp**, scan the QR code with your phone (or use the pairing code on a phone).
3. Get your key: dashboard → **For developers** → **Generate key** (with Connect: **Connect** page). It is shown once; store it like a password.
4. Send your first message:
```bash
curl https://app.wahasend.com/api/v1/alert/send \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{"phone": "+22212345678", "message": "Hello from WahaSend"}'
```
Answer (201):
```json
{
"success": true,
"messageId": "4f0c6a1e-2b7d-4c1a-9e35-7d8f2a6b1c90",
"status": "queued",
"priority": "normal"
}
```
The message goes out a few seconds later with a typing indicator. Follow it with `GET /messages` or [webhook events](https://wahasend.com/docs/webhooks.md).
#### Base URL and format
- Every path below starts with `https://app.wahasend.com/api/v1`.
- Send JSON with `Content-Type: application/json` (10 MB max per request). Answers are JSON.
- Success answers carry `"success": true` (the [WhatsApp number](https://wahasend.com/docs/api/numbers.md) routes don't, for now). Errors always use [one envelope](https://wahasend.com/docs/errors.md).
- Times are ISO 8601 in UTC (`2026-09-30T10:30:12.000Z`). Ids are UUIDs.
- Every answer has an `X-Request-Id` header: quote it when you write to support.
#### What each product unlocks
Each endpoint below says which feature it needs. An account owns any mix of products and gets the features of all of them. Calling something your products don't include answers 403 `PRODUCT_REQUIRED` with `details.products`. Notify, Assistant and Shop let you add as many WhatsApp numbers as you need; Developer and Connect include 3, and more are extra numbers (see [WhatsApp numbers](https://wahasend.com/docs/api/numbers.md)).
| Product | Features | API key |
| --- | --- | --- |
| Notify | `send, batch, campaigns, reminders, api` | yes |
| Assistant | `send, bots, knowledge, api` | yes |
| Shop | `send, bots, commerce, api` | yes |
| Developer | `send, batch, api, otp, media, webhooks` | yes |
| Connect | `send, batch, inbox, mcp, api` | yes |
#### For AI agents
These docs also exist as Markdown, for coding agents (Claude Code, Cursor, Codex…) and anything that reads the web without running JavaScript: [llms.txt](https://wahasend.com/llms.txt) lists every page, [llms-full.txt](https://wahasend.com/llms-full.txt) has all of them in one file, and every page has its own `.md` address (for example [getting-started.md](https://wahasend.com/docs/getting-started.md); French under `/docs/fr/`). To let an AI app send and read WhatsApp messages itself, use [Connect](https://wahasend.com/docs/connect.md).
Coding agents can also install the WahaSend skill (what to call for what, webhook signatures, errors, keeping the number safe), and API tools can import [openapi.json](https://wahasend.com/openapi.json) (OpenAPI 3.1: Postman, Insomnia, SDK generators, GPT actions).
```bash
npx skills add ahmedEssyad/wahasendskills
```
A prompt for your agent:
```text
Read https://wahasend.com/llms.txt and the pages it links to, then add WhatsApp notifications to this project with the WahaSend API. My key is in the WAHASEND_API_KEY environment variable.
```
---
## Authentication
Send your key in the `X-API-Key` header on every request. Every product comes with API keys; a key reaches what your products include, nothing more (403 `PRODUCT_REQUIRED` otherwise).
```bash
curl https://app.wahasend.com/api/v1/me -H "X-API-Key: wn_8c1f…"
```
| Key | Looks like | Where | What it may call |
| --- | --- | --- | --- |
| Account key | `wn_…` | Dashboard → For developers (or the Connect page) | Everything your products include, as an organization **admin** (not team, ownership or deleting the organization). Pick a number with `containerId`. |
| Per-number key | `waha_…` | WhatsApp → the number → More settings | Sending and reading **through that number only**: `/alert/*`, `/otp/*`, `/recipients/*`, `/chats*`, `/contacts*`, `/bulk/*`, `/mcp`, and its own `/session/status\|qr\|pair\|start\|reconnect\|logout\|history\|profile`. Anything else answers 403 `NUMBER_KEY_SCOPE`. |
#### Which products can use the API
| Product | A key reaches |
| --- | --- |
| **Notify** | `/alert/send`, `/alert/batch`, `/alert/validate`, `/recipients/validate`, [campaigns](https://wahasend.com/docs/api/campaigns.md) `/bulk/*`, [reminders](https://wahasend.com/docs/api/reminders.md) `/reminders/*` |
| **Assistant** · **Shop** | `/alert/send`, the assistant routes `/bots/*` (Shop: catalogue and orders too), a [webhook](https://wahasend.com/docs/webhooks.md) for `bot.*` (and `commerce.order`) |
| **Developer** | Sending and batches, [OTP](https://wahasend.com/docs/api/otp.md) `/otp/*`, media sends, edit/delete/forward, every [webhook](https://wahasend.com/docs/webhooks.md) event |
| **Connect** | Sending and batches, [chats and contacts](https://wahasend.com/docs/api/inbox.md), the MCP endpoint `/mcp` |
Every product also reaches the [account and history](https://wahasend.com/docs/api/account.md) routes and the [WhatsApp number](https://wahasend.com/docs/api/numbers.md) routes. OTP, media sends and the full set of webhook events stay with Developer.
Give each app or server the smallest key it needs: a per-number key for a shop's checkout, the account key for your back office.
#### Bearer, for AI apps
Many MCP clients can only send `Authorization: Bearer …`. On the MCP endpoint (`/mcp`), `Authorization: Bearer YOUR_KEY` works exactly like `X-API-Key`. Elsewhere, use `X-API-Key`: `Bearer` there is for dashboard sessions.
#### Replacing a key
Regenerate a key from the same page (or `POST /me/regenerate-key`, `POST /session/:containerId/regenerate-key`). The old key stops working **at once**, so update your servers first if you can, then regenerate, then deploy the new key. There is one account key at a time. Keys are stored hashed: we can't show a lost key, only replace it.
> A key sends messages from your real number. Keep it on your server, never in a web page or a mobile app.
A deactivated account answers 403 `ACCOUNT_DEACTIVATED`; a missing or wrong key 401 `AUTH_ERROR` (`details.reason`: `missing_credentials` or `invalid_api_key`).
---
## Phone numbers and @lid
Write numbers in international format: country code then number, with or without `+`. Spaces, dashes and brackets are removed. `+222 12 34 56 78`, `22212345678` and `22212345678@c.us` are the same person. No leading `0` and no `00`.
#### Several numbers
WhatsApp can block a number. To keep going, an organization can send through several of its numbers (WhatsApp numbers page → More settings → Several numbers, or `PUT /session/send-group`). It is off by default: then everything works as before, from your default number.
- **Applies to** sends that name no number: `/alert/send`, `/alert/batch`, `/alert/send-*`, campaigns and reminders, from the dashboard or the account key. An explicit `containerId` or a per-number key always wins and never moves. Connect (Claude) and the assistants stay on their own number.
- **backup**: your default number, and the next one of the group when it is disconnected or may be blocked. **share**: messages spread over the group's connected numbers.
- **Same contact, same number** (on by default): a contact keeps getting messages from the same number while it works. If it goes down, the contact moves to another number of the group and stays there.
- A queued message whose number goes down before it is sent moves to a connected number of the group (never outside it). Something that may already have gone out is never sent again.
- Answers carry `containerId`: the number a message is on. `GET /messages` and the `message.*` [webhooks](https://wahasend.com/docs/webhooks.md) show the number that really sent it.
- Limits stay per number (and per organization): several numbers spread the load, they do not raise any limit. Using several numbers to get around a ban can get them banned too: this is for continuity, not for mass messaging.
#### WhatsApp ids ending in @lid
WhatsApp now hides many people's numbers behind a **Linked ID** such as `104937261508834@lid`. That is how their messages reach you, and it is a valid address: send to it exactly as you received it. Never strip `@lid` or treat its digits as a phone number.
- Accepted wherever a field says "or a WhatsApp id": text and media sends, batches, validation (always counted valid: there is no number to check).
- Not accepted by OTP, campaigns, reminders and `toPhone` of forward: they need a real number.
- In chats, a person is keyed by phone when WhatsApp told us the number behind the id, else by the `@lid` id.
---
## Sending messages
One text message comes with every product. Photos, files, voice notes, contacts, locations, links, polls, forwarding, editing and deleting come with **Developer** (feature `media`).
#### Send a text message
`POST /api/v1/alert/send`
Queues one text message. It goes out a few seconds later with a typing indicator, like a person would send it (`high` priority skips that). The number is checked on WhatsApp first.
- Unlocked by: every product (`send`)
- Per-number key: yes
> Follow the message with `GET /messages` or the `message.*` [webhook events](https://wahasend.com/docs/webhooks.md). `messageId` is WahaSend's id, not WhatsApp's. `containerId` is the number it is queued on; with [several numbers](https://wahasend.com/docs/phone-numbers.md#several-numbers) it can move to another number of the group if that one goes down, and `GET /messages` and the webhooks then show the number that really sent it.
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phone` | string | yes | International phone number (`+22212345678`) or a WhatsApp id ending in `@lid`. See [Phone numbers](https://wahasend.com/docs/phone-numbers.md). |
| `message` | string | yes | The text, 1–4096 characters. Links get a preview. |
| `priority` | "high" \| "normal" \| "low" | no | Default `normal`. `high` jumps the queue and skips the typing simulation; use it for time-critical alerts only. |
| `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/alert/send \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+22212345678",
"message": "Your order #1042 is ready for pickup.",
"priority": "normal"
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/alert/send', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"phone": "+22212345678",
"message": "Your order #1042 is ready for pickup.",
"priority": "normal"
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/alert/send",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"phone": "+22212345678",
"message": "Your order #1042 is ready for pickup.",
"priority": "normal",
},
)
print(r.json())
```
**Response 201**
```json
{
"success": true,
"messageId": "4f0c6a1e-2b7d-4c1a-9e35-7d8f2a6b1c90",
"status": "queued",
"priority": "normal",
"containerId": "6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64"
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`NUMBER_NOT_ON_WHATSAPP`](https://wahasend.com/docs/errors.md#number_not_on_whatsapp) (400): This number is not on WhatsApp.
- [`NUMBER_VALIDATION_UNAVAILABLE`](https://wahasend.com/docs/errors.md#number_validation_unavailable) (503): Numbers cannot be checked right now. Try again shortly.
- [`NO_SESSION_CONFIGURED`](https://wahasend.com/docs/errors.md#no_session_configured) (400): This account has no connected WhatsApp number.
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
#### Send a photo
`POST /api/v1/alert/send-image`
Sends an image from a link, with an optional caption.
- Unlocked by: Developer (`media`)
- Per-number key: yes
> Sent right away through your number (no queue, no typing simulation) and recorded in `GET /messages` as `sent`, so `message.delivered` / `message.read` [events](https://wahasend.com/docs/webhooks.md) follow. `messageId` is WahaSend's id (use it to edit, delete or forward the message); `wahaMessageId` is WhatsApp's.
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phone` | string | yes | International phone number (`+22212345678`) or a WhatsApp id ending in `@lid`. See [Phone numbers](https://wahasend.com/docs/phone-numbers.md). |
| `imageUrl` | string (URL) | yes | Link to the image (JPEG, PNG or WebP). Must be a public address: private and local addresses are refused. |
| `caption` | string | no | Text under the file, up to 1024 characters. |
| `filename` | string | no | File name shown to the contact, up to 255 characters. |
| `mimetype` | string | no | MIME type, e.g. `application/pdf` (up to 100 characters). Guessed when missing. |
| `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/alert/send-image \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+22212345678",
"imageUrl": "https://example.com/menu.jpg",
"caption": "Today'\''s menu"
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/alert/send-image', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"phone": "+22212345678",
"imageUrl": "https://example.com/menu.jpg",
"caption": "Today's menu"
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/alert/send-image",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"phone": "+22212345678",
"imageUrl": "https://example.com/menu.jpg",
"caption": "Today's menu",
},
)
print(r.json())
```
**Response 201**
```json
{
"success": true,
"messageId": "3d7f1b2c-5e8a-4c69-b0d3-8f2a6c4e1b95",
"wahaMessageId": "true_22212345678@c.us_3EB0C767D26A1D2E5A8F",
"containerId": "6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64"
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`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.
- [`PRODUCT_REQUIRED`](https://wahasend.com/docs/errors.md#product_required) (403): This feature is not included in your products.
#### Send a video
`POST /api/v1/alert/send-video`
Sends an MP4 video from a link.
- Unlocked by: Developer (`media`)
- Per-number key: yes
> Sent right away through your number (no queue, no typing simulation) and recorded in `GET /messages` as `sent`, so `message.delivered` / `message.read` [events](https://wahasend.com/docs/webhooks.md) follow. `messageId` is WahaSend's id (use it to edit, delete or forward the message); `wahaMessageId` is WhatsApp's.
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phone` | string | yes | International phone number (`+22212345678`) or a WhatsApp id ending in `@lid`. See [Phone numbers](https://wahasend.com/docs/phone-numbers.md). |
| `videoUrl` | string (URL) | yes | Link to the MP4 file. Must be a public address: private and local addresses are refused. |
| `caption` | string | no | Text under the file, up to 1024 characters. |
| `filename` | string | no | File name shown to the contact, up to 255 characters. |
| `mimetype` | string | no | MIME type, e.g. `application/pdf` (up to 100 characters). Guessed when missing. |
| `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/alert/send-video \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+22212345678",
"videoUrl": "https://example.com/demo.mp4",
"caption": "How it works"
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/alert/send-video', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"phone": "+22212345678",
"videoUrl": "https://example.com/demo.mp4",
"caption": "How it works"
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/alert/send-video",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"phone": "+22212345678",
"videoUrl": "https://example.com/demo.mp4",
"caption": "How it works",
},
)
print(r.json())
```
**Response 201**
```json
{
"success": true,
"messageId": "3d7f1b2c-5e8a-4c69-b0d3-8f2a6c4e1b95",
"wahaMessageId": "true_22212345678@c.us_3EB0C767D26A1D2E5A8F",
"containerId": "6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64"
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`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.
- [`PRODUCT_REQUIRED`](https://wahasend.com/docs/errors.md#product_required) (403): This feature is not included in your products.
#### Send a file
`POST /api/v1/alert/send-file`
Sends a document (PDF, spreadsheet…) from a link.
- Unlocked by: Developer (`media`)
- Per-number key: yes
> Sent right away through your number (no queue, no typing simulation) and recorded in `GET /messages` as `sent`, so `message.delivered` / `message.read` [events](https://wahasend.com/docs/webhooks.md) follow. `messageId` is WahaSend's id (use it to edit, delete or forward the message); `wahaMessageId` is WhatsApp's.
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phone` | string | yes | International phone number (`+22212345678`) or a WhatsApp id ending in `@lid`. See [Phone numbers](https://wahasend.com/docs/phone-numbers.md). |
| `fileUrl` | string (URL) | yes | Link to the file. Must be a public address: private and local addresses are refused. |
| `filename` | string | no | File name shown to the contact, up to 255 characters. |
| `caption` | string | no | Text under the file, up to 1024 characters. |
| `mimetype` | string | no | MIME type, e.g. `application/pdf` (up to 100 characters). Guessed when missing. |
| `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/alert/send-file \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+22212345678",
"fileUrl": "https://example.com/invoice-1042.pdf",
"filename": "invoice-1042.pdf",
"caption": "Your invoice"
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/alert/send-file', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"phone": "+22212345678",
"fileUrl": "https://example.com/invoice-1042.pdf",
"filename": "invoice-1042.pdf",
"caption": "Your invoice"
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/alert/send-file",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"phone": "+22212345678",
"fileUrl": "https://example.com/invoice-1042.pdf",
"filename": "invoice-1042.pdf",
"caption": "Your invoice",
},
)
print(r.json())
```
**Response 201**
```json
{
"success": true,
"messageId": "3d7f1b2c-5e8a-4c69-b0d3-8f2a6c4e1b95",
"wahaMessageId": "true_22212345678@c.us_3EB0C767D26A1D2E5A8F",
"containerId": "6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64"
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`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.
- [`PRODUCT_REQUIRED`](https://wahasend.com/docs/errors.md#product_required) (403): This feature is not included in your products.
#### Send a voice note
`POST /api/v1/alert/send-voice`
Sends an audio file as a voice note. Use OGG/Opus for it to play as a voice note.
- Unlocked by: Developer (`media`)
- Per-number key: yes
> Sent right away through your number (no queue, no typing simulation) and recorded in `GET /messages` as `sent`, so `message.delivered` / `message.read` [events](https://wahasend.com/docs/webhooks.md) follow. `messageId` is WahaSend's id (use it to edit, delete or forward the message); `wahaMessageId` is WhatsApp's.
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phone` | string | yes | International phone number (`+22212345678`) or a WhatsApp id ending in `@lid`. See [Phone numbers](https://wahasend.com/docs/phone-numbers.md). |
| `audioUrl` | string (URL) | yes | Link to the audio file. Must be a public address: private and local addresses are refused. |
| `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/alert/send-voice \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+22212345678",
"audioUrl": "https://example.com/welcome.ogg"
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/alert/send-voice', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"phone": "+22212345678",
"audioUrl": "https://example.com/welcome.ogg"
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/alert/send-voice",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"phone": "+22212345678",
"audioUrl": "https://example.com/welcome.ogg",
},
)
print(r.json())
```
**Response 201**
```json
{
"success": true,
"messageId": "3d7f1b2c-5e8a-4c69-b0d3-8f2a6c4e1b95",
"wahaMessageId": "true_22212345678@c.us_3EB0C767D26A1D2E5A8F",
"containerId": "6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64"
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`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.
- [`PRODUCT_REQUIRED`](https://wahasend.com/docs/errors.md#product_required) (403): This feature is not included in your products.
#### Share contacts
`POST /api/v1/alert/send-contact`
Sends 1 to 10 contact cards.
- Unlocked by: Developer (`media`)
- Per-number key: yes
> Sent right away through your number (no queue, no typing simulation) and recorded in `GET /messages` as `sent`, so `message.delivered` / `message.read` [events](https://wahasend.com/docs/webhooks.md) follow. `messageId` is WahaSend's id (use it to edit, delete or forward the message); `wahaMessageId` is WhatsApp's.
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phone` | string | yes | International phone number (`+22212345678`) or a WhatsApp id ending in `@lid`. See [Phone numbers](https://wahasend.com/docs/phone-numbers.md). |
| `contacts` | array | yes | 1–10 contacts. |
| `contacts[].fullName` | string | yes | Name, 1–255 characters. |
| `contacts[].phoneNumber` | string | yes | Their number, 5–20 characters. |
| `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/alert/send-contact \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+22212345678",
"contacts": [
{
"fullName": "Front desk",
"phoneNumber": "+22245250000"
}
]
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/alert/send-contact', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"phone": "+22212345678",
"contacts": [
{
"fullName": "Front desk",
"phoneNumber": "+22245250000"
}
]
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/alert/send-contact",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"phone": "+22212345678",
"contacts": [
{
"fullName": "Front desk",
"phoneNumber": "+22245250000",
},
],
},
)
print(r.json())
```
**Response 201**
```json
{
"success": true,
"messageId": "3d7f1b2c-5e8a-4c69-b0d3-8f2a6c4e1b95",
"wahaMessageId": "true_22212345678@c.us_3EB0C767D26A1D2E5A8F",
"containerId": "6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64"
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`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.
- [`PRODUCT_REQUIRED`](https://wahasend.com/docs/errors.md#product_required) (403): This feature is not included in your products.
#### Send a location
`POST /api/v1/alert/send-location`
Sends a map pin.
- Unlocked by: Developer (`media`)
- Per-number key: yes
> Sent right away through your number (no queue, no typing simulation) and recorded in `GET /messages` as `sent`, so `message.delivered` / `message.read` [events](https://wahasend.com/docs/webhooks.md) follow. `messageId` is WahaSend's id (use it to edit, delete or forward the message); `wahaMessageId` is WhatsApp's.
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phone` | string | yes | International phone number (`+22212345678`) or a WhatsApp id ending in `@lid`. See [Phone numbers](https://wahasend.com/docs/phone-numbers.md). |
| `latitude` | number | yes | −90 to 90. |
| `longitude` | number | yes | −180 to 180. |
| `title` | string | no | Place name, up to 255 characters. |
| `address` | string | no | Address, up to 500 characters. |
| `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/alert/send-location \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+22212345678",
"latitude": 18.0858,
"longitude": -15.9785,
"title": "Our shop",
"address": "Tevragh Zeina, Nouakchott"
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/alert/send-location', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"phone": "+22212345678",
"latitude": 18.0858,
"longitude": -15.9785,
"title": "Our shop",
"address": "Tevragh Zeina, Nouakchott"
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/alert/send-location",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"phone": "+22212345678",
"latitude": 18.0858,
"longitude": -15.9785,
"title": "Our shop",
"address": "Tevragh Zeina, Nouakchott",
},
)
print(r.json())
```
**Response 201**
```json
{
"success": true,
"messageId": "3d7f1b2c-5e8a-4c69-b0d3-8f2a6c4e1b95",
"wahaMessageId": "true_22212345678@c.us_3EB0C767D26A1D2E5A8F",
"containerId": "6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64"
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`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.
- [`PRODUCT_REQUIRED`](https://wahasend.com/docs/errors.md#product_required) (403): This feature is not included in your products.
#### Send a link with a custom preview
`POST /api/v1/alert/send-link`
Sends text with a link preview you choose (title, description, image).
- Unlocked by: Developer (`media`)
- Per-number key: yes
> Sent right away through your number (no queue, no typing simulation) and recorded in `GET /messages` as `sent`, so `message.delivered` / `message.read` [events](https://wahasend.com/docs/webhooks.md) follow. `messageId` is WahaSend's id (use it to edit, delete or forward the message); `wahaMessageId` is WhatsApp's.
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phone` | string | yes | International phone number (`+22212345678`) or a WhatsApp id ending in `@lid`. See [Phone numbers](https://wahasend.com/docs/phone-numbers.md). |
| `text` | string | yes | Message text, 1–4096 characters. Include the link in it. |
| `previewUrl` | string (URL) | yes | The link the preview is for. Must be a public address: private and local addresses are refused. |
| `previewTitle` | string | yes | Preview title, 1–255 characters. |
| `previewDescription` | string | no | Up to 500 characters. |
| `previewImageUrl` | string (URL) | no | Public link to the preview image. |
| `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/alert/send-link \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+22212345678",
"text": "Track your order: https://example.com/t/1042",
"previewUrl": "https://example.com/t/1042",
"previewTitle": "Order #1042"
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/alert/send-link', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"phone": "+22212345678",
"text": "Track your order: https://example.com/t/1042",
"previewUrl": "https://example.com/t/1042",
"previewTitle": "Order #1042"
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/alert/send-link",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"phone": "+22212345678",
"text": "Track your order: https://example.com/t/1042",
"previewUrl": "https://example.com/t/1042",
"previewTitle": "Order #1042",
},
)
print(r.json())
```
**Response 201**
```json
{
"success": true,
"messageId": "3d7f1b2c-5e8a-4c69-b0d3-8f2a6c4e1b95",
"wahaMessageId": "true_22212345678@c.us_3EB0C767D26A1D2E5A8F",
"containerId": "6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64"
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`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.
- [`PRODUCT_REQUIRED`](https://wahasend.com/docs/errors.md#product_required) (403): This feature is not included in your products.
#### Send a poll
`POST /api/v1/alert/send-poll`
Sends a poll with 2 to 12 options. Votes arrive as `poll.vote` [webhook events](https://wahasend.com/docs/webhooks.md).
- Unlocked by: Developer (`media`)
- Per-number key: yes
> Sent right away through your number (no queue, no typing simulation) and recorded in `GET /messages` as `sent`, so `message.delivered` / `message.read` [events](https://wahasend.com/docs/webhooks.md) follow. `messageId` is WahaSend's id (use it to edit, delete or forward the message); `wahaMessageId` is WhatsApp's.
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phone` | string | yes | International phone number (`+22212345678`) or a WhatsApp id ending in `@lid`. See [Phone numbers](https://wahasend.com/docs/phone-numbers.md). |
| `question` | string | yes | 1–255 characters. |
| `options` | string[] | yes | 2–12 options, each 1–100 characters. |
| `multipleAnswers` | boolean | no | Allow several answers. Default `false`. |
| `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/alert/send-poll \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+22212345678",
"question": "Which delivery day suits you?",
"options": [
"Monday",
"Wednesday",
"Friday"
]
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/alert/send-poll', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"phone": "+22212345678",
"question": "Which delivery day suits you?",
"options": [
"Monday",
"Wednesday",
"Friday"
]
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/alert/send-poll",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"phone": "+22212345678",
"question": "Which delivery day suits you?",
"options": [
"Monday",
"Wednesday",
"Friday",
],
},
)
print(r.json())
```
**Response 201**
```json
{
"success": true,
"messageId": "3d7f1b2c-5e8a-4c69-b0d3-8f2a6c4e1b95",
"wahaMessageId": "true_22212345678@c.us_3EB0C767D26A1D2E5A8F",
"containerId": "6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64"
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`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.
- [`PRODUCT_REQUIRED`](https://wahasend.com/docs/errors.md#product_required) (403): This feature is not included in your products.
#### Forward a message
`POST /api/v1/alert/forward`
Forwards a message your number has to another chat. The recipient sees "Forwarded". Recorded in `GET /messages` like the media sends.
- Unlocked by: Developer (`media`)
- Per-number key: yes
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `fromPhone` | string | no | Not needed (WhatsApp finds the message by its id); still accepted. |
| `messageId` | string | yes | The message: WahaSend's `messageId` of a message you sent, or a WhatsApp id (e.g. `wahaMessageId` of a `message.inbound` event). |
| `toPhone` | string | yes | International phone number, 8–20 characters (`+22212345678`). `@lid` ids are not accepted here. |
| `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/alert/forward \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messageId": "true_22212345678@c.us_3EB0C767D26A1D2E5A8F",
"toPhone": "+22287654321"
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/alert/forward', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"messageId": "true_22212345678@c.us_3EB0C767D26A1D2E5A8F",
"toPhone": "+22287654321"
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/alert/forward",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"messageId": "true_22212345678@c.us_3EB0C767D26A1D2E5A8F",
"toPhone": "+22287654321",
},
)
print(r.json())
```
**Response 201**
```json
{
"success": true,
"messageId": "3d7f1b2c-5e8a-4c69-b0d3-8f2a6c4e1b95",
"wahaMessageId": "true_22212345678@c.us_3EB0C767D26A1D2E5A8F",
"containerId": "6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64"
}
```
**Errors to expect**
- [`MESSAGE_NOT_FOUND`](https://wahasend.com/docs/errors.md#message_not_found) (404): No message with this id was sent by this account.
- [`MESSAGE_NOT_SENT`](https://wahasend.com/docs/errors.md#message_not_sent) (409): This message has not been sent yet (or it failed). Only sent messages can be edited or deleted.
- [`MESSAGE_WRONG_NUMBER`](https://wahasend.com/docs/errors.md#message_wrong_number) (403): This message was sent from another WhatsApp number than the one this request uses.
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`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.
- [`PRODUCT_REQUIRED`](https://wahasend.com/docs/errors.md#product_required) (403): This feature is not included in your products.
#### Edit a sent message
`PUT /api/v1/alert/edit`
Changes the text of a message your number sent. WhatsApp only allows this for a short while after sending.
- Unlocked by: Developer (`media`)
- Per-number key: yes
> With WahaSend's id the message is found in your history: it must be sent (`sent`, `delivered` or `read`, else `MESSAGE_NOT_SENT`), yours (else `MESSAGE_NOT_FOUND`) and from the number this request uses (a per-number key or `containerId`; else `MESSAGE_WRONG_NUMBER`). Its text in `GET /messages` is updated too.
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `messageId` | string | yes | The message to change. WahaSend's `messageId` (from `/alert/send`, a media send or `GET /messages`; the message must be sent), or WhatsApp's id as before (then `phone` is required). |
| `text` | string | yes | New text, 1–4096 characters. |
| `phone` | string | no | The chat's phone number or `@lid` id. Required with a WhatsApp id; with WahaSend's id it is taken from the message (if given, it must match). |
| `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 PUT https://app.wahasend.com/api/v1/alert/edit \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messageId": "4f0c6a1e-2b7d-4c1a-9e35-7d8f2a6b1c90",
"text": "Your order #1042 is ready (counter 2)."
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/alert/edit', {
method: 'PUT',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"messageId": "4f0c6a1e-2b7d-4c1a-9e35-7d8f2a6b1c90",
"text": "Your order #1042 is ready (counter 2)."
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.put(
"https://app.wahasend.com/api/v1/alert/edit",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"messageId": "4f0c6a1e-2b7d-4c1a-9e35-7d8f2a6b1c90",
"text": "Your order #1042 is ready (counter 2).",
},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"messageId": "4f0c6a1e-2b7d-4c1a-9e35-7d8f2a6b1c90",
"wahaMessageId": "true_22212345678@c.us_3EB0C767D26A1D2E5A8F"
}
```
**Errors to expect**
- [`MESSAGE_NOT_FOUND`](https://wahasend.com/docs/errors.md#message_not_found) (404): No message with this id was sent by this account.
- [`MESSAGE_NOT_SENT`](https://wahasend.com/docs/errors.md#message_not_sent) (409): This message has not been sent yet (or it failed). Only sent messages can be edited or deleted.
- [`MESSAGE_WRONG_NUMBER`](https://wahasend.com/docs/errors.md#message_wrong_number) (403): This message was sent from another WhatsApp number than the one this request uses.
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`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.
- [`PRODUCT_REQUIRED`](https://wahasend.com/docs/errors.md#product_required) (403): This feature is not included in your products.
#### Delete a sent message
`POST /api/v1/alert/delete`
Deletes a message your number sent, for everyone.
- Unlocked by: Developer (`media`)
- Per-number key: yes
> Same rules as [edit](https://wahasend.com/docs/api/sending.md#alert-edit). With a WhatsApp id, send `phone` too: `{ "phone": "+22212345678", "messageId": "true_…" }`.
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `messageId` | string | yes | The message to delete. WahaSend's `messageId` (from `/alert/send`, a media send or `GET /messages`; the message must be sent), or WhatsApp's id as before (then `phone` is required). |
| `phone` | string | no | The chat's phone number or `@lid` id. Required with a WhatsApp id; with WahaSend's id it is taken from the message (if given, it must match). |
| `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/alert/delete \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messageId": "4f0c6a1e-2b7d-4c1a-9e35-7d8f2a6b1c90"
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/alert/delete', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"messageId": "4f0c6a1e-2b7d-4c1a-9e35-7d8f2a6b1c90"
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/alert/delete",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"messageId": "4f0c6a1e-2b7d-4c1a-9e35-7d8f2a6b1c90",
},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"messageId": "4f0c6a1e-2b7d-4c1a-9e35-7d8f2a6b1c90",
"wahaMessageId": "true_22212345678@c.us_3EB0C767D26A1D2E5A8F"
}
```
**Errors to expect**
- [`MESSAGE_NOT_FOUND`](https://wahasend.com/docs/errors.md#message_not_found) (404): No message with this id was sent by this account.
- [`MESSAGE_NOT_SENT`](https://wahasend.com/docs/errors.md#message_not_sent) (409): This message has not been sent yet (or it failed). Only sent messages can be edited or deleted.
- [`MESSAGE_WRONG_NUMBER`](https://wahasend.com/docs/errors.md#message_wrong_number) (403): This message was sent from another WhatsApp number than the one this request uses.
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`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.
- [`PRODUCT_REQUIRED`](https://wahasend.com/docs/errors.md#product_required) (403): This feature is not included in your products.
---
## Validate numbers
Find out which numbers are on WhatsApp before you write to them. Comes with **Notify**, **Developer** and **Connect** (feature `batch`). Nothing is sent.
#### Check up to 500 numbers
`POST /api/v1/alert/validate`
Returns the numbers that are on WhatsApp and those that aren't, as digits. `@lid` ids count as valid.
- Unlocked by: Notify, Developer, Connect (`batch`)
- Per-number key: yes
> When **none** of the numbers is on WhatsApp the answer is a 400 `NO_VALID_NUMBERS` error with `details.invalidNumbers`, not an empty `valid` list.
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phones` | string[] | yes | 1–500 phone numbers or `@lid` ids. |
| `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/alert/validate \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phones": [
"+22212345678",
"+22287654321"
]
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/alert/validate', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"phones": [
"+22212345678",
"+22287654321"
]
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/alert/validate",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"phones": [
"+22212345678",
"+22287654321",
],
},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"valid": [
"22212345678"
],
"invalid": [
"22287654321"
],
"total": 2
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`NO_VALID_NUMBERS`](https://wahasend.com/docs/errors.md#no_valid_numbers) (400): None of these numbers are on WhatsApp.
- [`NUMBER_VALIDATION_UNAVAILABLE`](https://wahasend.com/docs/errors.md#number_validation_unavailable) (503): Numbers cannot be checked right now. Try again shortly.
- [`NO_SESSION_CONFIGURED`](https://wahasend.com/docs/errors.md#no_session_configured) (400): This account has no connected WhatsApp number.
#### Check a long list (up to 10,000)
`POST /api/v1/recipients/validate`
For contact lists: removes duplicates, checks the format, then asks WhatsApp five numbers at a time. Slower; use it before a campaign.
- Unlocked by: Notify, Developer, Connect (`batch`)
- Per-number key: yes
> A number WhatsApp could not be asked about is listed as valid with an empty `chatId`.
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phones` | string[] | yes | 1–10,000 phone numbers. Spaces, dashes and brackets are removed. |
| `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/recipients/validate \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phones": [
"+222 12 34 56 78",
"+22287654321",
"12"
]
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/recipients/validate', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"phones": [
"+222 12 34 56 78",
"+22287654321",
"12"
]
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/recipients/validate",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"phones": [
"+222 12 34 56 78",
"+22287654321",
"12",
],
},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"total": 3,
"valid": 1,
"invalid": 2,
"validNumbers": [
{
"phone": "+22212345678",
"chatId": "22212345678@c.us"
}
],
"invalidNumbers": [
{
"phone": "12",
"reason": "Invalid format",
"code": "invalid_format"
},
{
"phone": "+22287654321",
"reason": "Not on WhatsApp",
"code": "not_on_whatsapp"
}
]
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`NO_SESSION_CONFIGURED`](https://wahasend.com/docs/errors.md#no_session_configured) (400): This account has no connected WhatsApp number.
---
## Batch sends
The same text to up to 500 people in one call. Comes with **Notify**, **Developer** and **Connect** (feature `batch`). For personalised texts and scheduling, use [campaigns](https://wahasend.com/docs/api/campaigns.md).
#### Send one text to many
`POST /api/v1/alert/batch`
Checks every number on WhatsApp, skips the ones that aren't, and queues one message per remaining recipient.
- Unlocked by: Notify, Developer, Connect (`batch`)
- Per-number key: yes
> Sending the same text to many people who didn't ask for it is the fastest way to get a number banned. See [Good practice](https://wahasend.com/docs/good-practice.md).
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `recipients` | string[] | yes | 1–500 phone numbers or `@lid` ids. |
| `message` | string | yes | 1–4096 characters. |
| `priority` | "high" \| "normal" \| "low" | no | Default `normal`. |
| `skipInvalidNumbers` | boolean | no | Default `true`: numbers not on WhatsApp are skipped and listed in `invalidNumbers`. `false`: if any number is not on WhatsApp, nothing is sent (`NUMBER_NOT_ON_WHATSAPP` with `details.invalidNumbers`). |
| `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/alert/batch \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipients": [
"+22212345678",
"+22287654321"
],
"message": "We are closed on Friday for the holiday."
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/alert/batch', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"recipients": [
"+22212345678",
"+22287654321"
],
"message": "We are closed on Friday for the holiday."
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/alert/batch",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"recipients": [
"+22212345678",
"+22287654321",
],
"message": "We are closed on Friday for the holiday.",
},
)
print(r.json())
```
**Response 201**
```json
{
"success": true,
"count": 1,
"messages": [
{
"phone": "22212345678",
"messageId": "4f0c6a1e-2b7d-4c1a-9e35-7d8f2a6b1c90",
"containerId": "6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64"
}
],
"invalidNumbers": [
"22287654321"
],
"skipped": 1
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`NO_VALID_NUMBERS`](https://wahasend.com/docs/errors.md#no_valid_numbers) (400): None of these numbers are on WhatsApp.
- [`NUMBER_NOT_ON_WHATSAPP`](https://wahasend.com/docs/errors.md#number_not_on_whatsapp) (400): This number is not on WhatsApp.
- [`NUMBER_VALIDATION_UNAVAILABLE`](https://wahasend.com/docs/errors.md#number_validation_unavailable) (503): Numbers cannot be checked right now. Try again shortly.
- [`NO_SESSION_CONFIGURED`](https://wahasend.com/docs/errors.md#no_session_configured) (400): This account has no connected WhatsApp number.
---
## Campaigns
A campaign sends a template with `{{variables}}` to a list, spaced 3–7 seconds apart per message, now or at a set time. Comes with **Notify** (feature `campaigns`). Statuses: `draft` ⇄ `scheduled` → `sending` ⇄ `paused` → `completed`; `scheduled` or `paused` → `cancelled`. `stats` counts `sent`, `failed`, `delivered` and `read` (from WhatsApp's receipts, each message once).
#### Create a campaign
`POST /api/v1/bulk/campaigns`
Saves it as a `draft`, or `scheduled` when you give `scheduledAt`. Nothing is sent until you call send (or the time comes).
- Unlocked by: Notify (`campaigns`) + Developer or Connect for an API key
- Per-number key: yes
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | yes | 1–255 characters. |
| `template` | string | yes | 1–4096 characters. `{{name}}`-style placeholders are filled from each recipient's `variables` (any language in the keys). |
| `recipients` | array | yes | 1–10,000 recipients. |
| `recipients[].phone` | string | yes | International phone number, 8–20 characters (`+22212345678`). `@lid` ids are not accepted here. |
| `recipients[].variables` | object (string → string) | no | Values for the template. |
| `scheduledAt` | string (ISO 8601) | no | When to start, with any offset: `2026-10-05T08:00:00Z` or `2026-10-05T08:00:00+00:00` (Nouakchott is UTC). Checked every minute; it starts from your default number. |
**Example**
curl:
```bash
curl -X POST https://app.wahasend.com/api/v1/bulk/campaigns \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Ramadan hours",
"template": "Hello {{name}}, from Monday we open 10:00–16:00.",
"recipients": [
{
"phone": "+22212345678",
"variables": {
"name": "Aïcha"
}
},
{
"phone": "+22287654321",
"variables": {
"name": "Moussa"
}
}
]
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/bulk/campaigns', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"name": "Ramadan hours",
"template": "Hello {{name}}, from Monday we open 10:00–16:00.",
"recipients": [
{
"phone": "+22212345678",
"variables": {
"name": "Aïcha"
}
},
{
"phone": "+22287654321",
"variables": {
"name": "Moussa"
}
}
]
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/bulk/campaigns",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"name": "Ramadan hours",
"template": "Hello {{name}}, from Monday we open 10:00–16:00.",
"recipients": [
{
"phone": "+22212345678",
"variables": {
"name": "Aïcha",
},
},
{
"phone": "+22287654321",
"variables": {
"name": "Moussa",
},
},
],
},
)
print(r.json())
```
**Response 201**
```json
{
"success": true,
"campaign": {
"id": "c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71",
"name": "Ramadan hours",
"status": "draft",
"recipientCount": 2,
"scheduledAt": null
}
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
#### List campaigns
`GET /api/v1/bulk/campaigns`
Every campaign of the organization, oldest first.
- Unlocked by: Notify (`campaigns`) + Developer or Connect for an API key
- Per-number key: yes
**Example**
curl:
```bash
curl https://app.wahasend.com/api/v1/bulk/campaigns \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/bulk/campaigns', {
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.get(
"https://app.wahasend.com/api/v1/bulk/campaigns",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"campaigns": [
{
"id": "c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71",
"name": "Ramadan hours",
"template": "Hello {{name}}, from Monday we open 10:00–16:00.",
"status": "completed",
"stats": {
"total": 2,
"sent": 2,
"delivered": 2,
"failed": 0,
"read": 1
},
"scheduledAt": null,
"createdAt": "2026-09-30T09:12:44.120Z"
}
]
}
```
#### Campaign status
`GET /api/v1/bulk/campaigns/:id/status`
Where a campaign stands, with its recipients and counts.
- Unlocked by: Notify (`campaigns`) + Developer or Connect for an API key
- Per-number key: yes
> `delivered` counts a message the first time WhatsApp reports it delivered or read; `read` the first time it is read. Campaigns from before 2026-10-01 have no `read`.
**Path parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | yes | Campaign id. |
**Example**
curl:
```bash
curl https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/status \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/status', {
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.get(
"https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/status",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"id": "c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71",
"name": "Ramadan hours",
"template": "Hello {{name}}, from Monday we open 10:00–16:00.",
"recipients": [
{
"phone": "+22212345678",
"variables": {
"name": "Aïcha"
}
}
],
"status": "sending",
"stats": {
"total": 2,
"sent": 1,
"delivered": 1,
"failed": 0,
"read": 0
},
"scheduledAt": null,
"startedAt": "2026-09-30T09:15:02.000Z",
"completedAt": null,
"createdAt": "2026-09-30T09:12:44.120Z"
}
```
**Errors to expect**
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
#### Edit before it starts
`PUT /api/v1/bulk/campaigns/:id`
Changes the name, template, recipients or start time of a `draft` or `scheduled` campaign.
- Unlocked by: Notify (`campaigns`) + Developer or Connect for an API key
- Per-number key: yes
**Path parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | yes | Campaign id. |
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | 1–255 characters. |
| `template` | string | no | 1–4096 characters. |
| `recipients` | array | no | Replaces the whole list (1–10,000). |
| `recipients[].phone` | string | yes | International phone number, 8–20 characters (`+22212345678`). `@lid` ids are not accepted here. |
| `recipients[].variables` | object (string → string) | no | Values for the template. |
| `scheduledAt` | string (ISO 8601) \| null | no | A new start time (any offset) makes it `scheduled`; `null` makes it a `draft` again. |
**Example**
curl:
```bash
curl -X PUT https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71 \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"template": "Hello {{name}}, from Monday we open 10:00–16:30.",
"scheduledAt": "2026-10-05T08:00:00+00:00"
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71', {
method: 'PUT',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"template": "Hello {{name}}, from Monday we open 10:00–16:30.",
"scheduledAt": "2026-10-05T08:00:00+00:00"
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.put(
"https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"template": "Hello {{name}}, from Monday we open 10:00–16:30.",
"scheduledAt": "2026-10-05T08:00:00+00:00",
},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"campaign": {
"id": "c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71",
"name": "Ramadan hours",
"template": "Hello {{name}}, from Monday we open 10:00–16:30.",
"status": "scheduled",
"stats": {
"total": 2,
"sent": 0,
"delivered": 0,
"failed": 0
},
"scheduledAt": "2026-10-05T08:00:00.000Z",
"createdAt": "2026-09-30T09:12:44.120Z"
}
}
```
**Errors to expect**
- [`CAMPAIGN_INVALID_STATE`](https://wahasend.com/docs/errors.md#campaign_invalid_state) (400): The campaign cannot do this in its current state.
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
#### Start sending
`POST /api/v1/bulk/campaigns/:id/send`
Starts a `draft` or `scheduled` campaign now. Messages are queued 3–7 seconds apart.
- Unlocked by: Notify (`campaigns`) + Developer or Connect for an API key
- Per-number key: yes
**Path parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | yes | Campaign id. |
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `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/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/send \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/send', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/send",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"campaignId": "c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71",
"status": "sending",
"totalRecipients": 2,
"estimatedDuration": "1 minutes"
}
```
**Errors to expect**
- [`CAMPAIGN_INVALID_STATE`](https://wahasend.com/docs/errors.md#campaign_invalid_state) (400): The campaign cannot do this in its current state.
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
- [`NO_SESSION_CONFIGURED`](https://wahasend.com/docs/errors.md#no_session_configured) (400): This account has no connected WhatsApp number.
#### Pause
`POST /api/v1/bulk/campaigns/:id/pause`
Stops the messages not sent yet. Only a `sending` campaign can pause.
- Unlocked by: Notify (`campaigns`) + Developer or Connect for an API key
- Per-number key: yes
**Path parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | yes | Campaign id. |
**Example**
curl:
```bash
curl -X POST https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/pause \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/pause', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/pause",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"status": "paused",
"jobsRemoved": 118
}
```
**Errors to expect**
- [`CAMPAIGN_INVALID_STATE`](https://wahasend.com/docs/errors.md#campaign_invalid_state) (400): The campaign cannot do this in its current state.
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
#### Resume
`POST /api/v1/bulk/campaigns/:id/resume`
Sends what a `paused` campaign had left, never from the start.
- Unlocked by: Notify (`campaigns`) + Developer or Connect for an API key
- Per-number key: yes
**Path parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | yes | Campaign id. |
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `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/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/resume \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/resume', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/resume",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"status": "sending",
"jobsRequeued": 118
}
```
**Errors to expect**
- [`CAMPAIGN_INVALID_STATE`](https://wahasend.com/docs/errors.md#campaign_invalid_state) (400): The campaign cannot do this in its current state.
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
- [`NO_SESSION_CONFIGURED`](https://wahasend.com/docs/errors.md#no_session_configured) (400): This account has no connected WhatsApp number.
#### Delete
`DELETE /api/v1/bulk/campaigns/:id`
Deletes a campaign that is not `sending` (pause it first) and its message history.
- Unlocked by: Notify (`campaigns`) + Developer or Connect for an API key
- Per-number key: yes
**Path parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | yes | Campaign id. |
**Example**
curl:
```bash
curl -X DELETE https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71 \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71', {
method: 'DELETE',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.delete(
"https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true
}
```
**Errors to expect**
- [`CAMPAIGN_INVALID_STATE`](https://wahasend.com/docs/errors.md#campaign_invalid_state) (400): The campaign cannot do this in its current state.
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
#### Cancel
`POST /api/v1/bulk/campaigns/:id/cancel`
Cancels a `scheduled` or `paused` campaign for good: status `cancelled`, the messages not sent yet are dropped (marked `failed`, error "Campaign cancelled"). To stop a `sending` campaign, pause it first.
- Unlocked by: Notify (`campaigns`) + Developer or Connect for an API key
- Per-number key: yes
**Path parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | yes | Campaign id. |
**Example**
curl:
```bash
curl -X POST https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/cancel \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/cancel', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/bulk/campaigns/c1f9a2d4-6b3e-4a7c-9d15-3e8b2f6a0c71/cancel",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"status": "cancelled",
"jobsRemoved": 0,
"messagesCancelled": 118
}
```
**Errors to expect**
- [`CAMPAIGN_INVALID_STATE`](https://wahasend.com/docs/errors.md#campaign_invalid_state) (400): The campaign cannot do this in its current state.
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
---
## Reminders
A reminder is a message to one person at a set time, once or repeated daily, weekly or monthly (monthly keeps the day of the month; the 31st becomes the last day of shorter months). Checked every 30 seconds. Comes with **Notify** (feature `reminders`). Statuses: `scheduled`, `processing`, `done`, `failed`, `cancelled`.
#### List reminders
`GET /api/v1/reminders`
Upcoming ones first (soonest first), then the others (latest change first).
- Unlocked by: Notify (`reminders`) + Developer or Connect for an API key
- Account key only
**Query parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `status` | string | no | `scheduled`, `processing`, `done`, `failed` or `cancelled`. |
| `search` | string | no | Part of the phone number or label (100 characters max). |
| `page` | number | no | Default 1. |
| `limit` | number | no | Default 50, max 100. |
**Example**
curl:
```bash
curl "https://app.wahasend.com/api/v1/reminders?status=scheduled&limit=20" \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/reminders?status=scheduled&limit=20', {
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.get(
"https://app.wahasend.com/api/v1/reminders?status=scheduled&limit=20",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"total": 1,
"page": 1,
"limit": 20,
"reminders": [
{
"id": "a7e3c5d1-9b2f-4e6a-8c04-5d1f3b7e9a28",
"ownerId": "2c7d0e19-4b8a-4f63-9e21-7a5c3d9b1f08",
"containerId": null,
"label": "Rent — flat 3",
"phone": "+22212345678",
"message": "Hello {{name}}, your rent of {{amount}} is due tomorrow.",
"variables": {
"name": "Aïcha",
"amount": "45 000 MRU"
},
"sendAt": "2026-11-01T09:00:00.000Z",
"firstSendAt": "2026-10-01T09:00:00.000Z",
"repeat": "monthly",
"status": "scheduled",
"batchId": null,
"sendCount": 1,
"lastSentAt": "2026-10-01T09:00:03.412Z",
"lastMessageId": "4f0c6a1e-2b7d-4c1a-9e35-7d8f2a6b1c90",
"lastError": null,
"createdAt": "2026-09-28T14:02:11.000Z",
"updatedAt": "2026-10-01T09:00:03.500Z",
"lastMessageStatus": "read"
}
]
}
```
#### Create a reminder
`POST /api/v1/reminders`
Schedules one message, optionally repeated. `{{variables}}` in the text are filled at send time.
- Unlocked by: Notify (`reminders`) + Developer or Connect for an API key
- Account key only
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phone` | string | yes | International phone number; spaces, dashes, dots and brackets are removed. |
| `message` | string | yes | 1–4096 characters, may use `{{variables}}`. |
| `sendAt` | string (ISO 8601) | yes | First send time, with `Z` or an offset (`2026-10-01T09:00:00+00:00`). At most 5 minutes in the past. |
| `repeat` | "none" \| "daily" \| "weekly" \| "monthly" | no | Default `none`. |
| `label` | string \| null | no | Your own note, up to 255 characters. |
| `variables` | object (string → string) | no | Up to 50 values (keys ≤ 100, values ≤ 1000 characters). |
| `containerId` | uuid \| null | no | Number to send from; `null` = your default number. |
**Example**
curl:
```bash
curl -X POST https://app.wahasend.com/api/v1/reminders \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+22212345678",
"message": "Hello {{name}}, your rent of {{amount}} is due tomorrow.",
"sendAt": "2026-10-01T09:00:00Z",
"repeat": "monthly",
"label": "Rent — flat 3",
"variables": {
"name": "Aïcha",
"amount": "45 000 MRU"
}
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/reminders', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"phone": "+22212345678",
"message": "Hello {{name}}, your rent of {{amount}} is due tomorrow.",
"sendAt": "2026-10-01T09:00:00Z",
"repeat": "monthly",
"label": "Rent — flat 3",
"variables": {
"name": "Aïcha",
"amount": "45 000 MRU"
}
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/reminders",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"phone": "+22212345678",
"message": "Hello {{name}}, your rent of {{amount}} is due tomorrow.",
"sendAt": "2026-10-01T09:00:00Z",
"repeat": "monthly",
"label": "Rent — flat 3",
"variables": {
"name": "Aïcha",
"amount": "45 000 MRU",
},
},
)
print(r.json())
```
**Response 201**
```json
{
"success": true,
"reminder": {
"id": "a7e3c5d1-9b2f-4e6a-8c04-5d1f3b7e9a28",
"phone": "+22212345678",
"sendAt": "2026-10-01T09:00:00.000Z",
"repeat": "monthly",
"status": "scheduled",
"…": "…"
}
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
#### Import many
`POST /api/v1/reminders/import`
One message template for up to 5000 rows (phone + time each). Rows in the past are skipped and listed, not refused.
- Unlocked by: Notify (`reminders`) + Developer or Connect for an API key
- Account key only
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `message` | string | yes | 1–4096 characters, may use `{{variables}}`. |
| `repeat` | "none" \| "daily" \| "weekly" \| "monthly" | no | Default `none`. |
| `containerId` | uuid \| null | no | Number to send from. |
| `rows` | array | yes | 1–5000 rows. |
| `rows[].phone` | string | yes | International phone number, 8–20 characters (`+22212345678`). `@lid` ids are not accepted here. |
| `rows[].sendAt` | string (ISO 8601) | yes | With `Z` or an offset. |
| `rows[].label` | string \| null | no | Up to 255 characters. |
| `rows[].variables` | object (string → string) | no | Values for this row. |
**Example**
curl:
```bash
curl -X POST https://app.wahasend.com/api/v1/reminders/import \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Hello {{name}}, see you at {{time}}.",
"rows": [
{
"phone": "+22212345678",
"sendAt": "2026-10-02T08:00:00Z",
"variables": {
"name": "Aïcha",
"time": "10:00"
}
},
{
"phone": "+22287654321",
"sendAt": "2026-10-02T08:00:00Z",
"variables": {
"name": "Moussa",
"time": "11:30"
}
}
]
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/reminders/import', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"message": "Hello {{name}}, see you at {{time}}.",
"rows": [
{
"phone": "+22212345678",
"sendAt": "2026-10-02T08:00:00Z",
"variables": {
"name": "Aïcha",
"time": "10:00"
}
},
{
"phone": "+22287654321",
"sendAt": "2026-10-02T08:00:00Z",
"variables": {
"name": "Moussa",
"time": "11:30"
}
}
]
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/reminders/import",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"message": "Hello {{name}}, see you at {{time}}.",
"rows": [
{
"phone": "+22212345678",
"sendAt": "2026-10-02T08:00:00Z",
"variables": {
"name": "Aïcha",
"time": "10:00",
},
},
{
"phone": "+22287654321",
"sendAt": "2026-10-02T08:00:00Z",
"variables": {
"name": "Moussa",
"time": "11:30",
},
},
],
},
)
print(r.json())
```
**Response 201**
```json
{
"success": true,
"created": 2,
"batchId": "e5c1a9f3-2d7b-4b8e-a6f0-8c3d1e5b9a72",
"skipped": []
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
#### Edit a reminder
`PUT /api/v1/reminders/:id`
Any field of create. A new `sendAt` re-arms a done, failed or cancelled reminder.
- Unlocked by: Notify (`reminders`) + Developer or Connect for an API key
- Account key only
**Path parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | yes | Reminder id. |
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phone` | string | no | International phone number. |
| `message` | string | no | 1–4096 characters. |
| `sendAt` | string (ISO 8601) | no | New time; re-arms the reminder. |
| `repeat` | "none" \| "daily" \| "weekly" \| "monthly" | no | New repetition. |
| `label` | string \| null | no | Up to 255 characters. |
| `variables` | object (string → string) | no | Replaces the values. |
| `containerId` | uuid \| null | no | Number to send from. |
**Example**
curl:
```bash
curl -X PUT https://app.wahasend.com/api/v1/reminders/a7e3c5d1-9b2f-4e6a-8c04-5d1f3b7e9a28 \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"sendAt": "2026-10-03T09:00:00Z"
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/reminders/a7e3c5d1-9b2f-4e6a-8c04-5d1f3b7e9a28', {
method: 'PUT',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"sendAt": "2026-10-03T09:00:00Z"
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.put(
"https://app.wahasend.com/api/v1/reminders/a7e3c5d1-9b2f-4e6a-8c04-5d1f3b7e9a28",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"sendAt": "2026-10-03T09:00:00Z",
},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"reminder": {
"id": "a7e3c5d1-9b2f-4e6a-8c04-5d1f3b7e9a28",
"sendAt": "2026-10-03T09:00:00.000Z",
"status": "scheduled",
"…": "…"
}
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
- [`FORBIDDEN_ACTION`](https://wahasend.com/docs/errors.md#forbidden_action) (400): This action is not allowed.
#### Cancel
`POST /api/v1/reminders/:id/cancel`
Stops a `scheduled` reminder (it stays in the list as `cancelled`).
- Unlocked by: Notify (`reminders`) + Developer or Connect for an API key
- Account key only
**Path parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | yes | Reminder id. |
**Example**
curl:
```bash
curl -X POST https://app.wahasend.com/api/v1/reminders/a7e3c5d1-9b2f-4e6a-8c04-5d1f3b7e9a28/cancel \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/reminders/a7e3c5d1-9b2f-4e6a-8c04-5d1f3b7e9a28/cancel', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/reminders/a7e3c5d1-9b2f-4e6a-8c04-5d1f3b7e9a28/cancel",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"reminder": {
"id": "a7e3c5d1-9b2f-4e6a-8c04-5d1f3b7e9a28",
"status": "cancelled",
"…": "…"
}
}
```
**Errors to expect**
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
- [`FORBIDDEN_ACTION`](https://wahasend.com/docs/errors.md#forbidden_action) (400): This action is not allowed.
#### Delete
`DELETE /api/v1/reminders/:id`
Removes the reminder for good.
- Unlocked by: Notify (`reminders`) + Developer or Connect for an API key
- Account key only
**Path parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | uuid | yes | Reminder id. |
**Example**
curl:
```bash
curl -X DELETE https://app.wahasend.com/api/v1/reminders/a7e3c5d1-9b2f-4e6a-8c04-5d1f3b7e9a28 \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/reminders/a7e3c5d1-9b2f-4e6a-8c04-5d1f3b7e9a28', {
method: 'DELETE',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.delete(
"https://app.wahasend.com/api/v1/reminders/a7e3c5d1-9b2f-4e6a-8c04-5d1f3b7e9a28",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true
}
```
**Errors to expect**
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
---
## One-time codes (OTP)
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`).
#### 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.
#### 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.
---
## Chats and contacts
Read what your number sent and received, one conversation per person, and look up contacts. Comes with **Connect** (feature `inbox`). A person is keyed by their phone digits, or by their `@lid` id when WhatsApp hides the number.
#### List conversations
`GET /api/v1/chats`
Newest first, with the last message and the unread count.
- Unlocked by: Connect (`inbox`)
- Per-number key: yes
> A chat known only by its WhatsApp id has `phone: "…@lid"`; when both are known, `lid` is added. A last message with a file adds `lastAttachment`, e.g. `"PDF · invoice.pdf · 240 KB"`.
**Query parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | number | no | Default 50, at most 200 conversations (values up to 500 are accepted and give 200). |
| `search` | string | no | Part of a phone number or name. |
| `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 "https://app.wahasend.com/api/v1/chats?limit=20" \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/chats?limit=20', {
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.get(
"https://app.wahasend.com/api/v1/chats?limit=20",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"chats": [
{
"phone": "22212345678",
"name": "Aïcha",
"lastMessage": "Merci, à demain",
"lastDirection": "in",
"lastAt": "2026-09-30T10:41:07.000Z",
"unread": 1
}
]
}
```
#### Read a conversation
`GET /api/v1/chats/:phone`
The last messages with one person, oldest first. Sent messages carry their delivery `status`.
- Unlocked by: Connect (`inbox`)
- Per-number key: yes
**Path parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phone` | string | yes | Phone digits (with or without `+`) or the `@lid` id, URL-encoded. |
**Query parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | number | no | Default 50, at most 500. |
| `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 https://app.wahasend.com/api/v1/chats/22212345678 \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/chats/22212345678', {
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.get(
"https://app.wahasend.com/api/v1/chats/22212345678",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"phone": "22212345678",
"name": "Aïcha",
"messages": [
{
"id": "4f0c6a1e-2b7d-4c1a-9e35-7d8f2a6b1c90",
"direction": "out",
"text": "Your order #1042 is ready for pickup.",
"at": "2026-09-30T10:30:12.000Z",
"status": "read"
},
{
"id": "9a3e1b77-0c52-4f8e-b1d4-2e6c9f0a7b13",
"direction": "in",
"text": "Merci, à demain",
"at": "2026-09-30T10:41:07.000Z",
"type": "chat"
}
]
}
```
#### Unread messages
`GET /api/v1/chats/unread`
Received messages nobody has marked read, oldest first. Does not mark them.
- Unlocked by: Connect (`inbox`)
- Per-number key: yes
**Query parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | number | no | Default 100, at most 500. |
| `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 https://app.wahasend.com/api/v1/chats/unread \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/chats/unread', {
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.get(
"https://app.wahasend.com/api/v1/chats/unread",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"messages": [
{
"id": "9a3e1b77-0c52-4f8e-b1d4-2e6c9f0a7b13",
"phone": "22212345678",
"name": "Aïcha",
"text": "Merci, à demain",
"type": "chat",
"at": "2026-09-30T10:41:07.000Z"
}
]
}
```
#### Mark messages read
`POST /api/v1/chats/unread/ack`
Marks the given received messages as read in WahaSend (the contact sees no blue ticks). A per-number key marks only its own number's messages.
- Unlocked by: Connect (`inbox`)
- Per-number key: yes
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `ids` | uuid[] | yes | 1–500 message ids from the unread list. |
**Example**
curl:
```bash
curl -X POST https://app.wahasend.com/api/v1/chats/unread/ack \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"ids": [
"9a3e1b77-0c52-4f8e-b1d4-2e6c9f0a7b13"
]
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/chats/unread/ack', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"ids": [
"9a3e1b77-0c52-4f8e-b1d4-2e6c9f0a7b13"
]
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/chats/unread/ack",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"ids": [
"9a3e1b77-0c52-4f8e-b1d4-2e6c9f0a7b13",
],
},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"marked": 1
}
```
#### Mark a conversation read
`POST /api/v1/chats/:phone/read`
Marks everything received from this person as read (with a per-number key: on its number only).
- Unlocked by: Connect (`inbox`)
- Per-number key: yes
**Path parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phone` | string | yes | Phone digits or `@lid` id. |
**Example**
curl:
```bash
curl -X POST https://app.wahasend.com/api/v1/chats/22212345678/read \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/chats/22212345678/read', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/chats/22212345678/read",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"marked": 3
}
```
#### Find contacts
`GET /api/v1/contacts`
Searches the phone's contacts by saved name, WhatsApp name or part of the number.
- Unlocked by: Connect (`inbox`)
- Per-number key: yes
**Query parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `search` | string | yes | 1–100 characters. |
| `limit` | number | no | Default 10, at most 50. |
**Example**
curl:
```bash
curl "https://app.wahasend.com/api/v1/contacts?search=A%C3%AFcha" \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/contacts?search=A%C3%AFcha', {
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.get(
"https://app.wahasend.com/api/v1/contacts?search=A%C3%AFcha",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"contacts": [
{
"id": "5e1b7c3a-9d2f-4a86-b0e4-1c7f9a3d5b62",
"name": "Aïcha",
"phone": "22212345678",
"lid": "104937261508834@lid",
"saved": true,
"optedOut": false
}
]
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
#### Count contacts
`GET /api/v1/contacts/count`
How many contacts are known, and when the phone's recent history was last imported.
- Unlocked by: Connect (`inbox`)
- Per-number key: yes
**Example**
curl:
```bash
curl https://app.wahasend.com/api/v1/contacts/count \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/contacts/count', {
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.get(
"https://app.wahasend.com/api/v1/contacts/count",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"count": 312,
"historyImportedAt": "2026-09-29T18:03:55.000Z",
"importing": false
}
```
#### Update from the phone
`POST /api/v1/contacts/sync`
Re-reads the address book now and imports recent chats in the background. The number must be connected.
- Unlocked by: Connect (`inbox`)
- Per-number key: yes
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `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/contacts/sync \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/contacts/sync', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/contacts/sync",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"contacts": 312,
"saved": 240,
"lidsLinked": 118,
"messagesLinked": 6,
"importing": true
}
```
**Errors to expect**
- [`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.
---
## WhatsApp numbers
Check and link your numbers from code. Every product can call these. A per-number key sees only its own number; with the account key, pick one with `containerId` (from `GET /session/list`). Adding and removing numbers is done in the dashboard. Notify, Assistant and Shop add as many numbers as they need. With Developer or Connect active, the account has 3 numbers included, plus the extra numbers it buys ($10 a month each, from the dashboard's Subscription page); adding one more answers 403 `NUMBER_LIMIT_REACHED`. Numbers you already have are never removed: an account above its allowance keeps sending from all of them.
#### List your numbers
`GET /api/v1/session/list`
Every number of the organization with its live state. `id` is the `containerId` other calls take.
- Unlocked by: every product
- Account key only
> With Notify, Assistant or Shop only, there is no limit: `maxSessions` is `null` and `canCreate` is `true`. With Developer or Connect active, `maxSessions` is the 3 numbers included plus the extra numbers bought (`allowance.included` + `allowance.extra`, `allowance.capped: true`). Without an active product `maxSessions` is `0`.
> `allowance.over` counts the numbers above the allowance (added before, or extra numbers that ended): they all keep working, and `canCreate` stays `false` until you buy extra numbers or remove some. A new number past the allowance answers 403 `NUMBER_LIMIT_REACHED` with `details: {max, used, included, extra, capped}`.
> This answer (and the other number routes) has no `success` field.
**Example**
curl:
```bash
curl https://app.wahasend.com/api/v1/session/list \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/session/list', {
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.get(
"https://app.wahasend.com/api/v1/session/list",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"sessions": [
{
"id": "6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64",
"instanceName": "waha-pool-1",
"label": "Shop",
"phoneNumber": "22212345678",
"status": "WORKING",
"isDefault": true,
"connected": true,
"me": {
"id": "22212345678@c.us",
"pushName": "Kaba Shop"
},
"hasApiKey": true,
"apiKeyPreview": "waha_V1StG...",
"createdAt": "2026-09-01T12:00:00.000Z"
}
],
"maxSessions": 3,
"usedSessions": 1,
"canCreate": true,
"allowance": {
"capped": true,
"included": 3,
"extra": 0,
"extraExpiresAt": null,
"used": 1,
"max": 3,
"over": 0
}
}
```
#### Several numbers: the setting
`GET /api/v1/session/send-group`
Whether sends that name no number go through a group of your numbers, and how. Off by default. Owner and admins. See [Several numbers](https://wahasend.com/docs/phone-numbers.md#several-numbers).
- Unlocked by: every product
- Account key only
**Example**
curl:
```bash
curl https://app.wahasend.com/api/v1/session/send-group \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/session/send-group', {
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.get(
"https://app.wahasend.com/api/v1/session/send-group",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"sendGroup": {
"enabled": true,
"containerIds": [
"6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64",
"5d1e8a2c-7b3f-4c69-a0d4-2e9f1b7c6a53"
],
"mode": "backup",
"sticky": true
}
}
```
#### Several numbers: change it
`PUT /api/v1/session/send-group`
Any of the fields; the others stay. Signed-in owner or admin only (not an API key); each change is recorded in the activity log.
- Unlocked by: every product
- Account key only
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `enabled` | boolean | no | Turn sending through several numbers on or off. |
| `containerIds` | uuid[] | no | The group's numbers, in order (up to 20, your own). Needed when on. |
| `mode` | "backup" \| "share" | no | `backup` (default): the default number, the next one when it is down. `share`: spread over the group. |
| `sticky` | boolean | no | Same contact, same number (default `true`). |
**Example**
curl:
```bash
curl -X PUT https://app.wahasend.com/api/v1/session/send-group \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"containerIds": [
"6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64",
"5d1e8a2c-7b3f-4c69-a0d4-2e9f1b7c6a53"
],
"mode": "backup",
"sticky": true
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/session/send-group', {
method: 'PUT',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"enabled": true,
"containerIds": [
"6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64",
"5d1e8a2c-7b3f-4c69-a0d4-2e9f1b7c6a53"
],
"mode": "backup",
"sticky": true
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.put(
"https://app.wahasend.com/api/v1/session/send-group",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"enabled": True,
"containerIds": [
"6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64",
"5d1e8a2c-7b3f-4c69-a0d4-2e9f1b7c6a53",
],
"mode": "backup",
"sticky": True,
},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"sendGroup": {
"enabled": true,
"containerIds": [
"6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64",
"5d1e8a2c-7b3f-4c69-a0d4-2e9f1b7c6a53"
],
"mode": "backup",
"sticky": true
}
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
- [`USER_REQUIRED`](https://wahasend.com/docs/errors.md#user_required) (403): Sign in to the dashboard to do this; API keys cannot.
#### Connection state
`GET /api/v1/session/status`
`status` is `WORKING` when the number can send; `action` says what to do otherwise.
- Unlocked by: every product
- Per-number key: yes
> Other states: `SCAN_QR_CODE` (link it), `STARTING`, `STOPPED`, `FAILED`, `UNREACHABLE`.
**Query parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `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 https://app.wahasend.com/api/v1/session/status \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/session/status', {
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.get(
"https://app.wahasend.com/api/v1/session/status",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"session": "n6d2b9c1e8f4a4e37",
"containerId": "6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64",
"status": "WORKING",
"connected": true,
"me": {
"id": "22212345678@c.us",
"pushName": "Kaba Shop"
},
"engine": "GOWS",
"manager": {
"retryCount": 0,
"maxRetries": 5,
"queuesPaused": false,
"lastHealthy": "2026-09-30T10:40:00.000Z",
"connectedSince": "2026-09-28T07:12:00.000Z",
"uptime": "2d 3h",
"totalRecoveries": 1,
"totalFailures": 1,
"lastError": null,
"lastErrorAt": null
},
"action": {
"type": "connected",
"message": "Session is connected and operational. All messaging features are available.",
"canLogout": true
}
}
```
**Errors to expect**
- [`NO_SESSION_CONFIGURED`](https://wahasend.com/docs/errors.md#no_session_configured) (400): This account has no connected WhatsApp number.
- [`NOT_FOUND`](https://wahasend.com/docs/errors.md#not_found) (404): Not found.
#### QR code to link
`GET /api/v1/session/qr`
While the state is `SCAN_QR_CODE`: a PNG to scan in WhatsApp → Settings → Linked devices → Link a device. It changes about every minute.
- Unlocked by: every product
- Per-number key: yes
> Already linked: `{ "status": "already_connected" }`. Another state: `{ "status": "STARTING", "message", "hint" }`.
**Query parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `format` | "raw" | no | `raw` returns the QR text instead of an image. |
| `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 https://app.wahasend.com/api/v1/session/qr \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/session/qr', {
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.get(
"https://app.wahasend.com/api/v1/session/qr",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"status": "SCAN_QR_CODE",
"qr": {
"image": "data:image/png;base64,iVBORw0KGgo…",
"expiresInSeconds": 60,
"message": "Scan this QR code with your WhatsApp app. Open WhatsApp > Settings > Linked Devices > Link a Device."
}
}
```
**Errors to expect**
- [`WAHA_UNREACHABLE`](https://wahasend.com/docs/errors.md#waha_unreachable) (503): WhatsApp service is unreachable. Please try again later.
- [`NO_SESSION_CONFIGURED`](https://wahasend.com/docs/errors.md#no_session_configured) (400): This account has no connected WhatsApp number.
#### Pairing code (no QR)
`POST /api/v1/session/pair`
Gets an 8-character code to type in WhatsApp → Linked devices → Link with phone number instead. Handy when the QR is on the same phone.
- Unlocked by: every product
- Per-number key: yes
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `phone` | string | yes | The number being linked, international format. |
| `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/session/pair \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+22212345678"
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/session/pair', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"phone": "+22212345678"
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/session/pair",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"phone": "+22212345678",
},
)
print(r.json())
```
**Response 200**
```json
{
"status": "code_sent",
"code": "ABCD-1234",
"message": "A pairing code has been generated. Enter it in your WhatsApp app: Settings > Linked Devices > Link a Device > Link with phone number instead."
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`WAHA_UNREACHABLE`](https://wahasend.com/docs/errors.md#waha_unreachable) (503): WhatsApp service is unreachable. Please try again later.
- [`WAHA_ERROR`](https://wahasend.com/docs/errors.md#waha_error) (502): WhatsApp could not complete the request. Try again.
#### Start
`POST /api/v1/session/start`
Starts a stopped number. Then fetch the QR or a pairing code if it asks for one.
- Unlocked by: every product
- Per-number key: yes
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `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/session/start \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/session/start', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/session/start",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"status": "starting",
"message": "Session is starting. Use GET /api/v1/session/qr to retrieve the QR code once ready, or POST /api/v1/session/pair for a pairing code."
}
```
**Errors to expect**
- [`WAHA_ERROR`](https://wahasend.com/docs/errors.md#waha_error) (502): WhatsApp could not complete the request. Try again.
- [`NO_SESSION_CONFIGURED`](https://wahasend.com/docs/errors.md#no_session_configured) (400): This account has no connected WhatsApp number.
#### Reconnect
`POST /api/v1/session/reconnect`
Restarts the connection of a number that is stuck. The link to the phone is kept.
- Unlocked by: every product
- Per-number key: yes
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `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/session/reconnect \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/session/reconnect', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/session/reconnect",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"message": "Restart initiated. Waiting for session to connect..."
}
```
**Errors to expect**
- [`WAHA_ERROR`](https://wahasend.com/docs/errors.md#waha_error) (502): WhatsApp could not complete the request. Try again.
#### Unlink
`POST /api/v1/session/logout`
Logs the number out of WhatsApp. Linking it again needs a new QR scan or pairing code.
- Unlocked by: every product
- Per-number key: yes
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `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/session/logout \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/session/logout', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/session/logout",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"status": "logged_out",
"message": "WhatsApp session has been disconnected. You will need to scan a new QR code to reconnect."
}
```
**Errors to expect**
- [`WAHA_ERROR`](https://wahasend.com/docs/errors.md#waha_error) (502): WhatsApp could not complete the request. Try again.
#### Connection history
`GET /api/v1/session/history`
Latest state changes of the number, newest first.
- Unlocked by: every product
- Per-number key: yes
**Query parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | number | no | Default 20, at most 100. |
| `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 https://app.wahasend.com/api/v1/session/history \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/session/history', {
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.get(
"https://app.wahasend.com/api/v1/session/history",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"session": "n6d2b9c1e8f4a4e37",
"events": [
{
"id": "b3f1…",
"status": "WORKING",
"previousStatus": "STARTING",
"reason": null,
"autoRecovery": false,
"metadata": null,
"timestamp": "2026-09-28T07:12:00.000Z"
}
]
}
```
#### Read the profile
`GET /api/v1/session/profile`
The WhatsApp name, "about" text and picture of the number.
- Unlocked by: every product
- Per-number key: yes
**Query parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `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 https://app.wahasend.com/api/v1/session/profile \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/session/profile', {
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.get(
"https://app.wahasend.com/api/v1/session/profile",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"session": "n6d2b9c1e8f4a4e37",
"profile": {
"id": "22212345678@c.us",
"name": "Kaba Shop",
"status": "Open 9:00–18:00",
"picture": "https://pps.whatsapp.net/…"
}
}
```
**Errors to expect**
- [`WAHA_ERROR`](https://wahasend.com/docs/errors.md#waha_error) (502): WhatsApp could not complete the request. Try again.
#### Change the profile
`PUT /api/v1/session/profile`
Any of name, about text and picture. Each is tried on its own; `failed` lists what WhatsApp refused.
- Unlocked by: every product
- Per-number key: yes
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | no | 1–25 characters. |
| `status` | string | no | "About" text, up to 139 characters. |
| `pictureUrl` | string (URL) | no | Public link to a square image. |
| `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 PUT https://app.wahasend.com/api/v1/session/profile \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Kaba Shop",
"status": "Open 9:00–18:00"
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/session/profile', {
method: 'PUT',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"name": "Kaba Shop",
"status": "Open 9:00–18:00"
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.put(
"https://app.wahasend.com/api/v1/session/profile",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"name": "Kaba Shop",
"status": "Open 9:00–18:00",
},
)
print(r.json())
```
**Response 200**
```json
{
"updated": {
"name": "updated",
"status": "updated"
}
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
#### Remove the picture
`DELETE /api/v1/session/profile/picture`
Removes the profile picture.
- Unlocked by: every product
- Per-number key: yes
**Query parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `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 DELETE https://app.wahasend.com/api/v1/session/profile/picture \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/session/profile/picture', {
method: 'DELETE',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.delete(
"https://app.wahasend.com/api/v1/session/profile/picture",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"status": "deleted",
"message": "Profile picture removed."
}
```
**Errors to expect**
- [`WAHA_ERROR`](https://wahasend.com/docs/errors.md#waha_error) (502): WhatsApp could not complete the request. Try again.
---
## Account, history and keys
Who you are, what was sent, and your keys. Every product can read these and create API keys (see [Which products can use the API](https://wahasend.com/docs/authentication.md)).
#### Your account
`GET /api/v1/me`
The organization, its active products and features, and whether a key is set. A quick way to test a key.
- Unlocked by: every product
- Account key only
**Example**
curl:
```bash
curl https://app.wahasend.com/api/v1/me \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/me', {
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.get(
"https://app.wahasend.com/api/v1/me",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"id": "2c7d0e19-4b8a-4f63-9e21-7a5c3d9b1f08",
"name": "Aminetou Sall",
"email": "aminetou@example.com",
"memberRole": "admin",
"organization": {
"id": "2c7d0e19-4b8a-4f63-9e21-7a5c3d9b1f08",
"name": "Kaba Shop",
"requireTwoFactor": false
},
"products": [
"developer"
],
"features": [
"send",
"batch",
"api",
"otp",
"media",
"webhooks"
],
"maxNumbers": null,
"webhookUrl": "https://example.com/hooks/wahasend",
"hasApiKey": true,
"apiKeyPreview": "3f9c1a7b2e4d***",
"…": "…"
}
```
#### Message history
`GET /api/v1/messages`
Messages sent by text sends, batches, campaigns, OTP, reminders and assistants, newest first.
- Unlocked by: every product
- Account key only
> OTP messages show `[OTP MESSAGE]` instead of the code. Media sends appear with their caption or a label such as `[image] https://…`.
> `wahaMessageId` is WhatsApp's id once the message is sent (null before); `containerId` is the number it went from (null on older rows).
**Query parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `type` | "alert" \| "bulk" \| "otp" | no | Filter by kind. |
| `status` | string | no | `queued`, `sending`, `sent`, `delivered`, `read` or `failed`. |
| `search` | string | no | Part of the recipient or text. |
| `page` | number | no | Default 1. |
| `limit` | number | no | Default 50, at most 100. |
**Example**
curl:
```bash
curl "https://app.wahasend.com/api/v1/messages?status=failed&limit=20" \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/messages?status=failed&limit=20', {
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.get(
"https://app.wahasend.com/api/v1/messages?status=failed&limit=20",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"total": 1,
"page": 1,
"limit": 20,
"messages": [
{
"id": "4f0c6a1e-2b7d-4c1a-9e35-7d8f2a6b1c90",
"type": "alert",
"recipient": "22212345678",
"content": "Your order #1042 is ready for pickup.",
"status": "failed",
"priority": "normal",
"error": "Message delivery failed (ACK 0)",
"sentAt": null,
"deliveredAt": null,
"createdAt": "2026-09-30T10:30:10.000Z",
"wahaMessageId": "true_22212345678@c.us_3EB0C767D26A1D2E5A8F",
"containerId": "6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64"
}
]
}
```
#### Counts
`GET /api/v1/stats`
Totals of all messages, by status and kind, plus the last 24 hours and 7 days.
- Unlocked by: every product
- Account key only
**Example**
curl:
```bash
curl https://app.wahasend.com/api/v1/stats \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/stats', {
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.get(
"https://app.wahasend.com/api/v1/stats",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"totalMessages": 1247,
"sent": 30,
"delivered": 1150,
"failed": 47,
"alerts": 900,
"bulk": 320,
"otp": 27,
"last24h": 58,
"last7d": 412
}
```
#### Replace the account key
`POST /api/v1/me/regenerate-key`
Issues a new account key; the old one stops working at once. `POST /me/generate-key` does the same the first time (409 `CONFLICT` once a key exists).
- Unlocked by: every product (`api`)
- Account key only
**Example**
curl:
```bash
curl -X POST https://app.wahasend.com/api/v1/me/regenerate-key \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/me/regenerate-key', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/me/regenerate-key",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"apiKey": "wn_8c1f…64 hex characters…",
"warning": "Save this new API key now. Your old key has been invalidated."
}
```
**Errors to expect**
- [`PRODUCT_REQUIRED`](https://wahasend.com/docs/errors.md#product_required) (403): This feature is not included in your products.
#### Replace a per-number key
`POST /api/v1/session/:containerId/regenerate-key`
New key for one number; the old one stops working. `POST /session/:containerId/generate-key` creates the first one.
- Unlocked by: every product (`api`)
- Account key only
**Path parameters**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `containerId` | uuid | yes | The number, from `GET /session/list`. |
**Example**
curl:
```bash
curl -X POST https://app.wahasend.com/api/v1/session/6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64/regenerate-key \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/session/6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64/regenerate-key', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/session/6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64/regenerate-key",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"apiKey": "waha_V1StGXR8_Z5jdHi6B-myT…",
"containerId": "6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64",
"label": "Shop",
"warning": "Store this key securely. It cannot be retrieved again."
}
```
**Errors to expect**
- [`CONTAINER_NOT_FOUND`](https://wahasend.com/docs/errors.md#container_not_found) (404): WhatsApp number not found.
- [`PRODUCT_REQUIRED`](https://wahasend.com/docs/errors.md#product_required) (403): This feature is not included in your products.
---
## Webhooks
WahaSend posts events to one URL of yours: replies from customers, delivery receipts, a number that disconnects, an assistant that needs a person, a new order. **Developer** gets every event (feature `webhooks`). **Assistant** and **Shop** owners can set a webhook too and get their own product's events: `bot.needs_review` and `bot.handoff`, plus `commerce.order` with Shop. Set the address in the dashboard (**For developers** → Webhook URL) or with the API:
##### Set or clear the webhook URL
`PUT /api/v1/me/webhook`
One URL per organization. `null` stops the events. The first URL also creates the signing secret, returned once here as `signingSecret` (read it again with `GET /me/webhook`).
- Unlocked by: Assistant, Shop, Developer (`webhooks | bots`)
- Account key only
**Request body**
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `webhookUrl` | string (URL) \| null | yes | Public http(s) address, up to 2000 characters. Private and local addresses are refused. |
**Example**
curl:
```bash
curl -X PUT https://app.wahasend.com/api/v1/me/webhook \
-H "X-API-Key: $WAHASEND_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"webhookUrl": "https://example.com/hooks/wahasend-7Qe2xLp9"
}'
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/me/webhook', {
method: 'PUT',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
"webhookUrl": "https://example.com/hooks/wahasend-7Qe2xLp9"
}),
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.put(
"https://app.wahasend.com/api/v1/me/webhook",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
json={
"webhookUrl": "https://example.com/hooks/wahasend-7Qe2xLp9",
},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"webhookUrl": "https://example.com/hooks/wahasend-7Qe2xLp9",
"signingSecret": "whsec_5f0c…64 hex characters…"
}
```
**Errors to expect**
- [`VALIDATION_ERROR`](https://wahasend.com/docs/errors.md#validation_error) (400): Validation failed. Check the details for specific field errors.
- [`PRODUCT_REQUIRED`](https://wahasend.com/docs/errors.md#product_required) (403): This feature is not included in your products.
##### Webhook URL and signing secret
`GET /api/v1/me/webhook`
The URL and the secret to [check signatures](https://wahasend.com/docs/webhooks.md#webhook-security) with. Owners and admins only.
- Unlocked by: Assistant, Shop, Developer (`webhooks | bots`)
- Account key only
> `signingSecret` is null when no URL was ever set (or on a server without encryption set up: deliveries are then unsigned).
**Example**
curl:
```bash
curl https://app.wahasend.com/api/v1/me/webhook \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/me/webhook', {
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.get(
"https://app.wahasend.com/api/v1/me/webhook",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"webhookUrl": "https://example.com/hooks/wahasend-7Qe2xLp9",
"signingSecret": "whsec_5f0c…64 hex characters…"
}
```
**Errors to expect**
- [`PRODUCT_REQUIRED`](https://wahasend.com/docs/errors.md#product_required) (403): This feature is not included in your products.
- [`ROLE_FORBIDDEN`](https://wahasend.com/docs/errors.md#role_forbidden) (403): Your role in this organization does not allow this. Ask an owner or admin.
##### Replace the signing secret
`POST /api/v1/me/webhook/regenerate-secret`
A new secret; deliveries are signed with it at once and the old one no longer verifies. Update your receiver right after.
- Unlocked by: Assistant, Shop, Developer (`webhooks | bots`)
- Account key only
**Example**
curl:
```bash
curl -X POST https://app.wahasend.com/api/v1/me/webhook/regenerate-secret \
-H "X-API-Key: $WAHASEND_API_KEY"
```
JavaScript:
```js
const res = await fetch('https://app.wahasend.com/api/v1/me/webhook/regenerate-secret', {
method: 'POST',
headers: { 'X-API-Key': process.env.WAHASEND_API_KEY },
});
const data = await res.json();
```
Python:
```python
import os
import requests
r = requests.post(
"https://app.wahasend.com/api/v1/me/webhook/regenerate-secret",
headers={"X-API-Key": os.environ["WAHASEND_API_KEY"]},
)
print(r.json())
```
**Response 200**
```json
{
"success": true,
"signingSecret": "whsec_91ab…64 hex characters…",
"warning": "Update your receiver with this secret now. Deliveries are signed with it from now on."
}
```
**Errors to expect**
- [`PRODUCT_REQUIRED`](https://wahasend.com/docs/errors.md#product_required) (403): This feature is not included in your products.
- [`WEBHOOK_SIGNING_UNAVAILABLE`](https://wahasend.com/docs/errors.md#webhook_signing_unavailable) (503): Webhook signing is not available on this server yet (SECRETS_ENCRYPTION_KEY is not set).
#### How events are delivered
- `POST` with `Content-Type: application/json` and `User-Agent: WahaSend/1.0`, body `{ event, data, timestamp }`.
- Headers: `X-WahaSend-Event` (the event name), `X-WahaSend-Delivery` (one id per event, the same on every retry: use it to drop duplicates) and `X-WahaSend-Signature` ([check it](https://wahasend.com/docs/webhooks.md#webhook-security)).
- Answer any 2xx within 10 seconds. Anything else (or a timeout, or a redirect: redirects are not followed) is retried: 5 attempts in all, waiting about 5 s, 10 s, 20 s and 40 s between them. Then the event is dropped.
- Events go to the URL set at delivery time; a private or local address is refused then too.
- Order is not guaranteed and an event can come twice: make your handler idempotent (use `messageId` / `wahaMessageId`).
Every event:
```json
{
"event": "message.delivered",
"data": { … },
"timestamp": "2026-09-30T10:30:15.204Z"
}
```
#### Checking that it's us
Every event is signed with your organization's **signing secret** (`whsec_…`, made with your first webhook URL; see it in the dashboard or with `GET /me/webhook`). The header looks like this:
```http
X-WahaSend-Signature: t=1790762475,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
```
1. Read `t` (Unix seconds) and `v1` from the header.
2. Compute HMAC-SHA256 with your secret over `t + "." + body`, where body is the **raw** request body exactly as received (not re-serialized JSON). Hex-encode it.
3. Compare it with `v1` in constant time. Refuse the event if they differ.
4. Refuse it too if `t` is more than 5 minutes away from your clock: an old event replayed by someone else then fails.
Node.js (Express):
```js
const crypto = require('crypto');
const express = require('express');
const app = express();
const SECRET = process.env.WAHASEND_WEBHOOK_SECRET; // whsec_…
function verify(rawBody, header, toleranceSeconds = 300) {
const parts = Object.fromEntries((header || '').split(',').map(p => p.split('=')));
const t = Number(parts.t);
if (!t || !parts.v1 || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${rawBody}`).digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(parts.v1, 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// express.raw keeps the body as bytes: sign-check those, then parse
app.post('/hooks/wahasend', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8');
if (!verify(raw, req.get('X-WahaSend-Signature'))) return res.status(400).end();
const { event, data } = JSON.parse(raw);
// … handle it (dedupe on req.get('X-WahaSend-Delivery'))
res.sendStatus(200);
});
```
Python (Flask):
```python
import hashlib, hmac, os, time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["WAHASEND_WEBHOOK_SECRET"].encode() # whsec_…
def verify(raw_body: bytes, header: str, tolerance_seconds: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in (header or "").split(",") if "=" in p)
try:
t = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - t) > tolerance_seconds:
return False
expected = hmac.new(SECRET, f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
@app.post("/hooks/wahasend")
def wahasend():
raw = request.get_data() # the raw bytes, before any JSON parsing
if not verify(raw, request.headers.get("X-WahaSend-Signature", "")):
abort(400)
event = request.get_json()
# … handle it (dedupe on request.headers["X-WahaSend-Delivery"])
return "", 200
```
> Replace the secret with **New key** in the dashboard (or `POST /me/webhook/regenerate-secret`): events are signed with the new one at once, so update your receiver right after. Keep using HTTPS for the URL.
#### Events
| Event | When | Who gets it |
| --- | --- | --- |
| `message.inbound` | Someone wrote to your number. | Developer |
| `message.delivered` · `message.read` · `message.failed` | A message sent by text send, media send, batch, campaign, OTP, reminder or assistant changed status. (`message.sent` is rare: sends are already marked sent.) | Developer |
| `poll.vote` | Someone voted in a poll you sent. | Developer |
| `session.needs_scan` · `session.disconnected` · `session.recovered` | A number must be linked again, went down, or came back. | Developer |
| `bot.needs_review` · `bot.handoff` | An assistant could not answer, or handed the chat to a person. | Assistant, Shop or Developer |
| `commerce.order` | A customer placed a cash-on-delivery order with the shop assistant. | Shop or Developer |
message.inbound:
```json
{
"event": "message.inbound",
"data": {
"sender": "22212345678",
"senderName": "Aïcha",
"body": "Is the blue dress still available?",
"messageType": "chat",
"wahaMessageId": "false_22212345678@c.us_3A1F9C2B7E",
"containerId": "6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64",
"numberPhone": "22200000000",
"timestamp": "2026-09-30T10:41:07.512Z"
},
"timestamp": "2026-09-30T10:41:07.790Z"
}
```
- `sender` is the phone digits, or the `…@lid` id when WhatsApp hides the number.
- Optional fields: `isGroup` + `groupId` (group messages), `latitude` + `longitude` (a location, `messageType: "location"`), `selectedButtonId`, `selectedRowId`.
- Messages without text (a photo with no caption, a sticker) are not sent as events. A voice note arrives only on accounts with an assistant, as its transcript (`messageType: "voice"`).
- `containerId` and `numberPhone` say which of your numbers received it (`numberPhone` is null for a number not linked yet).
message.delivered (also .read, .failed):
```json
{
"event": "message.delivered",
"data": {
"messageId": "4f0c6a1e-2b7d-4c1a-9e35-7d8f2a6b1c90",
"recipient": "22212345678",
"type": "alert",
"status": "delivered",
"wahaMessageId": "true_22212345678@c.us_3EB0C767D26A1D2E5A8F",
"containerId": "6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64",
"numberPhone": "22200000000",
"timestamp": "2026-09-30T10:30:15.101Z"
},
"timestamp": "2026-09-30T10:30:15.204Z"
}
```
`type` is `alert` (text and media sends, batches, reminders), `bulk` (campaigns, with `campaignId` added), `otp` or `bot_reply`. `containerId` / `numberPhone` name the number it went from (null on messages from before they were recorded).
session.disconnected:
```json
{
"event": "session.disconnected",
"data": {
"status": "FAILED",
"message": "Your WhatsApp number has disconnected and automatic recovery failed. Please reconnect it from your dashboard.",
"retriesExhausted": true,
"lastAttempt": "2026-09-30T03:12:40.000Z",
"containerId": "6d2b9c1e-8f4a-4e37-a5c2-1b9e7d3f0a64",
"numberPhone": "22200000000",
"session": "n6d2b9c1e8f4a4e37",
"timestamp": "2026-09-30T03:12:40.020Z"
},
"timestamp": "2026-09-30T03:12:40.300Z"
}
```
When the WhatsApp server hosting the number is down, `session.disconnected` carries `"reason": "instance_down"` instead of `retriesExhausted`; the number comes back by itself. `session.needs_scan` carries `"status": "SCAN_QR_CODE"`; `session.recovered` carries `"status": "WORKING"` and `recoveredAt`. All three have `containerId`, `numberPhone` and `session`.
bot.needs_review / bot.handoff:
```json
{
"event": "bot.needs_review",
"data": {
"botId": "8b2e…",
"conversationId": "f41c…",
"contactPhone": "22212345678",
"question": "Do you deliver to Rosso?",
"lang": "fr",
"timestamp": "2026-09-30T11:02:10.000Z"
},
"timestamp": "2026-09-30T11:02:10.180Z"
}
```
`reason` is added when it is not a plain "couldn't answer": `"away"` (outside business hours; no `lang` then) or `"ai_quota"` (the month's AI allowance is used up).
commerce.order:
```json
{
"event": "commerce.order",
"data": {
"botId": "8b2e…",
"contactPhone": "22212345678",
"orderNumber": "A-1042",
"total": 12500,
"currency": "MRU",
"timestamp": "2026-09-30T12:20:44.000Z"
},
"timestamp": "2026-09-30T12:20:44.260Z"
}
```
`poll.vote` passes on WhatsApp's vote as it comes (`vote`, `poll`) with your `containerId` and `numberPhone` added.
---
## Connect: Claude and other AI apps (MCP)
With **Connect**, an AI app works your WhatsApp number for you: it reads your chats, drafts and sends replies, prospects, sends and opens files. It speaks MCP (Model Context Protocol) over HTTP. Server address:
```text
https://app.wahasend.com/api/v1/mcp
```
The AI does the thinking and the writing (in its own account); WahaSend only moves messages and applies the [sending limits](https://wahasend.com/docs/connect.md#connect-limits).
#### 1. claude.ai, Claude Desktop and the Claude phone app (sign-in, no key)
1. In Claude, open **Settings → Connectors** and tap **Add custom connector**.
2. Name: `WahaSend`. Link: `https://app.wahasend.com/api/v1/mcp`.
3. Tap **Connect**, sign in to WahaSend, choose the WhatsApp numbers Claude may use (one, several, or **All my numbers**, which includes numbers you add later) and tap **Allow**.
Behind it is standard OAuth 2.1 (authorization code + PKCE, dynamic client registration, scope `whatsapp`). Access tokens last 60 minutes and renew themselves for 30 days. The connection reaches only the numbers you chose, and works only on `/mcp`. Only owners and admins can allow it. On the **Connect** page you see connected apps with their numbers, change their numbers without reconnecting (it applies at the next request), or disconnect them; a password change or leaving the organization also ends them. A deleted number leaves the apps that had it; an app left with no number is disconnected.
#### 2. Claude Code, Cursor, Codex (with your key)
Replace `YOUR_KEY` with your account key (dashboard → **Connect**).
Claude Code:
```bash
claude mcp add --transport http wahasend https://app.wahasend.com/api/v1/mcp --header "Authorization: Bearer YOUR_KEY"
```
Claude Desktop or Cursor (MCP settings):
```json
{
"mcpServers": {
"wahasend": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://app.wahasend.com/api/v1/mcp",
"--header",
"Authorization: Bearer YOUR_KEY"
]
}
}
}
```
Codex: ~/.codex/config.toml:
```toml
[mcp_servers.wahasend]
url = "https://app.wahasend.com/api/v1/mcp"
bearer_token_env_var = "WAHASEND_API_KEY"
```
…then, in the terminal where you start Codex:
```bash
export WAHASEND_API_KEY=YOUR_KEY
```
Codex can also sign in without a key: `codex mcp add wahasend --url https://app.wahasend.com/api/v1/mcp` opens your browser; `codex mcp login wahasend` signs in again later. Any other MCP client: give it the link and send the key as `Authorization: Bearer` (or `X-API-Key`).
#### 3. ChatGPT
1. Use ChatGPT on the web with a paid plan (Plus, Pro, Business, Enterprise or Education); the phone app can't add apps.
2. In **Settings**, turn on **Developer mode**.
3. Create an app named `WahaSend` with the link `https://app.wahasend.com/api/v1/mcp` and Authentication **OAuth**.
4. Sign in to WahaSend, choose the numbers it may use and tap **Allow**. In a chat, open **+** → Developer mode → WahaSend.
#### Several WhatsApp numbers
One connection can work several of your numbers. Which ones: a Claude sign-in reaches the numbers chosen when you allowed it (or all of them); the account key reaches every number of the organization; a per-number key only its own. No tool ever sees or uses another number.
- `list_numbers` gives each number its id, phone and name (label), whether it is connected, whether WhatsApp may have blocked it, whether it is the default, and what it may still send today.
- Chats, replies and messages say which number they are on (`number`: id, phone, label). A contact who wrote to two numbers has two conversations. `list_chats`, `read_chat`, `new_replies` and `find_contact` take an optional `number` (its id, phone or label) to look at one number only.
- `send_message`, `send_attachment` and `send_to_many` take an optional `from` (id, phone or label). Without it, a reply goes out from the number the contact last talked with, and a first message from your default number. When several numbers could send and none is the default, the tool answers with the candidates and the AI asks you. The result says which number sent (`number`) and why (`numberChosenBy`).
- Limits stay per number: each number has its own spacing, hourly and daily caps and new-contact limit, with your organization's settings. A message is never moved to another number because one is limited, disconnected or may be blocked ([send groups](https://wahasend.com/docs/phone-numbers.md#several-numbers) don't apply to Connect). "New contact" means new to the sending number.
- `check_numbers` and `daily_allowance` take an optional `number`; `daily_allowance` without one lists every number. `open_attachment` fetches the file through the number its message is on.
#### Tools
The server gives the AI these tools and a set of rules (ask before a first message, reply in the contact's language, never work around a limit…). Tool answers are JSON; a refused send says why and when it lifts. There is also a `prospecting` prompt (arguments `offer`, `language`).
Descriptions below are what the AI reads, in English, with the recommended limits.
##### list_numbers
List my WhatsApp numbers. The user's WhatsApp numbers this connection may use: name (label), phone, whether it is connected, whether WhatsApp may have blocked it, whether it is the default, and what it may still send today. Every other tool names numbers the same way (id, phone or label).
No input.
##### send_message
Send a WhatsApp message. Send one text message from one of the user's WhatsApp numbers. From: pass `from` (a number's id, phone or label, see list_numbers) to choose which of the user's numbers sends; without it, a reply goes out from the number this contact last talked with, a first message from the default number — when that is unclear the result lists the numbers: ask the user. It never switches to another number by itself. Messages are spaced 30–90 s apart, capped at 60 an hour, 200 a day; the result says when it will go out. A first message to a new contact needs confirmed=true (after the user approved it) and counts against a small daily limit of that number.
- `to` string, required (1–100 chars): A phone number (+34612345678), a WhatsApp id exactly as returned (…@lid), or a contact name ("Percy")
- `text` string, required (1–4096 chars)
- `from` string, optional (1–100 chars): Which of the user's numbers sends (id, phone or label, see list_numbers). Omit to reply from the number the contact last talked with (or the default number for someone new).
- `confirmed` boolean, optional: Required for a first message to a new contact: true only after the user approved this recipient, number and text
##### send_attachment
Send a file on WhatsApp. Send one file (photo, PDF or other document, voice note, video) from one of the user's WhatsApp numbers, with an optional caption. Give exactly one source: forward_from (a message with a file in that number's chats, by the ids read_chat / new_replies return — WhatsApp forwards only within one number), url (a public https link to the file) or content_base64 + filename + mimetype (a file you made or the user gave you). The kind is chosen from the file: JPEG/PNG/WebP → photo, ogg/opus → voice note, MP4 → video, anything else allowed → document. Programs, scripts, web pages and archives are refused. From: pass `from` (a number's id, phone or label, see list_numbers) to choose which of the user's numbers sends; without it, a reply goes out from the number this contact last talked with, a first message from the default number — when that is unclear the result lists the numbers: ask the user. It never switches to another number by itself. Same rules as send_message: Messages are spaced 30–90 s apart, capped at 60 an hour, 200 a day; the result says when it will go out.
- `to` string, required (1–100 chars): A phone number (+34612345678), a WhatsApp id exactly as returned (…@lid), or a contact name ("Percy")
- `caption` string, optional (0–1024 chars): Text shown with the file (not for voice notes)
- `from` string, optional (1–100 chars): Which of the user's numbers sends (id, phone or label, see list_numbers). Omit to reply from the number the contact last talked with (or the default number for someone new).
- `confirmed` boolean, optional: true only after the user approved this recipient, number, file and caption. Required for a first message to a new contact, and to forward a file someone sent to another person
- `forward_from` object, optional
- `forward_from.chat` string, required (1–100 chars): The chat the file is in: phone, WhatsApp id (…@lid) or contact name
- `forward_from.message_id` uuid, required: The `id` of the message with the file, as read_chat / new_replies returned it
- `url` string (URL), optional (0–2048 chars): A public https link to the file itself
- `content_base64` string, optional (4–6991531 chars): The file's bytes, base64 (at most 5 MB)
- `filename` string, optional (1–200 chars): With content_base64: the file name, e.g. "price-list.pdf"
- `mimetype` string, optional (3–128 chars): With content_base64: its type, e.g. "application/pdf", "text/csv"
##### send_to_many
Send a message to several contacts. Send a text to a list of contacts, all from one of the user's numbers (from; the default number when omitted — never split across numbers). They go out one by one, 30–90 s apart; the result lists when each is scheduled and which could not be sent because a limit of that number was reached. {{name}} in the text is replaced per recipient. Requires confirmed=true, which you may only set after showing the user the recipients, the number and the text and getting their OK.
- `recipients` object[], required (1–50 items)
- `recipients[].phone` string, required (7–64 chars): Phone number in international format (+34612345678), or the WhatsApp id exactly as list_chats / new_replies returned it (e.g. 12345678901234@lid). Never strip "@lid".
- `recipients[].name` string, optional (0–100 chars)
- `text` string, required (1–4096 chars): May contain {{name}}
- `from` string, optional (1–100 chars): Which of the user's numbers sends (id, phone or label, see list_numbers). Omit to reply from the number the contact last talked with (or the default number for someone new).
- `confirmed` boolean, optional: Required: true only after the user approved this exact list, number and text
##### check_numbers
Check which numbers are on WhatsApp. Tell valid WhatsApp numbers from invalid ones before messaging. Does not send anything. Checked through one of the user's numbers (`number`; the default one when omitted).
- `phones` string[], required (1–500 items)
- `number` string, optional (1–100 chars): The user's number to check through (id, phone or label); people who already talked to it need no check
##### find_contact
Find a contact. Look someone up in the contacts of the user's numbers by name or part of their number. Returns their phone (and WhatsApp id when known) and which of the user's numbers know them. Use it before messaging someone the user names.
- `query` string, required (1–100 chars)
- `limit` integer, optional (1–50)
- `number` string, optional (1–100 chars): Only the contacts of this one of the user's numbers (id, phone or label)
##### list_chats
List conversations. Conversations of the user's numbers, newest first, with unread counts — one per contact and number (`number` says which of the user's numbers it is on). `phone` is either digits or a WhatsApp id ending in @lid; use it as-is to reply, from the same number.
- `limit` integer, optional (1–200)
- `search` string, optional (0–100 chars): Filter by phone or name
- `number` string, optional (1–100 chars): Only the conversations of this one of the user's numbers (id, phone or label)
##### read_chat
Read a conversation. The exchange with one contact, oldest first: what was sent (with delivery status) and what they replied. Each message has an `id` and the `number` of the user's it is on; messages with a file carry an `attachment` label — pass the id to open_attachment to see it. When the contact talked with several of the user's numbers, pass `number` to read one conversation; never mix them.
- `phone` string, required (7–64 chars): Phone number in international format (+34612345678), or the WhatsApp id exactly as list_chats / new_replies returned it (e.g. 12345678901234@lid). Never strip "@lid".
- `limit` integer, optional (1–500)
- `number` string, optional (1–100 chars): The one of the user's numbers whose conversation to read (id, phone or label)
##### open_attachment
Open an attachment. Open the file attached to one WhatsApp message: a photo is shown to you, a PDF or Word (.docx) file comes back as text, a voice note as a transcript. Videos and other files: details only. Use only when the file matters for what the user asked. The file is fetched through the number the message is on.
- `chat` string, required (1–100 chars): The contact: phone number, WhatsApp id exactly as returned (…@lid), or a contact name — the same chat you read with read_chat
- `message_id` uuid, required: The `id` of the message, as read_chat / new_replies returned it
##### new_replies
Get new replies. Everything received that has not been read yet, oldest first, each with the `number` of the user's it came in on (reply from that number). By default marks them as read so the next call returns only newer messages.
- `mark_read` boolean, optional (default true)
- `limit` integer, optional (1–500)
- `number` string, optional (1–100 chars): Only what came in on this one of the user's numbers (id, phone or label)
##### delivery_status
Check delivery status. Whether messages were sent, delivered, read or failed.
- `message_ids` uuid[], required (1–200 items)
##### daily_allowance
Daily allowance. What a number may still send: first messages to new contacts today, messages this hour and today, and when the next message can go out. Limits are per number. Without `number`, every number this connection may use.
- `number` string, optional (1–100 chars): One of the user's numbers (id, phone or label)
#### Sending limits
Every message an AI sends goes through a limiter per number: messages are spaced, capped per hour and per day, and first messages to new contacts are capped by the number's age. These defaults follow published guidance (WhatsApp's "about 200 new customers a day" and the practice of WhatsApp API providers). They were added after an AI tool sent 35 messages in two minutes and the number was banned.
| Protection | Recommended value |
| --- | --- |
| Sending hours | 09:00–17:00 (Africa/Nouakchott) |
| Spacing | 30–90 s between two messages |
| Per hour | 60 messages |
| Per day | 200 messages |
| First messages to new contacts, per day, by the number's age | 20 (days 0–9), 50 (days 10–19), 100 (days 20–29), 200 (from day 30) |
| Quiet start | none in the first 3 days after linking |
| Rest day | replies only after 3 days in a row of first messages |
| Reply rate | first messages pause when fewer than 30% of the last 7 days' got a reply (judged from 8 messages) |
| Copy-paste | the same first message to at most 3 new people a day |
| Opt-out | never message someone who asked to stop |
| Approval | a first message to a new contact needs your OK (`confirmed: true`) |
The organization's **owner** can change or turn off each of them (dashboard → **Connect** → More settings → Sending limits, or `PUT /mcp/safety` signed in; `GET /mcp/safety` shows the current and recommended values). The AI is told the actual values.
> WhatsApp alone decides which numbers it blocks. The recommended values lower the risk; no tool can remove it. Changing them is your choice and your responsibility, and WahaSend is not responsible for a blocked number.
#### Files
- **Open** (`open_attachment`): a photo is shown to the AI (up to 3.75 MB), a PDF or Word file comes back as text (30,000 characters max), a voice note as a transcript, a video as details only. Files up to 10 MB. The file is never stored; each opening is logged without its content. Voice notes use Connect's monthly AI allowance.
- **Send** (`send_attachment`): forward a file from your chats, a public **https** link (up to 16 MB) or bytes from the AI (up to 5 MB). Allowed: photos (JPEG, PNG, WebP), voice notes (OGG/Opus), MP4 and 3GP/MOV videos, PDF, Office and OpenDocument files without macros, RTF, TXT, CSV, GIF, audio. Programs, scripts, web pages, archives and macro files are refused; the first bytes must match the type.
- A file counts like a message for every limit. Forwarding a file someone sent you to another person needs your explicit OK.
- WhatsApp keeps files for a limited time: an old file may answer `ATTACHMENT_EXPIRED`.
---
## Errors and rate limits
Every error answers with the same envelope and an `X-Request-Id` header:
400 VALIDATION_ERROR:
```json
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation failed. Check the details for specific field errors.",
"details": { "phone": ["Invalid recipient. Use an international phone number (e.g. +22212345678) or a WhatsApp id ending in @lid."] },
"issues": [{ "field": "phone", "code": "custom", "message": "Invalid recipient. …" }],
"requestId": "7c9e2f4a-1b3d-4e5f-8a6b-0c1d2e3f4a5b"
}
}
```
- `code` is stable: match on it, never on `message` (the wording may change).
- `details` adds facts when there are some (`retryAfterSeconds`, `attemptsRemaining`, `products`…). `issues` lists each bad field of a `VALIDATION_ERROR` with a code such as `required`, `too_big` (with `maximum`) or `invalid_uuid`.
- `requestId` (same as the `X-Request-Id` header): quote it when you contact support, it lets us find your request.
- 429 and 503 answers carry `Retry-After` (seconds) when the wait is known. Wait that long before trying again.
- 5xx: safe to retry a read. Before retrying a send, check `GET /messages`: a timeout may have gone out.
#### Rate limits
| Limit | Value | When exceeded |
| --- | --- | --- |
| Changes per organization (POST, PUT, PATCH, DELETE; reads are free), shared by every key and person | 300 a minute (bursts up to 300) | 429 `RATE_LIMITED`, `details.scope: "organization"`, `retryAfterSeconds`, `limitPerMinute`, `Retry-After` |
| Text sends per number | 60 a minute (`high` priority: no cap) | Queued, sent later; never refused |
| Campaign messages | 20 a minute per number, 60 a minute per organization, 3–7 s apart | Queued, sent later |
| Verification codes ([OTP](https://wahasend.com/docs/errors.md#otp-limits)) | Per phone: 1 every 30 s, 3 per 10 min, 10 per 24 h; 60 a minute per organization | 429 `OTP_RATE_LIMITED`, `Retry-After` |
| AI sends through Connect (MCP) | See [sending limits](https://wahasend.com/docs/connect.md#connect-limits) | The tool answers "not sent" with the reason |
| Messages during the [free trial](https://wahasend.com/docs/errors.md#trial-limits) | 100 a day per organization (UTC day), every kind of send counted | 429 `TRIAL_LIMIT_REACHED`, `details.limit`, `used`, `resetsAt`, `Retry-After` |
| Sign-in | 20 attempts per 15 min per IP address; 10 wrong passwords lock the email for 15 min | 429 `RATE_LIMIT` / `TOO_MANY_LOGIN_ATTEMPTS` |
| Claude sign-in (OAuth) | Register 60/h per IP; token 120 per 15 min per app; authorize and revoke 60 per 15 min | 429 `RATE_LIMIT` |
#### OTP limits
So that nobody can use your sign-up form to flood a phone with codes (and get your number reported), `POST /otp/send` is limited per destination phone within your organization: one code every 30 seconds, 3 per 10 minutes and 10 per 24 hours; and 60 codes a minute for the whole organization. The windows slide (they are not calendar minutes or days). Over a limit the answer is 429 `OTP_RATE_LIMITED` with `Retry-After` and `details`: `scope` (`phone` or `organization`), `reason` (`cooldown`, `window`, `day` or `organization`), `limit` and `retryAfterSeconds`. Show the person when they can ask again; don't retry automatically.
#### Free trial limits
A new account gets the products it asked for free for 7 days (one trial per person and per product). While every product of the organization is on its trial, it can send 100 messages a day through WahaSend — single sends, batches (every recipient counts), campaigns (counted when you start them), reminders, OTP codes, media sends, assistant replies and Connect sends together — and its AI allowance is $2 for the whole trial. The count starts again at 00:00 UTC. A send that doesn't fit is refused, nothing is queued: 429 `TRIAL_LIMIT_REACHED` with `Retry-After` (seconds until midnight UTC) and `details`: `limit`, `used`, `requested`, `retryAfterSeconds`, `resetsAt`. A batch or campaign larger than what is left is refused whole. Once a product is paid, the limit is gone. When the trial ends, the product pauses (403 `PRODUCT_REQUIRED`) until it is paid; nothing is deleted.
#### Every error code
Generated from the server's error catalogue. Codes are never renamed or removed; new ones may appear.
##### VALIDATION_ERROR
HTTP 400 · Validation failed. Check the details for specific field errors.
##### INVALID_JSON
HTTP 400 · Request body contains invalid JSON.
##### INVALID_ID
HTTP 400 · Invalid identifier.
##### PAYLOAD_TOO_LARGE
HTTP 413 · The request is too large.
##### FILE_TOO_LARGE
HTTP 413 · The file is too large.
##### ROUTE_NOT_FOUND
HTTP 404 · This API route does not exist. Check /api for available endpoints.
##### NOT_FOUND
HTTP 404 · Not found.
##### CONFLICT
HTTP 409 · This resource already exists.
##### REFERENCE_ERROR
HTTP 400 · Referenced resource does not exist.
##### IN_USE
HTTP 409 · This is still used elsewhere and cannot be removed.
##### CONSTRAINT_VIOLATION
HTTP 400 · Operation violates a data constraint.
##### FORBIDDEN_ACTION
HTTP 400 · This action is not allowed.
##### RATE_LIMIT
HTTP 429 · Too many requests. Slow down and try again.
##### RATE_LIMITED
HTTP 429 · Your organization is sending too many requests. Wait a moment and try again.
##### AUTH_ERROR
HTTP 401 · Authentication required.
##### TOO_MANY_LOGIN_ATTEMPTS
HTTP 429 · Too many wrong passwords for this account. Try again in 15 minutes.
##### ACCOUNT_DEACTIVATED
HTTP 403 · Account is deactivated. Contact support.
##### OAUTH_TOKEN_SCOPE
HTTP 403 · This Claude connection can only be used by Claude (MCP endpoint).
##### OAUTH_REQUEST_INVALID
HTTP 400 · This request from Claude has expired or was already answered. Start again from Claude.
##### NUMBER_KEY_SCOPE
HTTP 403 · This API key belongs to one WhatsApp number and can only send and read through it.
##### USER_REQUIRED
HTTP 403 · Sign in to the dashboard to do this; API keys cannot.
##### WRONG_PASSWORD
HTTP 400 · The password is not correct.
##### NO_PASSWORD_SET
HTTP 400 · No password set. Ask the admin to reset your password.
##### INVALID_LINK
HTTP 400 · This link has expired or was already used. Ask for a new one.
##### SIGNUP_DISABLED
HTTP 403 · Sign-up is currently closed.
##### TWO_FACTOR_REQUIRED
HTTP 403 · This organization requires two-step sign-in. Turn it on in your account first.
##### TWO_FACTOR_CODE_INVALID
HTTP 400 · The code is not correct. Check your authenticator app and try again.
##### TWO_FACTOR_CHALLENGE_EXPIRED
HTTP 400 · This sign-in has expired. Enter your email and password again.
##### TWO_FACTOR_SETUP_EXPIRED
HTTP 400 · The set-up has expired. Start again.
##### TWO_FACTOR_ALREADY_ENABLED
HTTP 409 · Two-step sign-in is already on.
##### TWO_FACTOR_NOT_ENABLED
HTTP 409 · Two-step sign-in is not on.
##### TWO_FACTOR_OWNER_FIRST
HTTP 409 · Turn on two-step sign-in for your own account first.
##### TWO_FACTOR_UNAVAILABLE
HTTP 503 · Two-step sign-in is not available on this server yet.
##### COUNTRY_LOCKED
HTTP 409 · Your country can no longer be changed here once you have paid. Write to us to change it.
##### PRODUCT_REQUIRED
HTTP 403 · This feature is not included in your products.
##### NO_ACTIVE_PRODUCT
HTTP 403 · Your account has no active product yet. Contact us to activate one.
##### ADMIN_ONLY
HTTP 403 · This action is restricted to the platform admin.
##### ROLE_FORBIDDEN
HTTP 403 · Your role in this organization does not allow this. Ask an owner or admin.
##### ADMIN_CANNOT_DELETE
HTTP 403 · The platform admin organization cannot be deleted.
##### INVITE_INVALID
HTTP 400 · This invitation has expired, was cancelled or was already used. Ask for a new one.
##### INVITE_EMAIL_MISMATCH
HTTP 403 · This invitation was sent to another email address. Sign in with that address.
##### ALREADY_MEMBER
HTTP 409 · This person is already in your team.
##### TEAM_FULL
HTTP 409 · The team is full.
##### OWNER_ROLE_FIXED
HTTP 409 · The owner stays owner. Transfer the ownership first.
##### CANNOT_REMOVE_OWNER
HTTP 409 · The owner cannot be removed. Transfer the ownership first.
##### OWNER_CANNOT_LEAVE
HTTP 409 · You own this organization. Transfer the ownership before leaving.
##### LAST_ORGANIZATION
HTTP 409 · This is your only organization. Delete your account instead.
##### NOT_OWNER
HTTP 403 · Only the owner can do this.
##### ALREADY_OWNER
HTTP 409 · You already own this organization.
##### OWNS_ORGANIZATION
HTTP 409 · You own an organization. Transfer it to someone else first.
##### USE_LEAVE
HTTP 400 · Use "Leave this organization" to remove yourself.
##### TOO_MANY_ORGANIZATIONS
HTTP 409 · You can own up to 10 organizations.
##### NO_SESSION_CONFIGURED
HTTP 400 · This account has no connected WhatsApp number.
##### SESSION_NOT_CONNECTED
HTTP 409 · WhatsApp is not connected. Reconnect the number.
##### CONTAINER_NOT_FOUND
HTTP 404 · WhatsApp number not found.
##### NUMBER_LIMIT_REACHED
HTTP 403 · This account uses all the WhatsApp numbers it may have (Developer and Connect include 3). Add extra numbers, or remove a number.
##### PORT_EXHAUSTED
HTTP 503 · No room for a new WhatsApp number right now. Contact support.
##### NUMBER_NOT_ON_WHATSAPP
HTTP 400 · This number is not on WhatsApp.
##### NO_VALID_NUMBERS
HTTP 400 · None of these numbers are on WhatsApp.
##### NUMBER_ON_OTHER_ASSISTANT
HTTP 409 · This WhatsApp number already answers for another assistant that is on. Remove it there first, or turn that assistant off.
##### SECRETS_KEY_MISSING
HTTP 503 · SECRETS_ENCRYPTION_KEY must be set to give WhatsApp servers their own keys.
##### NUMBER_VALIDATION_UNAVAILABLE
HTTP 503 · Numbers cannot be checked right now. Try again shortly.
##### OTP_EXPIRED
HTTP 410 · OTP has expired. Request a new code.
##### OTP_ALREADY_VERIFIED
HTTP 400 · OTP has already been verified.
##### OTP_MAX_ATTEMPTS
HTTP 429 · Maximum verification attempts exceeded. Request a new code.
##### OTP_INVALID_CODE
HTTP 400 · Invalid code.
##### OTP_RATE_LIMITED
HTTP 429 · Too many verification codes for this number or this account. Wait before sending another one.
##### OTP_TEMPLATE_MISSING_CODE
HTTP 400 · The message template must contain {{code}}, where the code goes.
##### MESSAGE_NOT_FOUND
HTTP 404 · No message with this id was sent by this account.
##### MESSAGE_NOT_SENT
HTTP 409 · This message has not been sent yet (or it failed). Only sent messages can be edited or deleted.
##### MESSAGE_WRONG_NUMBER
HTTP 403 · This message was sent from another WhatsApp number than the one this request uses.
##### CAMPAIGN_INVALID_STATE
HTTP 400 · The campaign cannot do this in its current state.
##### STAFF_PHONE_LIMIT
HTTP 409 · You can add up to 10 phones for orders.
##### STAFF_PHONE_IS_BUSINESS
HTTP 400 · This is your business WhatsApp number. Add another phone: yours or a staff member's.
##### WEBHOOK_SIGNING_UNAVAILABLE
HTTP 503 · Webhook signing is not available on this server yet (SECRETS_ENCRYPTION_KEY is not set).
##### ATTACHMENT_NOT_FOUND
HTTP 404 · This message was not found in this chat, or it has no attachment.
##### ATTACHMENT_EXPIRED
HTTP 410 · This file is no longer on WhatsApp (older attachments expire). Ask the contact to send it again.
##### ATTACHMENT_TOO_LARGE
HTTP 413 · This file is too large to open here.
##### ATTACHMENT_UNSUPPORTED
HTTP 415 · This kind of file cannot be opened here.
##### TRANSCRIPTION_UNAVAILABLE
HTTP 503 · Voice notes cannot be read because speech-to-text is not enabled on this server.
##### SEND_FILE_TOO_LARGE
HTTP 413 · This file is too large to send on WhatsApp from here.
##### SEND_FILE_TYPE_NOT_ALLOWED
HTTP 415 · This kind of file cannot be sent. Photos, PDFs, Office files, text and CSV files, voice notes, audio and MP4 videos can.
##### SEND_FILE_URL_UNREACHABLE
HTTP 422 · The file at this link cannot be fetched. Use a public https link to the file itself.
##### WAHA_ERROR
HTTP 502 · WhatsApp could not complete the request. Try again.
##### WAHA_SESSION_ERROR
HTTP 503 · WhatsApp is not connected.
##### WAHA_UNREACHABLE
HTTP 503 · WhatsApp service is unreachable. Please try again later.
##### UPSTREAM_TIMEOUT
HTTP 504 · A service we depend on took too long to answer. Try again.
##### AI_UNAVAILABLE
HTTP 503 · The AI service is not available right now. Try again shortly.
##### AI_BUSY
HTTP 503 · The AI service is busy. Try again in a moment.
##### SERVICE_BUSY
HTTP 503 · The service is busy. Try again in a moment.
##### AI_QUOTA_EXCEEDED
HTTP 402 · This month's AI allowance is used up. It renews on the 1st, or contact us to raise it.
##### TRIAL_LIMIT_REACHED
HTTP 429 · Free trial: 100 messages a day. Activate your product to remove this limit.
##### PADDLE_NOT_CONFIGURED
HTTP 503 · Card payment is not available on this server. Use another way to pay.
##### PADDLE_NO_SUBSCRIPTION
HTTP 404 · This organization has no card payment to manage.
##### PADDLE_UNAVAILABLE
HTTP 502 · The card payment service did not answer. Try again in a moment.
##### PADDLE_SIGNATURE_INVALID
HTTP 401 · Invalid or expired Paddle signature.
##### PADDLE_CHANGE_REFUSED
HTTP 409 · This change could not be made. Check your card, or try again later.
##### PADDLE_NOTHING_TO_RESUME
HTTP 409 · This subscription is not set to stop: there is nothing to undo.
##### PADDLE_PERIOD_UNAVAILABLE
HTTP 409 · This billing period is not offered for this product.
##### PADDLE_SAME_PERIOD
HTTP 409 · This product is already billed this way.
##### INVOICE_NOT_AVAILABLE
HTTP 404 · No receipt to download for this payment.
##### INTERNAL_ERROR
HTTP 500 · An unexpected error occurred. Please try again or contact support.
---
## Limits and good practice
WahaSend connects your number the way WhatsApp Web does, as a linked device. WhatsApp watches how numbers behave and blocks the ones that look like machines or spam. We space and pace messages to lower the risk, but **no tool can guarantee a number will never be blocked**: how you use it matters most.
- Write to people who know you and expect your message: customers, people who wrote first, people who opted in.
- Start slowly with a new number: a few first messages a day, more after a few weeks. Keep a normal human use of the phone too.
- Personalise: a name and a reason in each message. The same text to hundreds of strangers is the fastest way to a ban.
- Check numbers first ([validate](https://wahasend.com/docs/api/validate.md)); many messages to numbers not on WhatsApp look like spam.
- Send in daytime hours, and stop when people stop answering or ask you to.
- Keep `high` priority for what can't wait (codes, alerts): it skips the typing simulation.
- Watch `session.disconnected` events and `GET /session/status`: a blocked or unlinked number stops sending.
WahaSend is an independent service. It is not made, endorsed or supported by WhatsApp.
---
## Changelog
#### 15 October 2026
- **WhatsApp numbers with Developer and Connect.** An account with Developer or Connect active (paid or on its free trial) has 3 WhatsApp numbers included; each extra number is $10 a month (a year costs 10 months), by card or by transfer from the Subscription page. Notify, Assistant and Shop alone still add as many numbers as they need. Numbers you already have are never removed: above the allowance you keep sending from all of them and can't add a new one. `GET /session/list` answers `allowance`, and a number past it 403 `NUMBER_LIMIT_REACHED` with `details`; see [WhatsApp numbers](https://wahasend.com/docs/api/numbers.md).
#### 6 October 2026
- **New prices.** Developer is now sold self-serve at $39 a month ($390 a year), with a free trial, by card or transfer. Shop is $119 and Connect $35 a month; Notify ($29) and Assistant ($79) are unchanged. A year costs 10 months.
- **Local prices.** In Mauritania, West Africa and the Maghreb, lower prices in the local currency (MRU, FCFA, MAD, DZD, TND), paid by transfer or mobile money. Card payments stay in US dollars at the international price. The API is the same everywhere.
#### 1 October 2026
- **Several numbers.** Sends that name no number can go through a group of your numbers (backup or share, same contact same number): see [Several numbers](https://wahasend.com/docs/phone-numbers.md#several-numbers). `/alert/send`, `/alert/batch` and media sends answer `containerId`. Every product can add as many WhatsApp numbers as it needs.
- **Signed webhooks.** Every event carries `X-WahaSend-Signature`, `X-WahaSend-Event` and `X-WahaSend-Delivery`; see [how to check it](https://wahasend.com/docs/webhooks.md#webhook-security). New `GET /me/webhook` and `POST /me/webhook/regenerate-secret`. Status, inbound, session and poll events now carry `containerId` and `numberPhone`. Assistant and Shop owners can set a webhook for their own events.
- **API keys for every product.** Notify, Assistant and Shop can create an account key and call their own routes (e.g. campaigns and reminders for Notify). See [which products can use the API](https://wahasend.com/docs/authentication.md#auth-products).
- **OTP limits.** Per phone and per organization, 429 `OTP_RATE_LIMITED` when over ([details](https://wahasend.com/docs/errors.md#otp-limits)). A template without `{{code}}` is refused (`OTP_TEMPLATE_MISSING_CODE`); every `{{code}}` is replaced.
- **Edit and delete by `messageId`.** `/alert/edit`, `/alert/delete` and `/alert/forward` take WahaSend's own id (WhatsApp ids still work). Media sends are recorded in `GET /messages` with status events; their answer's `messageId` is now WahaSend's id and WhatsApp's is in `wahaMessageId`. `GET /messages` rows show `wahaMessageId` and `containerId`.
- **Campaigns.** `stats.delivered` and `stats.read` count WhatsApp receipts; new [cancel](https://wahasend.com/docs/api/campaigns.md#campaign-cancel); edit a `scheduled` campaign (including `scheduledAt`); delete anything not `sending`; `scheduledAt` takes any ISO offset; `creditsRequired` is gone.
- **Smaller fixes.** `/alert/batch` honours `skipInvalidNumbers: false`; `/alert/forward` no longer needs `fromPhone`; a per-number key marks only its own number's chats read; `GET /api` points here.
- **Organizations and teams.** An account is an organization with members (owner, admins, agents). API keys act as an organization admin; the role is checked on every request.
- **Claude sign-in.** claude.ai, the Claude apps, ChatGPT and Codex connect to Connect with an OAuth sign-in: no key to copy.
- **Files in Connect.** `open_attachment` reads photos, PDFs, Word files and voice notes; `send_attachment` sends files.
- **Errors and limits.** One error envelope with stable codes and `requestId`; `Retry-After` on 429/503; a per-organization limit of 300 changes a minute.
- These docs were rewritten from the code, with a test that fails when they drift.