# WhatsApp API (/docs/whatsapp/api)

> Send and receive WhatsApp messages, manage templates and opt-outs, and run broadcasts from your own code.

The WhatsApp API does everything the dashboard does: send a message, read a
conversation, hand one over to a person, manage templates and opt-outs, and run
a broadcast. The agent answers incoming messages by default, and your code
follows along by polling.

  Every call takes `Authorization: Bearer <YOUR_API_KEY>`. The key scopes every
  request to your own workspace. A number, conversation, template or campaign in
  another workspace answers `404`, never another account's data.

**Base URL**  &nbsp;·&nbsp; All routes live under `/api/v1/whatsapp`.

## Before you start [#before-you-start]

- **An API key.** Copy it from the dashboard. See
  [Authentication](/docs/get-started/authentication).
- **A WhatsApp number connected**, in the dashboard or over the API with your
  Meta access token and WhatsApp Business Account id. See
  [Connect a number](#connect-a-number) below, or
  [Connect a number in the dashboard](/docs/whatsapp/import-number).
- **An approved template** for any chat you start yourself, because the 24-hour
  window is closed until the customer writes to you. See
  [Templates](/docs/whatsapp/templates).
- **An agent on the number** if you want replies answered. See
  [Agent replies](/docs/whatsapp/agent-replies).

To see the whole API at a glance, open the
[WhatsApp reference](/docs/api-reference/whatsapp/listWhatsappNumbers).

## How a conversation flows [#how-a-conversation-flows]

## The routes, by task [#the-routes-by-task]

| Task | Start here |
|---|---|
| Connect your own Meta number | [Connect numbers](/docs/api-reference/whatsapp/importWhatsappNumbers) |
| Find your number and what it can send | [List numbers](/docs/api-reference/whatsapp/listWhatsappNumbers) |
| Send a text, template, image or document | [Send a message](/docs/api-reference/whatsapp/sendWhatsappMessage) |
| Read a chat, and poll it for new messages | [List messages](/docs/api-reference/whatsapp/listWhatsappMessages) |
| The list of chats on a number | [List conversations](/docs/api-reference/whatsapp/listWhatsappConversations) |
| A person takes over, then hands back | [Take over](/docs/api-reference/whatsapp/takeOverWhatsappConversation) |
| Upload an image or document to send | [Upload media](/docs/api-reference/whatsapp/uploadWhatsappMedia) |
| Templates, and syncing them from Meta | [List templates](/docs/api-reference/whatsapp/listWhatsappTemplates) |
| Block someone, or lift a block | [List opt-outs](/docs/api-reference/whatsapp/listWhatsappOptOuts) |
| A broadcast to a list | [Create a campaign](/docs/api-reference/whatsapp/createWhatsappCampaign) |
| Turn calling on or off | [Get calling](/docs/api-reference/whatsapp/getWhatsappCalling) |

Lists come back as `{ "data": [...], "has_more": true, "next_cursor": "..." }`.
Pass `limit` (1 to 100, default 20) and hand `next_cursor` back until it is
`null`.

## Connect a number [#connect-a-number]

Connect numbers from your own WhatsApp Business Account with a Meta access token
that never expires and has both WhatsApp permissions. A system user token does.
[Meta credentials](/docs/whatsapp/meta-credentials) shows where to find the token
and the account id.

### Preview the numbers [#preview-the-numbers]

API reference: [Preview numbers](/docs/api-reference/whatsapp/previewWhatsappNumbers)

```bash
curl -X POST "https://backend.omnidim.io/api/v1/whatsapp/numbers/preview" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"waba_id": "YOUR_WABA_ID", "access_token": "YOUR_META_TOKEN"}'
```

Each number comes back with its `phone_number_id` and its health. A token that
cannot connect is refused here, with a code that says why (`token_expires`,
`token_missing_permissions` or `token_invalid`).

### Connect the ones you want [#connect-the-ones-you-want]

API reference: [Connect numbers](/docs/api-reference/whatsapp/importWhatsappNumbers)

```bash
curl -X POST "https://backend.omnidim.io/api/v1/whatsapp/numbers" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "waba_id": "YOUR_WABA_ID",
    "access_token": "YOUR_META_TOKEN",
    "phone_number_ids": ["PHONE_NUMBER_ID"],
    "app_secret": "YOUR_META_APP_SECRET"
  }'
```

The answer lists each connected number with its `id`, the id every other route
takes. `receiving` says whether this request turned replies on. Send
`app_secret` to turn them on. Find it in your Meta app under **App settings >
Basic**. Numbers that could not connect are in `failed`, with the reason.

To change the token or the app secret later, connect the same number again with
the new value. It updates the number.

## Send a message [#send-a-message]

### Find your number [#find-your-number]

API reference: [List numbers](/docs/api-reference/whatsapp/listWhatsappNumbers)

```bash
curl "https://backend.omnidim.io/api/v1/whatsapp/numbers" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

```json
{
  "data": [
    {
      "id": 412,
      "phone_number": "+15551234567",
      "provider": "meta",
      "name": "Support",
      "status": "active",
      "quality_rating": "GREEN",
      "messaging_limit": "TIER_10K",
      "capabilities": {
        "types": ["text", "template", "image", "document"],
        "reply_to": true,
        "window_enforced": true,
        "delivery_status": "push",
        "template_sync": true
      }
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

Keep the `id`. `capabilities.types` says what this number can send. A `status`
other than `active` means the number cannot send right now.

### Send a text, inside the window [#send-a-text-inside-the-window]

API reference: [Send a message](/docs/api-reference/whatsapp/sendWhatsappMessage)

```bash
curl -X POST "https://backend.omnidim.io/api/v1/whatsapp/messages" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2d9e-0b7a-4c1e-9a55-3e2f8b1d7c40" \
  -d '{
    "phone_number_id": 412,
    "to": "+15557654321",
    "type": "text",
    "text": "Your appointment is confirmed for Friday at 10 am."
  }'
```

The send comes back as `201` with the message, ready to look up again:

```json
{
  "id": 98123,
  "conversation_id": "412-15557654321",
  "sender": "person",
  "type": "text",
  "text": "Your appointment is confirmed for Friday at 10 am.",
  "status": "pending",
  "timestamp": "2026-10-18T09:04:11+00:00",
  "timeline": [{ "status": "accepted", "at": "2026-10-18T09:04:11+00:00" }]
}
```

A free-form send needs an open 24-hour window, which the customer opens by
messaging you. Outside it, the send is refused with `409 window_closed` and the
message asks for a template. For a new chat, or any send after a day of silence,
use a template.

### Or start the chat with a template [#or-start-the-chat-with-a-template]

Find an approved template, then send it by its id. A template works whether or
not the window is open, and it can be the first message in a conversation.

```bash
curl "https://backend.omnidim.io/api/v1/whatsapp/numbers/412/templates?status=approved" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

Each template lists the `fields` a send must fill:

```json
{
  "data": [
    {
      "id": 3676,
      "name": "appointment_reminder",
      "language": "en_US",
      "category": "UTILITY",
      "status": "approved",
      "body_preview": "Hi {{1}}, your appointment is on {{2}}.",
      "fields": [
        { "key": "1", "label": "Customer name", "kind": "text", "required": true, "example": "Demo User" },
        { "key": "2", "label": "Appointment time", "kind": "text", "required": true, "example": "Friday, 10 am" }
      ],
      "buttons": []
    }
  ],
  "has_more": false,
  "next_cursor": null
}
```

```bash
curl -X POST "https://backend.omnidim.io/api/v1/whatsapp/messages" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9a2b7c1e-4d6f-4a80-9c33-5e1f2d8b6a07" \
  -d '{
    "phone_number_id": 412,
    "to": "+15557654321",
    "type": "template",
    "template": { "id": 3676, "params": { "1": "Rahul", "2": "Friday, 10 am" } }
  }'
```

A template you have not synced yet will not be in the list. Run
[Sync templates](/docs/api-reference/whatsapp/syncWhatsappTemplates) after you
create or edit one in Meta.

### Send a file [#send-a-file]

API reference: [Upload media](/docs/api-reference/whatsapp/uploadWhatsappMedia)

Upload first, then send by the returned `media_id`. Uploading costs nothing;
only the send is charged.

```bash
curl -X POST "https://backend.omnidim.io/api/v1/whatsapp/media" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "phone_number_id=412" \
  -F "type=document" \
  -F "file=@invoice.pdf"
```

```json
{
  "media_id": "kQ8v2mZ0pLc4xR7tN1sW3yB6dF9hJ5aE",
  "type": "document",
  "mime": "application/pdf",
  "filename": "invoice.pdf",
  "size": 48213,
  "expires_at": "2026-10-19T09:04:11+00:00"
}
```

Then send it with `type` `image` or `document` and a `media` object. The
`media_id` works for 24 hours, for that number and type only.

  A send and a campaign launch both require an `Idempotency-Key` header: any
  unique string, up to 255 characters. The same key with the same body replays
  the first answer for 24 hours instead of sending twice. The same key with a
  different body answers `409`.

## Read and follow messages [#read-and-follow-messages]

There are no event webhooks, so you poll. Two ways, depending on what you need:

- **Follow one chat.** Call [List messages](/docs/api-reference/whatsapp/listWhatsappMessages)
  with `conversation_id` and `after` set to the newest message id you already
  have. You get only the messages newer than it, newest first.
- **Watch the whole inbox.** Call
  [List conversations](/docs/api-reference/whatsapp/listWhatsappConversations)
  with `updated_since` set to the time of your last call. You get every
  conversation with a change since then, including new messages, takes over and
  opt-outs.

```bash
curl "https://backend.omnidim.io/api/v1/whatsapp/messages?conversation_id=412-15557654321&after=98123" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

To read a message's delivery, call
[Get a message](/docs/api-reference/whatsapp/getWhatsappMessage). Its `timeline`
walks through `accepted`, `sent`, `delivered` and `read`, and a `failed` message
carries a `failure` with a reason and whether a retry is safe.

## Take over and hand back [#take-over-and-hand-back]

The agent owns a conversation by default and answers every incoming message. An
API send does **not** take it over: send a message and the agent keeps replying.

To answer as a person, take the conversation over. The agent goes quiet, and
customer messages are still stored and shown but not answered.

```bash
curl -X POST "https://backend.omnidim.io/api/v1/whatsapp/conversations/412-15557654321/take_over" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

[Hand back](/docs/api-reference/whatsapp/handBackWhatsappConversation) when the
person is done and the agent resumes on the next message. You can also set
`take_over: true` on a send to do both in one call.

## Broadcasts [#broadcasts]

A campaign sends one approved template to a list. It is always created as a
draft:

1. [Create a campaign](/docs/api-reference/whatsapp/createWhatsappCampaign) with
   a `name`, a `phone_number_id` and an approved `template_id`.
2. [Add contacts](/docs/api-reference/whatsapp/addWhatsappCampaignContacts), one
   object up to 500 at once. Each needs a `phone_number`. Any other key you send
   can fill a template parameter through `variable_mapping`.
3. [Launch it](/docs/api-reference/whatsapp/launchWhatsappCampaign) with a new
   `Idempotency-Key`. Every pending contact is queued once.

Launch checks the plan's monthly broadcast allowance and your wallet balance.
If either runs short, launch answers `402`. You can
[pause](/docs/api-reference/whatsapp/pauseWhatsappCampaign),
[resume](/docs/api-reference/whatsapp/resumeWhatsappCampaign) and
[cancel](/docs/api-reference/whatsapp/cancelWhatsappCampaign) a running
campaign, and read per-contact status with
[List contacts](/docs/api-reference/whatsapp/listWhatsappCampaignContacts).

Scheduling is not offered over the API. In the dashboard the same broadcasts are
described at [Broadcasts](/docs/whatsapp/broadcasts).

## Opt-outs [#opt-outs]

A customer stops your messages by replying STOP, or by turning marketing off in
WhatsApp. Both are listed under [List opt-outs](/docs/api-reference/whatsapp/listWhatsappOptOuts).

[Block a customer](/docs/api-reference/whatsapp/addWhatsappOptOut) yourself with
their number, and [lift it](/docs/api-reference/whatsapp/liftWhatsappOptOut) when
they ask to come back. A block the customer made themselves cannot be lifted over
the API: the request answers `409 customer_opt_out`, because only the customer can
opt back in by messaging you.

## Calling [#calling]

WhatsApp calling lets a customer call the number, and the agent answers. It is
off by default, only on Meta Cloud numbers, and must be included in your plan.
Read the state and the checks with
[Get calling](/docs/api-reference/whatsapp/getWhatsappCalling), then
[Turn it on](/docs/api-reference/whatsapp/setWhatsappCalling) once every blocking
check passes. See [WhatsApp calls](/docs/whatsapp/calling) for the product side.

## Errors [#errors]

Every error is `{ "error", "error_description", "code", "request_id" }`, and
`X-Request-Id` is on every response. `code` is the specific reason to branch on.

| Status | What it means |
|---|---|
| `400` | A bad parameter or body. `code` says which, such as `invalid_parameter`, `invalid_limit` or `token_expires`. |
| `401` | The API key is missing or wrong. |
| `402` | Not enough wallet balance, or the monthly broadcast allowance is used up. |
| `403` | Your plan or role cannot do this. |
| `404` | No such number, conversation, message, template, campaign or opt-out in your workspace. |
| `409` | A state conflict: `window_closed`, `opted_out`, `not_supported`, `invalid_transition` or a reused idempotency key. |
| `429` | Too many template syncs. `Retry-After` says how long to wait. |
| `502` | WhatsApp could not be reached. Nothing was sent or charged. |
| `503` | WhatsApp is not connected for the number, or is briefly unavailable. |

## Related [#related]

  }
    title="WhatsApp overview"
    href="/docs/whatsapp"
    description="Connect a number, understand the 24-hour window, and set up the agent."
  />
  }
    title="Chat inbox"
    href="/docs/whatsapp/inbox"
    description="Read chats, reply, take one over, and hand it back in the dashboard."
  />
  }
    title="Broadcasts"
    href="/docs/whatsapp/broadcasts"
    description="Send a template to a list and let the agent answer replies."
  />
  }
    title="WhatsApp API reference"
    href="/docs/api-reference/whatsapp/listWhatsappNumbers"
    description="Every route, with a live playground."
  />