Every message type the Avox API sends - text, templates, media, location, contacts, interactive, reactions - with field limits and full examples.
Every message goes to the same endpoint, POST https://api.avox.com/api/messages/send. The body
follows WhatsApp's own Cloud API message format: to, type, and one object named after the
type. This page shows every type the API accepts, with the limits Avox checks before anything is
sent.
The 24-hour rule is WhatsApp's: outside it, only a template goes through, and anything else
returns outside_messaging_window. See
Messaging window.
The curl examples are complete on their own. The Node.js and Python examples on this page call a
small send helper, so each example only shows the message itself:
export AVOX_API_KEY="avx_live_your_key_here"
// send.js - Node.js 18 or later (built-in fetch)
import { randomUUID } from "node:crypto";
export async function send(message, idempotencyKey = randomUUID()) {
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": idempotencyKey,
},
body: JSON.stringify(message),
});
const data = await res.json();
if (!data.ok) {
throw new Error(`${res.status} ${data.error.code}: ${data.error.message}`);
}
return data;
}
# send.py
import os
import uuid
import requests
def send(message, idempotency_key=None):
res = requests.post(
"https://api.avox.com/api/messages/send",
headers={
"Authorization": f"Bearer {os.environ['AVOX_API_KEY']}",
"Idempotency-Key": idempotency_key or str(uuid.uuid4()),
},
json=message,
timeout=30,
)
data = res.json()
if not data["ok"]:
raise RuntimeError(f"{res.status_code} {data['error']['code']}: {data['error']['message']}")
return data
Every successful send returns the same shape, whatever the type:
{
"ok": true,
"request_id": "req_4f1c9a0b7e2d36a8c5b1f0e9",
"message_id": "wamid.HBgLMTU1NTEyMzQ1NjcVAgARGBI5",
"result": {
"messaging_product": "whatsapp",
"contacts": [{ "input": "15551234567", "wa_id": "15551234567" }],
"messages": [{ "id": "wamid.HBgLMTU1NTEyMzQ1NjcVAgARGBI5" }]
}
}
result is WhatsApp's own reply, passed back unchanged. message_id is the same id as
result.messages[0].id, lifted to the top level for convenience.
A test key accepts exactly the same bodies and runs the same checks
on them, so every example here works in test mode too - the answer adds "test": true and a
message_id starting with wamid.TEST, and nothing is sent.
Any other top-level field (for example messaging_product) is dropped. Avox adds
messaging_product: "whatsapp" itself. The whole body must be under 64 KB.
text.body is required: 1 to 4096 characters. Set text.preview_url to true to show a preview
of the first link in the message.
curl -X POST https://api.avox.com/api/messages/send \
-H "Authorization: Bearer $AVOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "15551234567",
"type": "text",
"text": {
"body": "Your order A-1042 has shipped: https://example.com/track/A-1042",
"preview_url": true
}
}'
import { send } from "./send.js";
const result = await send({
to: "15551234567",
type: "text",
text: {
body: "Your order A-1042 has shipped: https://example.com/track/A-1042",
preview_url: true,
},
});
console.log(result.message_id);
from send import send
result = send({
"to": "15551234567",
"type": "text",
"text": {
"body": "Your order A-1042 has shipped: https://example.com/track/A-1042",
"preview_url": True,
},
})
print(result["message_id"])
Add context.message_id with the id of the customer's message to quote it:
{
"to": "15551234567",
"type": "text",
"context": { "message_id": "wamid.HBgLMTU1NTEyMzQ1NjcVAgASGBQzQTdC" },
"text": { "body": "Yes - your table for 4 is booked for 8pm." }
}
Templates are the only messages you can send outside the 24-hour window, and the only way to start a conversation. The template must be approved for the WhatsApp Business Account behind your key's number. Find the exact name and language on the Templates page.
Parameters fill the template's placeholders in order ({{1}}, {{2}}, ...). For a template built
with named placeholders, add parameter_name to each parameter, as WhatsApp's format describes.
curl -X POST https://api.avox.com/api/messages/send \
-H "Authorization: Bearer $AVOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "15551234567",
"type": "template",
"template": {
"name": "order_shipped",
"language": { "code": "en_US" },
"components": [
{
"type": "header",
"parameters": [
{ "type": "image", "image": { "link": "https://example.com/parcel.jpg" } }
]
},
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Maya" },
{ "type": "text", "text": "A-1042" }
]
},
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [{ "type": "text", "text": "A-1042" }]
}
]
}
}'
import { send } from "./send.js";
await send({
to: "15551234567",
type: "template",
template: {
name: "order_shipped",
language: { code: "en_US" },
components: [
{
type: "header",
parameters: [{ type: "image", image: { link: "https://example.com/parcel.jpg" } }],
},
{
type: "body",
parameters: [
{ type: "text", text: "Maya" },
{ type: "text", text: "A-1042" },
],
},
{
type: "button",
sub_type: "url",
index: "0",
parameters: [{ type: "text", text: "A-1042" }],
},
],
},
});
from send import send
send({
"to": "15551234567",
"type": "template",
"template": {
"name": "order_shipped",
"language": {"code": "en_US"},
"components": [
{
"type": "header",
"parameters": [{"type": "image", "image": {"link": "https://example.com/parcel.jpg"}}],
},
{
"type": "body",
"parameters": [
{"type": "text", "text": "Maya"},
{"type": "text", "text": "A-1042"},
],
},
{
"type": "button",
"sub_type": "url",
"index": "0",
"parameters": [{"type": "text", "text": "A-1042"}],
},
],
},
})
A template with no placeholders needs only name and language:
{
"to": "15551234567",
"type": "template",
"template": { "name": "hello_world", "language": { "code": "en_US" } }
}
For a quick reply button that should send your server a known value, use a payload parameter:
{
"type": "button",
"sub_type": "quick_reply",
"index": 0,
"parameters": [{ "type": "payload", "payload": "CONFIRM_A-1042" }]
}
WhatsApp's template errors come back as their own codes:
template_not_found (wrong name or language),
template_unavailable (paused or disabled), and
template_params_invalid (parameters don't fit). See
Template approval and Template categories.
image, video, audio, document and sticker all point at the file one of two ways - give
exactly one:
link - a public https:// URL, under 2048 characters. WhatsApp downloads the file from there,
so it must be reachable without signing in.id - a WhatsApp media id that already exists for this number (letters, numbers, - and _,
up to 128 characters).Sending both, or neither, returns invalid_payload with "Give
exactly one of id or link". If WhatsApp can't download a link, the send is refused with
meta_rejected.
WhatsApp also sets its own rules for file types and sizes per media type, and checks them when it downloads the file.
curl -X POST https://api.avox.com/api/messages/send \
-H "Authorization: Bearer $AVOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "15551234567",
"type": "image",
"image": {
"link": "https://example.com/receipts/A-1042.png",
"caption": "Your receipt for order A-1042"
}
}'
import { send } from "./send.js";
await send({
to: "15551234567",
type: "image",
image: {
link: "https://example.com/receipts/A-1042.png",
caption: "Your receipt for order A-1042",
},
});
from send import send
send({
"to": "15551234567",
"type": "image",
"image": {
"link": "https://example.com/receipts/A-1042.png",
"caption": "Your receipt for order A-1042",
},
})
curl -X POST https://api.avox.com/api/messages/send \
-H "Authorization: Bearer $AVOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "15551234567",
"type": "video",
"video": {
"link": "https://example.com/videos/setup-guide.mp4",
"caption": "A 60-second setup guide"
}
}'
import { send } from "./send.js";
await send({
to: "15551234567",
type: "video",
video: { link: "https://example.com/videos/setup-guide.mp4", caption: "A 60-second setup guide" },
});
from send import send
send({
"to": "15551234567",
"type": "video",
"video": {"link": "https://example.com/videos/setup-guide.mp4", "caption": "A 60-second setup guide"},
})
curl -X POST https://api.avox.com/api/messages/send \
-H "Authorization: Bearer $AVOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "15551234567",
"type": "audio",
"audio": { "link": "https://example.com/audio/welcome.mp3" }
}'
import { send } from "./send.js";
await send({
to: "15551234567",
type: "audio",
audio: { link: "https://example.com/audio/welcome.mp3" },
});
from send import send
send({
"to": "15551234567",
"type": "audio",
"audio": {"link": "https://example.com/audio/welcome.mp3"},
})
curl -X POST https://api.avox.com/api/messages/send \
-H "Authorization: Bearer $AVOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "15551234567",
"type": "document",
"document": {
"link": "https://example.com/invoices/INV-2041.pdf",
"filename": "INV-2041.pdf",
"caption": "Your invoice for September"
}
}'
import { send } from "./send.js";
await send({
to: "15551234567",
type: "document",
document: {
link: "https://example.com/invoices/INV-2041.pdf",
filename: "INV-2041.pdf",
caption: "Your invoice for September",
},
});
from send import send
send({
"to": "15551234567",
"type": "document",
"document": {
"link": "https://example.com/invoices/INV-2041.pdf",
"filename": "INV-2041.pdf",
"caption": "Your invoice for September",
},
})
WhatsApp stickers are WebP images.
curl -X POST https://api.avox.com/api/messages/send \
-H "Authorization: Bearer $AVOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "15551234567",
"type": "sticker",
"sticker": { "link": "https://example.com/stickers/thanks.webp" }
}'
import { send } from "./send.js";
await send({
to: "15551234567",
type: "sticker",
sticker: { link: "https://example.com/stickers/thanks.webp" },
});
from send import send
send({
"to": "15551234567",
"type": "sticker",
"sticker": {"link": "https://example.com/stickers/thanks.webp"},
})
curl -X POST https://api.avox.com/api/messages/send \
-H "Authorization: Bearer $AVOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "15551234567",
"type": "location",
"location": {
"latitude": 51.5072,
"longitude": -0.1276,
"name": "Acme Store - Central",
"address": "1 Example Street, London"
}
}'
import { send } from "./send.js";
await send({
to: "15551234567",
type: "location",
location: {
latitude: 51.5072,
longitude: -0.1276,
name: "Acme Store - Central",
address: "1 Example Street, London",
},
});
from send import send
send({
"to": "15551234567",
"type": "location",
"location": {
"latitude": 51.5072,
"longitude": -0.1276,
"name": "Acme Store - Central",
"address": "1 Example Street, London",
},
})
contacts is an array of 1 to 20 contact cards. Each card needs name.formatted_name (1 to 1000
characters). Every other field WhatsApp's contact format supports (phones, emails, org,
urls, addresses, the rest of name) is passed through.
curl -X POST https://api.avox.com/api/messages/send \
-H "Authorization: Bearer $AVOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "15551234567",
"type": "contacts",
"contacts": [
{
"name": { "formatted_name": "Acme Support", "first_name": "Acme" },
"phones": [{ "phone": "+15550001111", "type": "WORK" }],
"emails": [{ "email": "help@example.com", "type": "WORK" }]
}
]
}'
import { send } from "./send.js";
await send({
to: "15551234567",
type: "contacts",
contacts: [
{
name: { formatted_name: "Acme Support", first_name: "Acme" },
phones: [{ phone: "+15550001111", type: "WORK" }],
emails: [{ email: "help@example.com", type: "WORK" }],
},
],
});
from send import send
send({
"to": "15551234567",
"type": "contacts",
"contacts": [
{
"name": {"formatted_name": "Acme Support", "first_name": "Acme"},
"phones": [{"phone": "+15550001111", "type": "WORK"}],
"emails": [{"email": "help@example.com", "type": "WORK"}],
}
],
})
Interactive messages carry buttons, lists, link buttons or a Flow. Avox checks the shared parts;
WhatsApp checks the rest (such as how many buttons or rows each kind allows) and refuses anything
outside its rules with meta_rejected.
When the customer taps a button, its id comes back in the reply. With message forwarding on, your
server gets it as data.interactive.id in a message.received event. See
Interactive replies.
curl -X POST https://api.avox.com/api/messages/send \
-H "Authorization: Bearer $AVOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "15551234567",
"type": "interactive",
"interactive": {
"type": "button",
"body": { "text": "Is Friday at 3pm still good for delivery?" },
"footer": { "text": "Acme Deliveries" },
"action": {
"buttons": [
{ "type": "reply", "reply": { "id": "slot_keep", "title": "Yes, Friday" } },
{ "type": "reply", "reply": { "id": "slot_change", "title": "Pick another day" } }
]
}
}
}'
import { send } from "./send.js";
await send({
to: "15551234567",
type: "interactive",
interactive: {
type: "button",
body: { text: "Is Friday at 3pm still good for delivery?" },
footer: { text: "Acme Deliveries" },
action: {
buttons: [
{ type: "reply", reply: { id: "slot_keep", title: "Yes, Friday" } },
{ type: "reply", reply: { id: "slot_change", title: "Pick another day" } },
],
},
},
});
from send import send
send({
"to": "15551234567",
"type": "interactive",
"interactive": {
"type": "button",
"body": {"text": "Is Friday at 3pm still good for delivery?"},
"footer": {"text": "Acme Deliveries"},
"action": {
"buttons": [
{"type": "reply", "reply": {"id": "slot_keep", "title": "Yes, Friday"}},
{"type": "reply", "reply": {"id": "slot_change", "title": "Pick another day"}},
]
},
},
})
curl -X POST https://api.avox.com/api/messages/send \
-H "Authorization: Bearer $AVOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "15551234567",
"type": "interactive",
"interactive": {
"type": "list",
"header": { "type": "text", "text": "Book a visit" },
"body": { "text": "Choose a time that suits you." },
"action": {
"button": "See times",
"sections": [
{
"title": "Tomorrow",
"rows": [
{ "id": "t_0900", "title": "9:00", "description": "Morning" },
{ "id": "t_1400", "title": "14:00", "description": "Afternoon" }
]
}
]
}
}
}'
import { send } from "./send.js";
await send({
to: "15551234567",
type: "interactive",
interactive: {
type: "list",
header: { type: "text", text: "Book a visit" },
body: { text: "Choose a time that suits you." },
action: {
button: "See times",
sections: [
{
title: "Tomorrow",
rows: [
{ id: "t_0900", title: "9:00", description: "Morning" },
{ id: "t_1400", title: "14:00", description: "Afternoon" },
],
},
],
},
},
});
from send import send
send({
"to": "15551234567",
"type": "interactive",
"interactive": {
"type": "list",
"header": {"type": "text", "text": "Book a visit"},
"body": {"text": "Choose a time that suits you."},
"action": {
"button": "See times",
"sections": [
{
"title": "Tomorrow",
"rows": [
{"id": "t_0900", "title": "9:00", "description": "Morning"},
{"id": "t_1400", "title": "14:00", "description": "Afternoon"},
],
}
],
},
},
})
{
"to": "15551234567",
"type": "interactive",
"interactive": {
"type": "cta_url",
"body": { "text": "Your September statement is ready." },
"action": {
"name": "cta_url",
"parameters": { "display_text": "View statement", "url": "https://example.com/statements/2026-09" }
}
}
}
Opens a published WhatsApp Flow inside the chat. Get the Flow's id from the Flows page.
{
"to": "15551234567",
"type": "interactive",
"interactive": {
"type": "flow",
"body": { "text": "Tell us how your visit went - it takes a minute." },
"action": {
"name": "flow",
"parameters": {
"flow_message_version": "3",
"flow_id": "1234567890",
"flow_cta": "Start survey",
"flow_token": "survey-A-1042",
"flow_action": "navigate",
"flow_action_payload": { "screen": "WELCOME" }
}
}
}
}
See Flows on WhatsApp for how Flows work on Meta's side.
curl -X POST https://api.avox.com/api/messages/send \
-H "Authorization: Bearer $AVOX_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "15551234567",
"type": "reaction",
"reaction": {
"message_id": "wamid.HBgLMTU1NTEyMzQ1NjcVAgASGBQzQTdC",
"emoji": "👍"
}
}'
import { send } from "./send.js";
await send({
to: "15551234567",
type: "reaction",
reaction: { message_id: "wamid.HBgLMTU1NTEyMzQ1NjcVAgASGBQzQTdC", emoji: "\u{1F44D}" },
});
from send import send
send({
"to": "15551234567",
"type": "reaction",
"reaction": {"message_id": "wamid.HBgLMTU1NTEyMzQ1NjcVAgASGBQzQTdC", "emoji": "\U0001F44D"},
})
Avox checks the body in this order and stops at the first problem:
to must be a valid number - otherwise invalid_recipient.type must be present - otherwise invalid_payload with
path type.type must be one of the eleven types - otherwise
unsupported_message_type.invalid_payload.invalid_payload lists up to 10 problems in details. Each has a path (the field, with dots
between levels) and a message. Your submitted values are never echoed back:
{
"ok": false,
"error": {
"code": "invalid_payload",
"message": "The request body isn't valid. The details list says which fields to fix.",
"docs_url": "https://docs.avox.com/api/errors#invalid_payload",
"request_id": "req_0c7d1e4b9a2f63815e7d0a42",
"details": [
{ "path": "text.body", "message": "Text messages must be at most 4096 characters" }
]
}
}
Last updated