# Testing Your Avox API Integration

> Build against an avx_test_ key that never sends, trigger every error with special test numbers, check a key with the dashboard's dry run, then send real messages to your own phone.

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

Test in four layers: build against a **test key** that never sends anything, confirm your live key
with the dashboard's dry run, send real messages to your own phone, then make your code handle every
error it can meet. None of this needs a separate environment - test keys work against your own
connected number.

## 1. Test mode

A test key (`avx_test_...`) goes through exactly the same checks as a live key - the key, its number,
your plan, rate limits, the request body and [idempotency](https://docs.theavoxai.com/avox-api/idempotency) - and then, instead
of sending to WhatsApp, answers with a pretend result. Nothing reaches WhatsApp, nobody gets a
message, and nothing is charged.

An Admin (or a member with **Manage API keys**) creates one in **Settings > API Keys** (or on the
connection's page) by choosing **Test key**.
See [Live and test keys](https://docs.theavoxai.com/connections/api-keys#live-and-test-keys).

### The response

A test send answers like a live one, plus `"test": true` in the body and an `Avox-Test-Mode: true`
header on the response. The `message_id` starts with `wamid.TEST`, so it can never be mistaken for a
real one:

```http
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: req_3f9c2a7b1d4e5f60718293a4
Avox-Test-Mode: true

{
  "ok": true,
  "request_id": "req_3f9c2a7b1d4e5f60718293a4",
  "message_id": "wamid.TEST8F3A1C0B9D2E4F5A6B7C8D9E0F1A2B3C",
  "test": true,
  "result": {
    "messaging_product": "whatsapp",
    "messages": [{ "id": "wamid.TEST8F3A1C0B9D2E4F5A6B7C8D9E0F1A2B3C", "message_status": "accepted" }]
  }
}
```

Errors from a test key have the usual [error shape](https://docs.theavoxai.com/api/errors) with `"test": true` added next to
`"ok": false`, and the same `Avox-Test-Mode: true` header.

Every check still applies: a malformed body returns `invalid_payload`, a paused number returns
`connection_paused`, and so on. Test sends don't produce delivery statuses or replies, and they never
appear in your [Inbox](https://docs.theavoxai.com/inbox) or [Logs](https://docs.theavoxai.com/logs).

### Special test numbers

Send to one of these numbers with a test key to get a specific error on purpose. Any other valid
number succeeds.

| Send to | Expect | Simulates |
| --- | --- | --- |
| `+15550000001` | `400` [`invalid_recipient`](https://docs.theavoxai.com/api/errors#invalid_recipient) | A number that isn't valid |
| `+15550000002` | `422` [`outside_messaging_window`](https://docs.theavoxai.com/api/errors#outside_messaging_window) | More than 24 hours since the person last messaged you |
| `+15550000003` | `422` [`meta_rejected`](https://docs.theavoxai.com/api/errors#meta_rejected) | WhatsApp refusing the message |
| `+15550000004` | `422` [`recipient_unreachable`](https://docs.theavoxai.com/api/errors#recipient_unreachable) | A number that can't receive WhatsApp messages |
| `+15550000005` | `429` [`meta_rate_limited`](https://docs.theavoxai.com/api/errors#meta_rate_limited) with a `Retry-After` header | WhatsApp limiting how fast you send |
| `+15550000006` | `422` [`template_not_found`](https://docs.theavoxai.com/api/errors#template_not_found) | A template that doesn't exist in that language |
| `+15550000007` | `502` [`meta_unavailable`](https://docs.theavoxai.com/api/errors#meta_unavailable) | WhatsApp not answering in time |

Simulated errors include `meta_code` where WhatsApp would send one (for example `131047` for
`+15550000002`). With an `Idempotency-Key`, a retry gets the stored result back, except after
`meta_unavailable`, which frees the key so you can retry - the same as live.

**curl**

```bash
# Expect 422 outside_messaging_window, "test": true - nothing is sent.
curl -i -X POST https://api.avox.com/api/messages/send \
  -H "Authorization: Bearer $AVOX_TEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"+15550000002","type":"text","text":{"body":"test"}}'
```

**Node.js**

```js
import assert from "node:assert/strict";

const res = await fetch("https://api.avox.com/api/messages/send", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.AVOX_TEST_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ to: "+15550000002", type: "text", text: { body: "test" } }),
});
const data = await res.json();

assert.equal(res.status, 422);
assert.equal(data.error.code, "outside_messaging_window");
assert.equal(data.test, true);
assert.equal(res.headers.get("Avox-Test-Mode"), "true");
```

**Python**

```python
import os

import requests

res = requests.post(
    "https://api.avox.com/api/messages/send",
    headers={"Authorization": f"Bearer {os.environ['AVOX_TEST_KEY']}"},
    json={"to": "+15550000002", "type": "text", "text": {"body": "test"}},
    timeout=30,
)

assert res.status_code == 422
assert res.json()["error"]["code"] == "outside_messaging_window"
assert res.json()["test"] is True
assert res.headers.get("Avox-Test-Mode") == "true"
```

### Limits and logs

Test keys have their own, separate [rate limits](https://docs.theavoxai.com/avox-api/rate-limits#test-keys) and a daily
allowance per Switchboard, so a test suite never uses up your live allowance, and test requests don't
count toward a live key's daily allowance. Every test request appears on the
[API logs](https://docs.theavoxai.com/connections/api-keys#api-logs) page with a **Test** badge - choose **Test only** in the
mode filter to see just those, or search for a `wamid.TEST...` message id.

> **Tip**
>
> Keep test and live keys in separate settings (for example `AVOX_TEST_KEY` and `AVOX_API_KEY`), and
> check the `Avox-Test-Mode` header in your code if it matters which one answered.

## 2. The dashboard dry run

Every active key in **Settings > API Keys** has a **Test** button. Admins can use it, and so can any
member an Admin has given the **Manage API keys** permission (see
[Permissions](https://docs.theavoxai.com/switchboard/members#permissions)).

- It checks that the key is active, that its number is connected, and that the number is ready to
  send.
- It **doesn't** send a WhatsApp message and doesn't cost anything with Meta.
- It works the same for live and test keys. On a test key, the result adds "Test key: sends are
  simulated, nothing reaches WhatsApp."
- It's logged as **Test** in the key's **Recent requests**, separately from real sends. On the
  **API logs** page it shows the endpoint `Dashboard test`, and its **Request details** say
  **Source: Test button in the dashboard**.

The result appears under the key:

| Result | What to do |
| --- | --- |
| "Working - routes to" and the connection's name | The key is ready. Check it's the number you expect. |
| "This key has been revoked - create a new one to test." | Create a new key. See [`key_revoked`](https://docs.theavoxai.com/api/errors#key_revoked). |
| "This key has expired - use the key that replaced it, or create a new one." | The key was rotated and its grace period ended. See [`key_expired`](https://docs.theavoxai.com/api/errors#key_expired). |
| "This key's connection no longer exists." | Create a key for one of your current numbers. See [`connection_not_found`](https://docs.theavoxai.com/api/errors#connection_not_found). |
| "This key's connection is paused - resume it to send." | Resume the number on [Connections](https://docs.theavoxai.com/connections). See [`connection_paused`](https://docs.theavoxai.com/api/errors#connection_paused). |
| "This key's connection isn't active right now." | Check the number on [Connections](https://docs.theavoxai.com/connections) and [Health](https://docs.theavoxai.com/health). See [`connection_inactive`](https://docs.theavoxai.com/api/errors#connection_inactive). |
| "This connection needs to be reconnected before it can send." | Reconnect the number. See [`connection_not_ready`](https://docs.theavoxai.com/api/errors#connection_not_ready). |

The dry run checks the key and its number from inside the dashboard. It doesn't check your
server's copy of the key, your server's IP address, or your request body - that's what test mode and
the next step are for.

## 3. Send to your own phone

With a **live** key, use a personal WhatsApp number you control as the recipient - not the connected
number itself (WhatsApp refuses a message to the number sending it, with
[`recipient_unreachable`](https://docs.theavoxai.com/api/errors#recipient_unreachable)).

**1. Open a 24-hour window**

From your personal phone, send any message to the connected number. Now you can receive
non-template messages from it for 24 hours.

**2. Send a text message**

Run the [quickstart](https://docs.theavoxai.com/avox-api) example with your personal number as `to`. Expect `200` and a
`message_id`.

**3. Send a template**

Pick an approved template on [Templates](https://docs.theavoxai.com/templates) and send it with its exact name and
language. This is the message type that works even with no open window.

**4. Try each type you plan to use**

Media, interactive buttons and lists, locations - see [Message types](https://docs.theavoxai.com/avox-api/messages).
Tap the buttons on your phone to produce real replies.

**5. Watch the statuses arrive**

Open [Logs](https://docs.theavoxai.com/logs): each message produces `sent`, `delivered` and `read` events carrying its
`message_id`. Your phone's replies appear in the [Inbox](https://docs.theavoxai.com/inbox). See
[Delivery statuses and replies](https://docs.theavoxai.com/avox-api/webhooks).

**6. Check the API logs**

Open **API logs** in the dashboard: each send shows its key with a **Live** badge, its status
code, and your number with most digits hidden. Search by the `message_id` to find it.

## 4. Exercise the error paths

These requests are safe with either kind of key: each one is refused before anything is delivered.

Some problems are only found by WhatsApp after it accepts a message - for example, a text to
someone whose 24-hour window has closed. Those can come back as a `200` now and a `failed` status on
[Logs](https://docs.theavoxai.com/logs) shortly after, so check there too when testing live. With a test key, use the
[special test numbers](#special-test-numbers) to get those errors straight away.

| Try this | Expect |
| --- | --- |
| No `Authorization` header | `401` [`missing_api_key`](https://docs.theavoxai.com/api/errors#missing_api_key) |
| `Authorization: Bearer avx_live_wrong` | `401` [`invalid_api_key`](https://docs.theavoxai.com/api/errors#invalid_api_key) |
| Body that isn't JSON | `400` [`invalid_payload`](https://docs.theavoxai.com/api/errors#invalid_payload) |
| `"to": "0123"` | `400` [`invalid_recipient`](https://docs.theavoxai.com/api/errors#invalid_recipient) |
| `"type": "poll"` | `400` [`unsupported_message_type`](https://docs.theavoxai.com/api/errors#unsupported_message_type) |
| Text over 4096 characters | `400` [`invalid_payload`](https://docs.theavoxai.com/api/errors#invalid_payload) with `details` |
| An image with both `id` and `link` | `400` [`invalid_payload`](https://docs.theavoxai.com/api/errors#invalid_payload) |
| `Idempotency-Key: has space` | `400` [`invalid_idempotency_key`](https://docs.theavoxai.com/api/errors#invalid_idempotency_key) |
| Same `Idempotency-Key`, different body | `409` [`idempotency_conflict`](https://docs.theavoxai.com/api/errors#idempotency_conflict) |
| A template name that doesn't exist (live key) | `422` [`template_not_found`](https://docs.theavoxai.com/api/errors#template_not_found) |

**curl**

```bash
# Expect 400 invalid_recipient - nothing is sent.
curl -i -X POST https://api.avox.com/api/messages/send \
  -H "Authorization: Bearer $AVOX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"0123","type":"text","text":{"body":"test"}}'
```

**Node.js**

```js
import assert from "node:assert/strict";

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" },
  body: JSON.stringify({ to: "0123", type: "text", text: { body: "test" } }),
});
const data = await res.json();

assert.equal(res.status, 400);
assert.equal(data.error.code, "invalid_recipient");
assert.ok(res.headers.get("X-Request-Id"));
```

**Python**

```python
import os

import requests

res = requests.post(
    "https://api.avox.com/api/messages/send",
    headers={"Authorization": f"Bearer {os.environ['AVOX_API_KEY']}"},
    json={"to": "0123", "type": "text", "text": {"body": "test"}},
    timeout=30,
)

assert res.status_code == 400
assert res.json()["error"]["code"] == "invalid_recipient"
assert res.headers.get("X-Request-Id")
```

Each of these still counts towards your [rate limits](https://docs.theavoxai.com/avox-api/rate-limits) and shows on the API
logs page, so run them at a normal pace. Wrong keys, especially, are limited per address.

## Testing in your own CI

- **Integration tests:** use a **test key** for your CI system. It runs every check a live send does,
  sends nothing, and the [special test numbers](#special-test-numbers) cover each error path.
- **Unit tests:** don't call the API. Stub the HTTP call and feed your code real response bodies
  from these docs - a success, a `429` with `Retry-After`, a `422`, a `502`.
- **Live smoke tests:** if you need one, use a dedicated live key and send only to numbers you own.
  Revoke the key when the test system is retired.
- **Event fixtures:** if your code parses WhatsApp event payloads, copy a few real ones from
  [Logs](https://docs.theavoxai.com/logs) and use them as test fixtures.

When everything here passes, work through the [Go-live checklist](https://docs.theavoxai.com/avox-api/go-live) - including
switching your production server from the test key to a live key.
