# Avox API: Send WhatsApp Messages

> Send WhatsApp messages from your own server with a connection-scoped Avox API key - overview, request flow, and a 5-minute quickstart.

Source: https://docs.theavoxai.com/avox-api

The Avox API lets your own server send WhatsApp messages through a number you've connected to
Avox. You call one endpoint with an Avox API key, and Avox sends the message to WhatsApp for you.
Your server never needs Meta's own access token - that stays inside Avox.

```
POST https://api.avox.com/api/messages/send
```

- **One endpoint for every message type** - text, templates, media, locations, contacts,
  interactive messages, and reactions. See [Message types](https://docs.theavoxai.com/avox-api/messages).
- **One key, one number.** Each key belongs to exactly one connected WhatsApp number. There's no
  phone number id in the URL: the key already knows which number it sends from. See
  [Authentication](https://docs.theavoxai.com/avox-api/authentication).
- **WhatsApp's own message format.** The body is the same JSON WhatsApp's Cloud API uses, so
  existing payloads work with little or no change.
- **Clear errors.** Every failure returns a stable `code` you can branch on, with a link to its
  explanation on [API errors](https://docs.theavoxai.com/api/errors).
- **Safe retries.** Send an `Idempotency-Key` header and a retried request never sends the same
  message twice. See [Idempotency](https://docs.theavoxai.com/avox-api/idempotency).
- **Live and test keys.** A live key (`avx_live_...`) sends real messages. A test key
  (`avx_test_...`) runs every check and answers with a pretend result, so you can build without
  messaging anyone. See [Testing](https://docs.theavoxai.com/avox-api/testing).
- **A full audit trail.** Every request made with your keys is listed on the **API logs** page in
  the dashboard, with its outcome, error code, masked recipient, request id and timing. See
  [API logs](https://docs.theavoxai.com/connections/api-keys#api-logs).

## 5-minute quickstart

You need a WhatsApp number connected to Avox (see [Getting started](https://docs.theavoxai.com/getting-started)) and an
active plan that includes API access - the plan cards on [Plans](https://docs.theavoxai.com/plans) list **API access** on the
plans that have it. Then:

**1. Create an API key**

In the dashboard, open **Settings > API Keys** and click **Create a key**. In **Create an API
key**, pick the **Switchboard** and the **WhatsApp number**, choose **Live key** (or **Test key**
while you build), give it a **Key name** (2 to 60 characters) and, if you like, **What it's
for**. Click **Email me a code** and enter the 6-digit code we email you. You can also create a
key from a connection's own page. Creating keys needs an Admin (or a member with the **Manage API
keys** permission) - see [API keys](https://docs.theavoxai.com/connections/api-keys) for the full walkthrough.

**2. Copy the key right away**

The dialog **Your new API key** shows the full key exactly once. A live key starts with
`avx_live_`, a test key with `avx_test_`. Copy it and click **Done**. If you lose it, rotate
the key or create a new one - a key can't be shown again.

**3. Store it as a server secret**

Put it in an environment variable (for example `AVOX_API_KEY`) or your secrets manager on the
server that will send messages. Never put it in a browser page or a mobile app.

**4. Check the key**

Back in **Settings > API Keys**, click **Test** next to the key. It confirms the key works and
which number it sends from, without sending a message. See [Testing](https://docs.theavoxai.com/avox-api/testing).

**5. Send your first message**

Run one of the examples below with your own WhatsApp number as `to`. Your number must have
messaged the connected number in the last 24 hours for a plain text message to go through -
otherwise send an approved template. See [Messaging window](https://docs.theavoxai.com/whatsapp/messaging-window).

**6. See it in the audit trail**

Open **API logs** in the dashboard's sidebar. Your send is at the top of the **Sent** tab with
its key (and a **Live** or **Test** badge), status `200`, the recipient's number partly hidden,
and its request id. Click the row for **Request details**. The arrow next to a key in
**Settings > API Keys** also shows that key's **Recent requests**.

**curl**

```bash
curl -X POST https://api.avox.com/api/messages/send \
  -H "Authorization: Bearer $AVOX_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1d2c1e-8a4b-4c7e-9a51-3b0f2d9e7c10" \
  -d '{
    "to": "15551234567",
    "type": "text",
    "text": { "body": "Hello from Avox!" }
  }'
```

**Node.js**

```js
import { randomUUID } from "node:crypto";

const res = await fetch("https://api.avox.com/api/messages/send", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AVOX_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": randomUUID(),
  },
  body: JSON.stringify({
    to: "15551234567",
    type: "text",
    text: { body: "Hello from Avox!" },
  }),
});

const data = await res.json();
if (data.ok) {
  console.log("Sent:", data.message_id);
} else {
  console.error(res.status, data.error.code, data.error.message);
}
```

**Python**

```python
import os
import uuid

import requests

res = requests.post(
    "https://api.avox.com/api/messages/send",
    headers={
        "Authorization": f"Bearer {os.environ['AVOX_API_KEY']}",
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "to": "15551234567",
        "type": "text",
        "text": {"body": "Hello from Avox!"},
    },
    timeout=30,
)

data = res.json()
if data["ok"]:
    print("Sent:", data["message_id"])
else:
    print(res.status_code, data["error"]["code"], data["error"]["message"])
```

A successful send returns `200`:

```json
{
  "ok": true,
  "request_id": "req_4f1c9a0b7e2d36a8c5b1f0e9",
  "message_id": "wamid.HBgLMTU1NTEyMzQ1NjcVAgARGBI5",
  "result": {
    "messaging_product": "whatsapp",
    "contacts": [{ "input": "15551234567", "wa_id": "15551234567" }],
    "messages": [{ "id": "wamid.HBgLMTU1NTEyMzQ1NjcVAgARGBI5" }]
  }
}
```

Keep `message_id`. It's WhatsApp's id for the message, and every later status update (sent,
delivered, read, failed) carries the same id. See
[Delivery statuses and replies](https://docs.theavoxai.com/avox-api/webhooks).

## The request

| Part | Value |
| --- | --- |
| Method and URL | `POST https://api.avox.com/api/messages/send` |
| `Authorization` header | `Bearer avx_live_...` (or `Bearer avx_test_...` for a test key) - required. See [Authentication](https://docs.theavoxai.com/avox-api/authentication). |
| `Content-Type` header | `application/json` |
| `Idempotency-Key` header | Optional, recommended. 1 to 255 visible characters, no spaces. See [Idempotency](https://docs.theavoxai.com/avox-api/idempotency). |
| Body | One JSON object, at most 64 KB. See [Message types](https://docs.theavoxai.com/avox-api/messages). |

The body always has `to` (the recipient's full international number) and `type`, plus one object
named after the type:

```json
{
  "to": "15551234567",
  "type": "template",
  "template": {
    "name": "order_update",
    "language": { "code": "en_US" },
    "components": [
      { "type": "body", "parameters": [{ "type": "text", "text": "A-1042" }] }
    ]
  }
}
```

The accepted types are `text`, `template`, `image`, `video`, `audio`, `document`, `sticker`,
`location`, `contacts`, `interactive` and `reaction`. Avox adds `messaging_product` for you, and
any top-level field it doesn't recognise is dropped rather than passed on to WhatsApp.

## What happens to each request

Every request goes through the same checks, in this order, and stops at the first one that fails.
The error you get back is always the most basic thing to fix first.

**1. Size and format**

The body must be a JSON object under 64 KB. Otherwise:
[`payload_too_large`](https://docs.theavoxai.com/api/errors#payload_too_large) or
[`invalid_payload`](https://docs.theavoxai.com/api/errors#invalid_payload).

**2. Your key**

The `Authorization: Bearer` header must hold a real key that hasn't been revoked or expired,
used from an allowed address. Only API keys work here - being signed in to the dashboard
doesn't count. Codes: [`missing_api_key`](https://docs.theavoxai.com/api/errors#missing_api_key),
[`invalid_api_key`](https://docs.theavoxai.com/api/errors#invalid_api_key), [`key_revoked`](https://docs.theavoxai.com/api/errors#key_revoked),
[`key_expired`](https://docs.theavoxai.com/api/errors#key_expired), [`ip_not_allowed`](https://docs.theavoxai.com/api/errors#ip_not_allowed).

**3. The key's number**

The key sends only from the number it was created for. That number must still exist, be
connected, and not be paused. Codes:
[`connection_not_found`](https://docs.theavoxai.com/api/errors#connection_not_found),
[`connection_paused`](https://docs.theavoxai.com/api/errors#connection_paused),
[`connection_inactive`](https://docs.theavoxai.com/api/errors#connection_inactive).

**4. Your account**

API access must be on, the account in good standing, and the plan active (not paused) and one
that includes API access. While a failed payment is still being retried, sending keeps working.
Codes: [`api_access_disabled`](https://docs.theavoxai.com/api/errors#api_access_disabled),
[`account_suspended`](https://docs.theavoxai.com/api/errors#account_suspended),
[`subscription_inactive`](https://docs.theavoxai.com/api/errors#subscription_inactive),
[`api_not_in_plan`](https://docs.theavoxai.com/api/errors#api_not_in_plan).

**5. Limits**

Each key and each account can only send so fast, and some keys have a daily allowance. Codes:
[`rate_limited`](https://docs.theavoxai.com/api/errors#rate_limited), [`quota_exceeded`](https://docs.theavoxai.com/api/errors#quota_exceeded).
See [Rate limits](https://docs.theavoxai.com/avox-api/rate-limits).

**6. The message**

The recipient must be a valid international number and the message valid for its type. Codes:
[`invalid_recipient`](https://docs.theavoxai.com/api/errors#invalid_recipient),
[`unsupported_message_type`](https://docs.theavoxai.com/api/errors#unsupported_message_type),
[`invalid_payload`](https://docs.theavoxai.com/api/errors#invalid_payload).

**7. Idempotency**

If you sent an `Idempotency-Key`, Avox checks whether it has seen it before. A repeat of a
finished request returns the first result without sending again. Codes:
[`invalid_idempotency_key`](https://docs.theavoxai.com/api/errors#invalid_idempotency_key),
[`idempotency_conflict`](https://docs.theavoxai.com/api/errors#idempotency_conflict),
[`idempotency_in_progress`](https://docs.theavoxai.com/api/errors#idempotency_in_progress).

**8. Sent to WhatsApp**

Only now is the message handed to WhatsApp. If WhatsApp refuses it, you get a clear code such
as [`template_not_found`](https://docs.theavoxai.com/api/errors#template_not_found) or
[`outside_messaging_window`](https://docs.theavoxai.com/api/errors#outside_messaging_window) instead of WhatsApp's raw
reply, plus WhatsApp's own number in `meta_code`. A test key stops here with a pretend result
instead - see [Test mode](https://docs.theavoxai.com/avox-api/testing#1-test-mode).

Once Avox has matched a request to one of your keys (even a revoked or expired one), its outcome,
successful or not, is written to the [API logs](https://docs.theavoxai.com/connections/api-keys#api-logs), so you can look
up its `request_id` later. A request that never names a real key can't be tied to your account,
so it isn't listed.

## Responses

Every response, successful or not, carries two headers:

- `X-Request-Id` - a unique id for this request, like `req_4f1c9a0b7e2d36a8c5b1f0e9`. Log it, and
  include it if you contact [Support](https://docs.theavoxai.com/support). You can paste it into the search box on
  **API logs** to find the request.
- `Cache-Control: no-store`

Some responses carry more:

| Header | When |
| --- | --- |
| `Retry-After` | On errors that waiting fixes (`rate_limited`, `quota_exceeded`, `meta_rate_limited`, `idempotency_in_progress`). Whole seconds, at least `1`. |
| `Idempotent-Replayed: true` | When the response is a stored result for an `Idempotency-Key` you already used. Nothing was sent again. |
| `Avox-Test-Mode: true` | On every response to a test key. The body also has `"test": true`. |

A failed request returns `"ok": false` and an `error` object:

```json
{
  "ok": false,
  "error": {
    "code": "outside_messaging_window",
    "message": "It's been more than 24 hours since this person last messaged you. Send an approved template instead.",
    "docs_url": "https://docs.avox.com/api/errors#outside_messaging_window",
    "request_id": "req_9b2e5d7a1c4f80e36d2a7b15",
    "meta_code": 131047
  }
}
```

Branch on `error.code` - codes never change once published. `details` (which fields to fix) and
`meta_code` (WhatsApp's own error number) appear only when they apply.

| Status | Codes |
| --- | --- |
| `200` | Sent (or an idempotent replay of an earlier result) |
| `400` | `invalid_payload`, `invalid_recipient`, `unsupported_message_type`, `invalid_idempotency_key` |
| `401` | `missing_api_key`, `invalid_api_key`, `key_revoked`, `key_expired` |
| `402` | `subscription_inactive` |
| `403` | `ip_not_allowed`, `api_access_disabled`, `api_not_in_plan`, `account_suspended` |
| `404` | `connection_not_found` |
| `409` | `connection_paused`, `connection_inactive`, `connection_not_ready`, `idempotency_conflict`, `idempotency_in_progress` |
| `413` | `payload_too_large` |
| `422` | `template_not_found`, `template_unavailable`, `template_params_invalid`, `outside_messaging_window`, `recipient_unreachable`, `meta_rejected` |
| `429` | `rate_limited`, `quota_exceeded`, `meta_rate_limited` |
| `500` | `internal_error` |
| `502` | `meta_unavailable` |

Every code, its causes, and the fix are on [API errors](https://docs.theavoxai.com/api/errors).

> **Note**
>
> Integrations written before this error format read `error` as a plain string. Read
> `error.message` instead, or better, branch on `error.code`.

## Who can do what

| Action | Who |
| --- | --- |
| Create a live or test key | Admin, or a member with **Manage API keys** |
| Rotate a key | Admin, or a member with **Manage API keys** |
| Revoke a key | Admin, or a member with **Manage API keys** |
| Click **Test** on a key | Admin, or a member with **Manage API keys** |
| See keys and their recent requests | Admin, or a member with **See API keys** |
| Read and export the **API logs** | Admin and Manager (**See API logs** is on for Managers by default) |
| Resend a forwarded message from the API logs | Admin |
| Call the API | Anyone holding the key - keep it secret |

Managers start without **See API keys** and **Manage API keys**; an Admin can give either one to a
Manager person by person - see [Permissions](https://docs.theavoxai.com/switchboard/members#permissions). See
[Members and roles](https://docs.theavoxai.com/switchboard/members) for what each role can do elsewhere in the dashboard.

## Your plan and API access

API access depends on your plan: each plan either includes it or doesn't. On a plan without it, the
dashboard shows "API access isn't included in your plan" with a link to the plans that include it.
Your keys stay listed, but new keys can't be created or rotated, and existing keys stop sending with
[`api_not_in_plan`](https://docs.theavoxai.com/api/errors#api_not_in_plan) until your plan includes API access again. A paused
plan has no API access either. See [API access and your plan](https://docs.theavoxai.com/connections/api-keys#api-access-and-your-plan).

## What a key can do

A key is send-only. It sends from its own number on this one endpoint, and the response
holds only the outcome: `request_id`, WhatsApp's `message_id` and its reply to the send, or an
`error` with its code. A key can't read your contacts, conversations or anything else in Avox.
Incoming messages reach your server through
[Message forwarding](https://docs.theavoxai.com/connections/webhook-override) instead. The details are on
[What a key can do, and what it can't](https://docs.theavoxai.com/connections/api-keys#what-a-key-can-do-and-what-it-cant).

## Next steps

- [Connect your server](https://docs.theavoxai.com/avox-api/connect-your-server): A complete backend in Express, Next.js, FastAPI or Flask, with retries, idempotency and logging built in.

- [Authentication](https://docs.theavoxai.com/avox-api/authentication): Keys, the header, scoping, and rotation.

- [Message types](https://docs.theavoxai.com/avox-api/messages): Every type with a full example.

- [Idempotency](https://docs.theavoxai.com/avox-api/idempotency): Retry safely without sending twice.

- [Rate limits](https://docs.theavoxai.com/avox-api/rate-limits): What a limited request looks like and when to retry.

- [Webhooks](https://docs.theavoxai.com/avox-api/webhooks): Receive customer messages on your server, and where to see delivery statuses.

- [API logs](https://docs.theavoxai.com/connections/api-keys#api-logs): Every request made with your keys, with filters and CSV export.

- [Testing](https://docs.theavoxai.com/avox-api/testing): Build and test with a test key first.

- [Go-live checklist](https://docs.theavoxai.com/avox-api/go-live): Everything to check before real customers see it.

Ready-made examples: [Node.js](https://docs.theavoxai.com/integrations/nodejs), [Laravel](https://docs.theavoxai.com/integrations/laravel),
[Shopify](https://docs.theavoxai.com/integrations/shopify), [n8n](https://docs.theavoxai.com/integrations/n8n), [Make](https://docs.theavoxai.com/integrations/make),
[Zapier](https://docs.theavoxai.com/integrations/zapier), [Chatwoot](https://docs.theavoxai.com/integrations/chatwoot).
