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.
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.
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 - 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.
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/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 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 or Logs.
Send to one of these numbers with a test key to get a specific error on purpose. Any other valid number succeeds.
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.
# 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"}}'
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");
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"
Test keys have their own, separate rate limits 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 page with a Test badge - choose Test only in the
mode filter to see just those, or search for a wamid.TEST... message id.
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.
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).
Dashboard test, and its Request details say
Source: Test button in the dashboard.The result appears under the key:
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.
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).
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.
Send a text message
Run the quickstart example with your personal number as to. Expect 200 and a
message_id.
Send a template
Pick an approved template on Templates and send it with its exact name and language. This is the message type that works even with no open window.
Try each type you plan to use
Media, interactive buttons and lists, locations - see Message types. Tap the buttons on your phone to produce real replies.
Watch the statuses arrive
Open Logs: each message produces sent, delivered and read events carrying its
message_id. Your phone's replies appear in the Inbox. See
Delivery statuses and replies.
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.
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 shortly after, so check there too when testing live. With a test key, use the
special test numbers to get those errors straight away.
# 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"}}'
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"));
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 and shows on the API logs page, so run them at a normal pace. Wrong keys, especially, are limited per address.
429 with Retry-After, a 422, a 502.When everything here passes, work through the Go-live checklist - including switching your production server from the test key to a live key.
Last updated