WhatsApp: send messages through the API
Send text, image, video, audio, document, sticker, location, buttons and lists on WhatsApp through the XBASE API, within the 24-hour window.
Updated on
POST /whatsapp/messages sends a free-form message to a contact, within the 24-hour window. Outside it, send a template.
Send a message
POST https://api.xbase.app/api/external/v1/whatsapp/messages| Field | Type | Required | Rule |
|---|---|---|---|
numberId | text | yes | Sending number (list the numbers) |
to | text | yes | Contact phone with the country code, 8 to 15 digits. +, spaces, hyphens and parentheses are removed |
type | text | yes | text, image, video, audio, document, sticker, location, buttons, list or flow |
| type object | object | yes | A field with the same name as type, like "text": { "body": "…" }. See each type below |
curl -X POST https://api.xbase.app/api/external/v1/whatsapp/messages \
-H "Authorization: Bearer $XBASE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"numberId": "6d1f3c2a-4b5e-4f60-9a7b-8c9d0e1f2a3b",
"to": "15555550100",
"type": "text",
"text": { "body": "Your order is out for delivery and arrives today by 6 PM." }
}'The response is in Response. An extra field in the body is rejected with 400.
Text
{
"numberId": "6d1f3c2a-4b5e-4f60-9a7b-8c9d0e1f2a3b",
"to": "15555550100",
"type": "text",
"text": { "body": "Hi, Mary! How can I help?" }
}text.body has 1 to 4,096 characters. Links in the text are clickable on WhatsApp.
Media
Image, video, audio, document and sticker go by public https URL: Meta downloads the file from it. An http URL, a URL with a username and password, or an internal or private network address returns 400.
{
"numberId": "6d1f3c2a-4b5e-4f60-9a7b-8c9d0e1f2a3b",
"to": "15555550100",
"type": "image",
"image": {
"link": "https://cdn.mycompany.com/catalog/blue-sneakers.jpg",
"caption": "Blue sneakers, sizes 6 to 12"
}
}type | Object | Meta rules |
|---|---|---|
image | link, caption | JPEG or PNG, up to 5 MB |
video | link, caption | MP4 or 3GP, up to 16 MB |
audio | link, voice | AAC, MP3, MP4 or OGG, up to 16 MB. "voice": true shows it as a voice note, and the file must be OGG with the Opus codec |
document | link, filename, caption | Any file, up to 100 MB. filename (up to 240 characters) is the name the contact sees |
sticker | link | WebP of 512 × 512 pixels |
caption is optional, up to 1,024 characters.
Location
{
"numberId": "6d1f3c2a-4b5e-4f60-9a7b-8c9d0e1f2a3b",
"to": "15555550100",
"type": "location",
"location": {
"latitude": -23.5614,
"longitude": -46.6559,
"name": "Paulista store",
"address": "Av. Paulista, 1000, São Paulo"
}
}| Field | Rule |
|---|---|
location.latitude | From −90 to 90 |
location.longitude | From −180 to 180 |
location.name · location.address | Optional, up to 1,000 characters |
Buttons
{
"numberId": "6d1f3c2a-4b5e-4f60-9a7b-8c9d0e1f2a3b",
"to": "15555550100",
"type": "buttons",
"buttons": {
"header": "Order 12345",
"body": "Can we confirm the delivery for tomorrow?",
"footer": "Tap an option",
"buttons": [
{ "id": "confirm", "title": "Confirm" },
{ "id": "reschedule", "title": "Reschedule" }
]
}
}| Field | Rule |
|---|---|
buttons.body | Message text, up to 1,024 characters |
buttons.header · buttons.footer | Optional, up to 60 characters |
buttons.buttons | 1 to 3 buttons. id (up to 256 characters, no repeats) comes back when the contact taps; title has up to 20 characters |
List
{
"numberId": "6d1f3c2a-4b5e-4f60-9a7b-8c9d0e1f2a3b",
"to": "15555550100",
"type": "list",
"list": {
"body": "Choose the delivery time.",
"button": "See times",
"rows": [
{ "id": "morning", "title": "Morning", "description": "8 AM to 12 PM" },
{ "id": "afternoon", "title": "Afternoon", "description": "1 PM to 6 PM" }
]
}
}| Field | Rule |
|---|---|
list.body | Message text, up to 1,024 characters |
list.button | Text of the button that opens the list, up to 20 characters |
list.rows | 1 to 10 rows. id (up to 200 characters, no repeats) comes back when the contact picks it; title up to 24 characters; description optional, up to 72 |
list.header · list.footer | Optional, up to 60 characters |
The tapped button or the chosen row arrives as whatsapp.message.received in the workspace webhooks, with the id you sent.
Flow
To open a WhatsApp Flow (a form inside WhatsApp), use "type": "flow". The fields and an example are in Flows.
Response
Response 200:
{
"status": "accepted",
"messageId": "wamid.HBgNNTUyMTk5OTk5MDAwMBUCABEYEjQ1QzM1RkQ2RDhGNjZBMDQ3MwA=",
"threadId": "c1d7f0b2-5a4e-4f3d-9c2b-1a0e9d8c7b6a",
"dialogId": "5a0e4c31-7b2d-4e8f-a1c3-9d6b5e4f3a21"
}| Field | Description |
|---|---|
status | accepted: Meta accepted the send. Delivery and read come later, in the webhook events |
messageId | Message id at Meta (wamid.…). It goes in the reaction and shows up in the events |
threadId | The contact's conversation at XBASE |
dialogId | Message in the conversation |
Errors
| Status | code | When |
|---|---|---|
| 400 | (no code) | Invalid field: phone, unknown type, empty or too long text, http or internal URL, extra field. The message says which one |
| 404 | NUMBER_NOT_FOUND | The number does not exist in this workspace |
| 422 | NUMBER_DISCONNECTED | The number is disconnected |
| 422 | WINDOW_CLOSED | Outside the 24-hour window. Send a template |
| 422 | SEND_REJECTED | XBASE refused before sending. The message says why |
| 422 | META_ERROR | Meta refused, for example, media it could not download. details.metaCode has Meta's code |
In WINDOW_CLOSED, details.reason says why the window is closed: no_contact_message (the contact never wrote to this number), window_expired (their last message is more than 24 hours old) or awaiting_template_reply (the last send was a template and the contact has not replied yet). details.lastContactMessageAt has the date of the contact's last message.
{
"statusCode": 422,
"message": "Janela de atendimento de 24h expirada. Envie apenas templates aprovados pelo WhatsApp.",
"code": "WINDOW_CLOSED",
"details": { "reason": "window_expired", "lastContactMessageAt": "2026-11-03T14:02:11.000Z" }
}