# Avox API Integration

> Connect your own systems to WhatsApp with an Avox API key - requirements, setup, a first request, checks, common errors and FAQ.

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

The **Avox API** lets any system that can make an HTTPS request - your backend, a store, a CRM, an
automation tool - send WhatsApp messages through a number connected to Avox. One endpoint, one
key per number, WhatsApp's own message format.

This page is the quick guide. The full reference starts at [Avox API](https://docs.theavoxai.com/avox-api).

## What it does

- Sends every WhatsApp message type: text, templates, media, locations, contacts, interactive
  buttons and lists, Flows and reactions. See [Message types](https://docs.theavoxai.com/avox-api/messages).
- Returns WhatsApp's `message_id`, so you can match delivery statuses and replies later.
- Explains every failure with a stable code. See [API errors](https://docs.theavoxai.com/api/errors).
- Offers live keys (`avx_live_...`) that send for real, and test keys (`avx_test_...`) that run
  every check without sending anything. See [Testing](https://docs.theavoxai.com/avox-api/testing).
- Records every request on the **API logs** page in the dashboard, where you can filter, search
  and export them. See [API logs](https://docs.theavoxai.com/connections/api-keys#api-logs).

## Requirements

- A connected WhatsApp number with **Connected** status.
- An active plan that includes API access. Each plan either includes it or doesn't; the plan cards
  on [Plans](https://docs.theavoxai.com/plans) list **API access** on the plans that have it. See
  [API access and your plan](https://docs.theavoxai.com/connections/api-keys#api-access-and-your-plan).
- The **Admin** role (or a Manager with **Manage API keys**) to create, rotate or revoke keys.
  Managers can read the API logs, and see keys with **See API keys**.
- A server-side place to keep the key secret.

## Setup

**1. Create a key**

Go to **Settings > API Keys** and click **Create a key**. Pick the **Switchboard** and the **WhatsApp number**, choose **Live key** or **Test key**, name the key, then click **Email me a code** and type the code. The connection's own **Outbound API keys** section has the same form.

**2. Copy it once**

The dialog **Your new API key** shows the full key one time. Copy it, then click **Done**.

**3. Store it**

Save it as a secret on your server, for example `AVOX_API_KEY`.

**4. Send a request**

Call `POST https://api.avox.com/api/messages/send` with `Authorization: Bearer <key>`.

**5. Follow up**

Delivery statuses appear on [Logs](https://docs.theavoxai.com/logs) and replies in the [Inbox](https://docs.theavoxai.com/inbox). See [Delivery statuses and replies](https://docs.theavoxai.com/avox-api/webhooks).

**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: 3f0c6a52-1b7e-4b8e-9d0f-6a2c51e4d7b9" \
  -d '{"to":"15551234567","type":"template","template":{"name":"hello_world","language":{"code":"en_US"}}}'
```

**Node.js**

```js
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": crypto.randomUUID(),
  },
  body: JSON.stringify({
    to: "15551234567",
    type: "template",
    template: { name: "hello_world", language: { code: "en_US" } },
  }),
});
console.log(res.status, await res.json());
```

**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": "template",
        "template": {"name": "hello_world", "language": {"code": "en_US"}},
    },
    timeout=30,
)
print(res.status_code, res.json())
```

Use a template that's approved on your account - `hello_world` is only an example name.

## Check it's working

- Click **Test** next to the key in **Settings > API Keys**: "Working - routes to" and the
  connection's name means the key is ready. No message is sent.
- A real send returns `200` with `"ok": true` and a `message_id`. A test key's answer also has
  `"test": true`.
- Open **API logs** in the dashboard: the request is on the **Sent** tab with status `200`. Or
  expand the key in **Settings > API Keys** to see it under **Recent requests** as **Send**.

## Common errors

| Error | Fix |
| --- | --- |
| [`missing_api_key`](https://docs.theavoxai.com/api/errors#missing_api_key), [`invalid_api_key`](https://docs.theavoxai.com/api/errors#invalid_api_key) | Send `Authorization: Bearer <key>` with the full key. |
| [`key_revoked`](https://docs.theavoxai.com/api/errors#key_revoked) | Create a new key and update your server. |
| [`key_expired`](https://docs.theavoxai.com/api/errors#key_expired) | The key was rotated - put its replacement on your server. |
| [`connection_paused`](https://docs.theavoxai.com/api/errors#connection_paused) | Resume the number on [Connections](https://docs.theavoxai.com/connections). |
| [`subscription_inactive`](https://docs.theavoxai.com/api/errors#subscription_inactive) | Renew or resume the plan on [Billing](https://docs.theavoxai.com/billing). |
| [`api_not_in_plan`](https://docs.theavoxai.com/api/errors#api_not_in_plan) | Your plan doesn't include API access. Move to a plan with it on **Billing > Change plan**; the same keys work again. |
| [`invalid_payload`](https://docs.theavoxai.com/api/errors#invalid_payload) | Read `error.details` for the field to fix. |
| [`outside_messaging_window`](https://docs.theavoxai.com/api/errors#outside_messaging_window) | Send an approved template. |
| [`rate_limited`](https://docs.theavoxai.com/api/errors#rate_limited) | Wait `Retry-After` seconds and retry. |
| [AVX-3053](https://docs.theavoxai.com/error-codes/avx-3053) - couldn't create that API key | Only an Admin can create keys ("Only an Admin can create API keys"). Try again, or ask an Admin. |
| [AVX-3055](https://docs.theavoxai.com/error-codes/avx-3055) - couldn't verify that key | The **Test** request failed to run. Try again in a moment. |

## Limits

- Body up to 64 KB; text up to 4096 characters; captions up to 1024. See
  [Message types](https://docs.theavoxai.com/avox-api/messages).
- Per-key and per-account rate limits, with `Retry-After` on every `429`. See
  [Rate limits](https://docs.theavoxai.com/avox-api/rate-limits).
- WhatsApp's own messaging limits per number. See
  [Quality and limits](https://docs.theavoxai.com/whatsapp/quality-and-limits).

## Security

- Each key is send-only: it sends from its own number and can't read your contacts, conversations or
  anything else in your account. See
  [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).
- The full key is shown once. Keep it on your server - never in a browser, an app or a repository.
- Revoke a key with the bin button (**Revoke this key**); it stops working immediately. To replace
  a key without downtime, **Rotate** it and pick how long the old one keeps working. See
  [Rotating a key](https://docs.theavoxai.com/avox-api/authentication#rotating-a-key).
- **Settings > API Keys** warns you about unusual use, such as **Used from a new place** or **Far
  more requests than usual**.

## Build your own server for the Avox API

Your server is the only place that should hold the API key. The server below wraps the Avox API in one route, `POST /notify`, that your other apps call with an internal token and a `reference` from your own system. The `reference` becomes the `Idempotency-Key`, so the same notification is never sent twice.

```bash
# Environment
AVOX_API_KEY=avx_live_your_key
INTERNAL_TOKEN=a-long-random-string   # your apps send this in x-internal-token
```

**Express**

```js
// server.js - Node.js 18+ (npm install express). Run: AVOX_API_KEY=... node server.js
import express from "express";
import { randomUUID, timingSafeEqual } from "node:crypto";

const app = express();

// Avox client: one Idempotency-Key per message, retries only what's safe to retry.
const RETRY_SAME_KEY = new Set(["rate_limited", "idempotency_in_progress", "internal_error", "network_error"]);
const RETRY_NEW_KEY = new Set(["meta_rate_limited"]);
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const backoffMs = (attempt) => Math.min(30_000, 1_000 * 2 ** (attempt - 1)) * (0.5 + Math.random() / 2);

async function sendWhatsApp(message, idempotencyKey = randomUUID(), maxAttempts = 5) {
  let key = idempotencyKey;
  for (let attempt = 1; ; attempt++) {
    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": key,
      },
      body: JSON.stringify(message),
      signal: AbortSignal.timeout(30_000),
    }).catch(() => null);

    const data = res ? await res.json().catch(() => null) : null;
    const requestId = res?.headers.get("X-Request-Id") ?? null;
    if (data?.ok) {
      console.info(`[avox] sent message_id=${data.message_id} request_id=${requestId}`);
      return data;
    }

    const code = data?.error?.code ?? (res ? `http_${res.status}` : "network_error");
    console.warn(`[avox] ${code} request_id=${requestId} attempt=${attempt}`);
    const retryable = RETRY_SAME_KEY.has(code) || RETRY_NEW_KEY.has(code);
    if (!retryable || attempt >= maxAttempts) {
      throw Object.assign(new Error(`Avox send failed: ${code}`), {
        code,
        status: res?.status ?? 502,
        requestId: data?.error?.request_id ?? requestId,
        details: data?.error?.details,
      });
    }
    if (RETRY_NEW_KEY.has(code)) key = randomUUID(); // WhatsApp's refusal is stored against the old key
    const retryAfter = Number(res?.headers.get("Retry-After"));
    await sleep(retryAfter > 0 ? retryAfter * 1000 : backoffMs(attempt));
  }
}

function safeEqual(a, b) {
  const x = Buffer.from(a ?? "");
  const y = Buffer.from(b ?? "");
  return x.length > 0 && x.length === y.length && timingSafeEqual(x, y);
}

// POST /notify  { "to": "15551234567", "template": "order_shipped", "params": ["A-1042"], "reference": "order-A-1042-shipped" }
// Your own apps call this with the x-internal-token header; the key never leaves this server.
app.post("/notify", express.json({ limit: "100kb" }), async (req, res) => {
  if (!safeEqual(req.get("x-internal-token"), process.env.INTERNAL_TOKEN)) return res.sendStatus(401);
  const { to, template, params = [], language = "en_US", reference } = req.body ?? {};
  if (!to || !template || !reference) return res.status(400).json({ error: "to, template and reference are required" });

  try {
    const result = await sendWhatsApp(
      {
        to,
        type: "template",
        template: {
          name: template,
          language: { code: language },
          ...(params.length ? { components: [{ type: "body", parameters: params.map((t) => ({ type: "text", text: String(t) })) }] } : {}),
        },
        biz_opaque_callback_data: reference,
      },
      reference // the same reference always means the same message: retries never send twice
    );
    res.json({ ok: true, message_id: result.message_id });
  } catch (err) {
    res.status(err.status >= 500 ? 502 : 422).json({ ok: false, code: err.code, request_id: err.requestId, details: err.details });
  }
});

app.listen(process.env.PORT ?? 3000, () => console.log("Listening"));
```

**Next.js**

```ts
// app/api/notify/route.ts - Next.js (App Router)
import { randomUUID, timingSafeEqual } from "node:crypto";
import { NextResponse, type NextRequest } from "next/server";

export const runtime = "nodejs";

// Avox client: one Idempotency-Key per message, retries only what's safe to retry.
const RETRY_SAME_KEY = new Set(["rate_limited", "idempotency_in_progress", "internal_error", "network_error"]);
const RETRY_NEW_KEY = new Set(["meta_rate_limited"]);
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const backoffMs = (attempt: number) => Math.min(30_000, 1_000 * 2 ** (attempt - 1)) * (0.5 + Math.random() / 2);

type AvoxFailure = Error & { code: string; status: number; requestId: string | null; details?: unknown };

async function sendWhatsApp(message: Record<string, unknown>, idempotencyKey: string = randomUUID(), maxAttempts = 5) {
  let key = idempotencyKey;
  for (let attempt = 1; ; attempt++) {
    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": key,
      },
      body: JSON.stringify(message),
      cache: "no-store",
      signal: AbortSignal.timeout(30_000),
    }).catch(() => null);

    const data = res ? await res.json().catch(() => null) : null;
    const requestId = res?.headers.get("X-Request-Id") ?? null;
    if (data?.ok) {
      console.info(`[avox] sent message_id=${data.message_id} request_id=${requestId}`);
      return data as { ok: true; request_id: string; message_id: string | null; result: unknown };
    }

    const code: string = data?.error?.code ?? (res ? `http_${res.status}` : "network_error");
    console.warn(`[avox] ${code} request_id=${requestId} attempt=${attempt}`);
    const retryable = RETRY_SAME_KEY.has(code) || RETRY_NEW_KEY.has(code);
    if (!retryable || attempt >= maxAttempts) {
      throw Object.assign(new Error(`Avox send failed: ${code}`), {
        code,
        status: res?.status ?? 502,
        requestId: data?.error?.request_id ?? requestId,
        details: data?.error?.details,
      }) as AvoxFailure;
    }
    if (RETRY_NEW_KEY.has(code)) key = randomUUID(); // WhatsApp's refusal is stored against the old key
    const retryAfter = Number(res?.headers.get("Retry-After"));
    await sleep(retryAfter > 0 ? retryAfter * 1000 : backoffMs(attempt));
  }
}

function safeEqual(a: string | null, b: string | undefined) {
  const x = Buffer.from(a ?? "");
  const y = Buffer.from(b ?? "");
  return x.length > 0 && x.length === y.length && timingSafeEqual(x, y);
}

// POST /api/notify  { "to": "15551234567", "template": "order_shipped", "params": ["A-1042"], "reference": "order-A-1042-shipped" }
// Your own apps call this with the x-internal-token header; the key never leaves the server.
export async function POST(request: NextRequest) {
  if (!safeEqual(request.headers.get("x-internal-token"), process.env.INTERNAL_TOKEN)) {
    return NextResponse.json({ error: "unauthorized" }, { status: 401 });
  }
  const body = await request.json().catch(() => null);
  const { to, template, params = [], language = "en_US", reference } = body ?? {};
  if (!to || !template || !reference) {
    return NextResponse.json({ error: "to, template and reference are required" }, { status: 400 });
  }

  try {
    const result = await sendWhatsApp(
      {
        to,
        type: "template",
        template: {
          name: template,
          language: { code: language },
          ...(params.length ? { components: [{ type: "body", parameters: params.map((t: unknown) => ({ type: "text", text: String(t) })) }] } : {}),
        },
        biz_opaque_callback_data: reference,
      },
      reference // the same reference always means the same message: retries never send twice
    );
    return NextResponse.json({ ok: true, message_id: result.message_id });
  } catch (err) {
    const e = err as AvoxFailure;
    return NextResponse.json({ ok: false, code: e.code, request_id: e.requestId, details: e.details }, { status: e.status >= 500 ? 502 : 422 });
  }
}
```

**FastAPI**

```python
# main.py - pip install fastapi uvicorn httpx ; run: uvicorn main:app --port 3000
import asyncio
import hmac
import logging
import os
import random
import uuid

import httpx
from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse, Response

log = logging.getLogger("avox")
app = FastAPI()
http = httpx.AsyncClient(timeout=30)

# Avox client: one Idempotency-Key per message, retries only what's safe to retry.
RETRY_SAME_KEY = {"rate_limited", "idempotency_in_progress", "internal_error", "network_error"}
RETRY_NEW_KEY = {"meta_rate_limited"}


class AvoxError(Exception):
    def __init__(self, code, status, request_id, details=None):
        super().__init__(f"Avox send failed: {code}")
        self.code, self.status, self.request_id, self.details = code, status, request_id, details


def backoff(attempt):
    return min(30.0, 2 ** (attempt - 1)) * (0.5 + random.random() / 2)


async def send_whatsapp(message: dict, idempotency_key: str | None = None, max_attempts: int = 5) -> dict:
    key = idempotency_key or str(uuid.uuid4())
    attempt = 0
    while True:
        attempt += 1
        res, data = None, None
        try:
            res = await http.post(
                "https://api.avox.com/api/messages/send",
                headers={"Authorization": f"Bearer {os.environ['AVOX_API_KEY']}", "Idempotency-Key": key},
                json=message,
            )
            data = res.json()
        except (httpx.TransportError, ValueError):
            pass

        request_id = res.headers.get("X-Request-Id") if res is not None else None
        if data and data.get("ok"):
            log.info("sent message_id=%s request_id=%s", data.get("message_id"), request_id)
            return data

        error = (data or {}).get("error") or {}
        code = error.get("code") or (f"http_{res.status_code}" if res is not None else "network_error")
        log.warning("%s request_id=%s attempt=%s", code, request_id, attempt)
        if code not in RETRY_SAME_KEY | RETRY_NEW_KEY or attempt >= max_attempts:
            raise AvoxError(code, res.status_code if res is not None else 502,
                            error.get("request_id", request_id), error.get("details"))
        if code in RETRY_NEW_KEY:
            key = str(uuid.uuid4())  # WhatsApp's refusal is stored against the old key
        retry_after = int(res.headers.get("Retry-After") or 0) if res is not None else 0
        await asyncio.sleep(retry_after if retry_after > 0 else backoff(attempt))


def avox_error_response(err: AvoxError):
    status = 502 if err.status >= 500 else 422
    return JSONResponse({"ok": False, "code": err.code, "request_id": err.request_id, "details": err.details},
                        status_code=status)


# POST /notify  {"to": "15551234567", "template": "order_shipped", "params": ["A-1042"], "reference": "order-A-1042-shipped"}
# Your own apps call this with the x-internal-token header; the key never leaves this server.
@app.post("/notify")
async def notify(request: Request):
    token = request.headers.get("x-internal-token", "")
    if not token or not hmac.compare_digest(token, os.environ.get("INTERNAL_TOKEN", "")):
        return Response(status_code=401)
    body = await request.json()
    to, name, reference = body.get("to"), body.get("template"), body.get("reference")
    if not (to and name and reference):
        return JSONResponse({"error": "to, template and reference are required"}, status_code=400)

    template = {"name": name, "language": {"code": body.get("language", "en_US")}}
    params = body.get("params") or []
    if params:
        template["components"] = [{"type": "body", "parameters": [{"type": "text", "text": str(p)} for p in params]}]

    try:
        # The same reference always means the same message: retries never send twice.
        result = await send_whatsapp(
            {"to": to, "type": "template", "template": template, "biz_opaque_callback_data": reference},
            idempotency_key=reference,
        )
        return {"ok": True, "message_id": result.get("message_id")}
    except AvoxError as err:
        return avox_error_response(err)
```

Each version is a complete file: the Avox client (an `Idempotency-Key` per message, retries with backoff that honour `Retry-After`, `error.code` and `request_id` in every log line) plus the route. Run Express with Node.js 18 or later, the Next.js route in any App Router project, and FastAPI with Python 3.10 or later. For the full walkthrough - key setup, local testing with a tunnel, a deployment checklist - see [Connect your server](https://docs.theavoxai.com/avox-api/connect-your-server).

To receive this number's WhatsApp messages on your own server, see [Message forwarding](https://docs.theavoxai.com/integrations/webhooks).

## FAQ

**Does every plan include the API?**

API access depends on the plan. The plan cards on [Plans](https://docs.theavoxai.com/plans) and on **Billing > Change
plan** list **API access** on the plans that include it. On a plan without it, your keys stay listed
but can't send (`api_not_in_plan`), and new keys can't be created or rotated until you move to a plan
with API access. See [API access and your plan](https://docs.theavoxai.com/connections/api-keys#api-access-and-your-plan).

**Can one key send from several numbers?**

Each key belongs to one number, so create one key per number and pick the key in your code. See
[Authentication](https://docs.theavoxai.com/avox-api/authentication).

**How do I avoid duplicate messages when retrying?**

Send an `Idempotency-Key` header. See [Idempotency](https://docs.theavoxai.com/avox-api/idempotency).

**How do I get delivery statuses?**

Open [Logs](https://docs.theavoxai.com/logs): each message's `sent`, `delivered`, `read` and `failed` events carry its
`message_id`. See [Delivery statuses and replies](https://docs.theavoxai.com/avox-api/webhooks).

**Is there a test mode?**

Yes. Create a **Test key** (`avx_test_...`). It runs every check a live send does, then answers
with a pretend result (`"test": true`) and never sends anything. Special test numbers return each
WhatsApp error on purpose. The **Test** button on a key is a separate dry run that checks the key
and its number from the dashboard. See [Testing](https://docs.theavoxai.com/avox-api/testing).

**Where do I see what my integration sent?**

On the **API logs** page in the dashboard. The **Sent** tab lists every request made with your
keys: time, key, connection, outcome and error code, masked recipient, request id and latency. It
has filters, search by request id or message id, and **Export CSV**. See
[API logs](https://docs.theavoxai.com/connections/api-keys#api-logs).

**How do I replace a key?**

**Rotate** it in **Settings > API Keys**. You get a new key straight away and choose when the old
one stops: immediately, in 1 hour, in 24 hours or in 7 days. See
[Rotating a key](https://docs.theavoxai.com/avox-api/authentication#rotating-a-key).
