Guides
Send messages
Text, media, location and templates: formats, the 24-hour window, statuses, history and errors.
Updated November 12, 2026View as Markdown
POST /messages accepts a message and responds immediately (202, status queued). The actual delivery to Meta, any automatic retries and status tracking happen in the background.
Common fields#
| Field | Description |
|---|---|
phoneNumberId | The sending number: the id returned by the account connection. |
to | The recipient, in international format with a "+": +237600000000. Spaces, hyphens and parentheses are ignored. |
type | text, template, image, video, audio, document or location. |
A template (at any time)#
A template approved by Meta can be sent at any time, even without a prior exchange. It is the only way to start a conversation. See Templates.
curl -X POST "$INBOX_API/messages" \
-H "Authorization: Bearer $INBOX_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: rdv-2026-11-14-client-42" \
-d '{
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "+237600000000",
"type": "template",
"template": {
"name": "appointment_reminder",
"language": "en",
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "Awa"
},
{
"type": "text",
"text": "November 14 at 10 am"
}
]
}
]
}
}'const res = await fetch(`${process.env.INBOX_API}/messages`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INBOX_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": "rdv-2026-11-14-client-42",
},
body: JSON.stringify({
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "+237600000000",
"type": "template",
"template": {
"name": "appointment_reminder",
"language": "en",
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "Awa"
},
{
"type": "text",
"text": "November 14 at 10 am"
}
]
}
]
}
}),
});
const data = await res.json();import os
import requests
res = requests.post(
f"{os.environ['INBOX_API']}/messages",
headers={
"Authorization": f"Bearer {os.environ['INBOX_KEY']}",
"Idempotency-Key": "rdv-2026-11-14-client-42",
},
json={
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "+237600000000",
"type": "template",
"template": {
"name": "appointment_reminder",
"language": "en",
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "Awa",
},
{
"type": "text",
"text": "November 14 at 10 am",
},
],
},
],
},
},
)
print(res.json())<?php
$ch = curl_init(getenv('INBOX_API') . '/messages');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INBOX_KEY'),
'Content-Type: application/json',
'Idempotency-Key: rdv-2026-11-14-client-42',
],
CURLOPT_POSTFIELDS => json_encode([
'phoneNumberId' => '4d8e654e-d744-4bf3-8211-9ceffcccfe87',
'to' => '+237600000000',
'type' => 'template',
'template' => [
'name' => 'appointment_reminder',
'language' => 'en',
'components' => [
[
'type' => 'body',
'parameters' => [
[
'type' => 'text',
'text' => 'Awa',
],
[
'type' => 'text',
'text' => 'November 14 at 10 am',
],
],
],
],
],
]),
]);
$data = json_decode(curl_exec($ch), true);{
"id": "d5ad1e3e-6ab9-4f7f-8e4e-b17104a7bd00",
"direction": "outbound",
"status": "queued",
"type": "template",
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "237600000000",
"waMessageId": null,
"content": {
"type": "template",
"template": {
"name": "appointment_reminder",
"language": "en",
"components": [
{
"type": "body",
"parameters": [
{
"type": "text",
"text": "Awa"
},
{
"type": "text",
"text": "November 14 at 10 am"
}
]
}
]
}
},
"error": null,
"createdAt": "2026-11-12T08:33:45.855Z",
"sentAt": null,
"deliveredAt": null,
"readAt": null
}A free-form message: the 24-hour window#
A free-form message is only accepted if the recipient has written to you within the last 24 hours on this number. Otherwise:
{
"error": {
"code": "outside_service_window",
"message": "This contact has not written to you in the last 24 hours: only an approved template can be sent to them",
"requestId": "req_4fdba006-98d4-4164-893d-98389fb54c62"
}
}Within the window, all message types are allowed.
Text#
curl -X POST "$INBOX_API/messages" \
-H "Authorization: Bearer $INBOX_KEY" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "+237611111111",
"type": "text",
"text": {
"body": "Hello Awa, we have received your request."
}
}'const res = await fetch(`${process.env.INBOX_API}/messages`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INBOX_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "+237611111111",
"type": "text",
"text": {
"body": "Hello Awa, we have received your request."
}
}),
});
const data = await res.json();import os
import requests
res = requests.post(
f"{os.environ['INBOX_API']}/messages",
headers={
"Authorization": f"Bearer {os.environ['INBOX_KEY']}",
},
json={
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "+237611111111",
"type": "text",
"text": {
"body": "Hello Awa, we have received your request.",
},
},
)
print(res.json())<?php
$ch = curl_init(getenv('INBOX_API') . '/messages');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INBOX_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'phoneNumberId' => '4d8e654e-d744-4bf3-8211-9ceffcccfe87',
'to' => '+237611111111',
'type' => 'text',
'text' => [
'body' => 'Hello Awa, we have received your request.',
],
]),
]);
$data = json_decode(curl_exec($ch), true);{
"id": "2dbd95d9-4091-4cbf-8f75-c2dea9f39372",
"direction": "outbound",
"status": "queued",
"type": "text",
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "237611111111",
"waMessageId": null,
"content": {
"type": "text",
"text": {
"body": "Hello Awa, we have received your request.",
"previewUrl": false
}
},
"error": null,
"createdAt": "2026-11-12T08:33:46.048Z",
"sentAt": null,
"deliveredAt": null,
"readAt": null
}The text body is 1 to 4096 characters long.
Image#
curl -X POST "$INBOX_API/messages" \
-H "Authorization: Bearer $INBOX_KEY" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "+237611111111",
"type": "image",
"image": {
"link": "https://example.com/plan-d-acces.jpg",
"caption": "Directions to the clinic"
}
}'const res = await fetch(`${process.env.INBOX_API}/messages`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INBOX_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "+237611111111",
"type": "image",
"image": {
"link": "https://example.com/plan-d-acces.jpg",
"caption": "Directions to the clinic"
}
}),
});
const data = await res.json();import os
import requests
res = requests.post(
f"{os.environ['INBOX_API']}/messages",
headers={
"Authorization": f"Bearer {os.environ['INBOX_KEY']}",
},
json={
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "+237611111111",
"type": "image",
"image": {
"link": "https://example.com/plan-d-acces.jpg",
"caption": "Directions to the clinic",
},
},
)
print(res.json())<?php
$ch = curl_init(getenv('INBOX_API') . '/messages');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INBOX_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'phoneNumberId' => '4d8e654e-d744-4bf3-8211-9ceffcccfe87',
'to' => '+237611111111',
'type' => 'image',
'image' => [
'link' => 'https://example.com/plan-d-acces.jpg',
'caption' => 'Directions to the clinic',
],
]),
]);
$data = json_decode(curl_exec($ch), true);{
"id": "097f6e1e-64f0-4bdf-8a00-672f6d7b168c",
"direction": "outbound",
"status": "queued",
"type": "image",
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "237611111111",
"waMessageId": null,
"content": {
"type": "image",
"media": {
"link": "https://example.com/plan-d-acces.jpg",
"caption": "Directions to the clinic"
}
},
"error": null,
"createdAt": "2026-11-12T08:33:46.070Z",
"sentAt": null,
"deliveredAt": null,
"readAt": null
}For media (image, video, audio, document), provide either link (a public https:// URL) or id (media already hosted by Meta), never both. caption doesn't exist for audio. Uploading and downloading media through the API is not yet available.
Document#
curl -X POST "$INBOX_API/messages" \
-H "Authorization: Bearer $INBOX_KEY" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "+237611111111",
"type": "document",
"document": {
"link": "https://example.com/ordonnance.pdf",
"filename": "ordonnance.pdf",
"caption": "Your prescription"
}
}'const res = await fetch(`${process.env.INBOX_API}/messages`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INBOX_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "+237611111111",
"type": "document",
"document": {
"link": "https://example.com/ordonnance.pdf",
"filename": "ordonnance.pdf",
"caption": "Your prescription"
}
}),
});
const data = await res.json();import os
import requests
res = requests.post(
f"{os.environ['INBOX_API']}/messages",
headers={
"Authorization": f"Bearer {os.environ['INBOX_KEY']}",
},
json={
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "+237611111111",
"type": "document",
"document": {
"link": "https://example.com/ordonnance.pdf",
"filename": "ordonnance.pdf",
"caption": "Your prescription",
},
},
)
print(res.json())<?php
$ch = curl_init(getenv('INBOX_API') . '/messages');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INBOX_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'phoneNumberId' => '4d8e654e-d744-4bf3-8211-9ceffcccfe87',
'to' => '+237611111111',
'type' => 'document',
'document' => [
'link' => 'https://example.com/ordonnance.pdf',
'filename' => 'ordonnance.pdf',
'caption' => 'Your prescription',
],
]),
]);
$data = json_decode(curl_exec($ch), true);{
"id": "9e09a76b-537b-4d5f-8454-4305ba8748a6",
"direction": "outbound",
"status": "queued",
"type": "document",
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "237611111111",
"waMessageId": null,
"content": {
"type": "document",
"media": {
"link": "https://example.com/ordonnance.pdf",
"caption": "Your prescription",
"filename": "ordonnance.pdf"
}
},
"error": null,
"createdAt": "2026-11-12T08:33:46.098Z",
"sentAt": null,
"deliveredAt": null,
"readAt": null
}Location#
curl -X POST "$INBOX_API/messages" \
-H "Authorization: Bearer $INBOX_KEY" \
-H "Content-Type: application/json" \
-d '{
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "+237611111111",
"type": "location",
"location": {
"latitude": 4.0511,
"longitude": 9.7679,
"name": "Cabinet Dentaire Zokem",
"address": "Akwa, Douala"
}
}'const res = await fetch(`${process.env.INBOX_API}/messages`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INBOX_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "+237611111111",
"type": "location",
"location": {
"latitude": 4.0511,
"longitude": 9.7679,
"name": "Cabinet Dentaire Zokem",
"address": "Akwa, Douala"
}
}),
});
const data = await res.json();import os
import requests
res = requests.post(
f"{os.environ['INBOX_API']}/messages",
headers={
"Authorization": f"Bearer {os.environ['INBOX_KEY']}",
},
json={
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "+237611111111",
"type": "location",
"location": {
"latitude": 4.0511,
"longitude": 9.7679,
"name": "Cabinet Dentaire Zokem",
"address": "Akwa, Douala",
},
},
)
print(res.json())<?php
$ch = curl_init(getenv('INBOX_API') . '/messages');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INBOX_KEY'),
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'phoneNumberId' => '4d8e654e-d744-4bf3-8211-9ceffcccfe87',
'to' => '+237611111111',
'type' => 'location',
'location' => [
'latitude' => 4.0511,
'longitude' => 9.7679,
'name' => 'Cabinet Dentaire Zokem',
'address' => 'Akwa, Douala',
],
]),
]);
$data = json_decode(curl_exec($ch), true);{
"id": "a9691d7a-ee51-4da9-87b2-24cce79a9313",
"direction": "outbound",
"status": "queued",
"type": "location",
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "237611111111",
"waMessageId": null,
"content": {
"type": "location",
"location": {
"latitude": 4.0511,
"longitude": 9.7679,
"name": "Cabinet Dentaire Zokem",
"address": "Akwa, Douala"
}
},
"error": null,
"createdAt": "2026-11-12T08:33:46.131Z",
"sentAt": null,
"deliveredAt": null,
"readAt": null
}Track a message#
curl "$INBOX_API/messages/ID" \
-H "Authorization: Bearer $INBOX_KEY"const res = await fetch(`${process.env.INBOX_API}/messages/ID`, {
headers: {
Authorization: `Bearer ${process.env.INBOX_KEY}`,
},
});
const data = await res.json();import os
import requests
res = requests.get(
f"{os.environ['INBOX_API']}/messages/ID",
headers={
"Authorization": f"Bearer {os.environ['INBOX_KEY']}",
},
)
print(res.json())<?php
$ch = curl_init(getenv('INBOX_API') . '/messages/ID');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INBOX_KEY'),
],
]);
$data = json_decode(curl_exec($ch), true);{
"id": "d5ad1e3e-6ab9-4f7f-8e4e-b17104a7bd00",
"direction": "outbound",
"status": "sent",
"type": "template",
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "237600000000",
"waMessageId": "wamid.HBgMXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"content": {
"type": "template",
"template": {
"name": "appointment_reminder",
"language": "en",
"components": [
{
"type": "body",
"parameters": [
{
"text": "Awa",
"type": "text"
},
{
"text": "November 14 at 10 am",
"type": "text"
}
]
}
]
}
},
"error": null,
"createdAt": "2026-11-12T08:33:45.855Z",
"sentAt": "2026-11-12T08:33:45.891Z",
"deliveredAt": null,
"readAt": null
}error is null as long as everything is fine; on failure, it contains Meta's code and message. To be notified without polling the API, use webhooks (message.sent, message.delivered, message.read, message.failed).
History#
curl "$INBOX_API/messages" \
-H "Authorization: Bearer $INBOX_KEY"const res = await fetch(`${process.env.INBOX_API}/messages`, {
headers: {
Authorization: `Bearer ${process.env.INBOX_KEY}`,
},
});
const data = await res.json();import os
import requests
res = requests.get(
f"{os.environ['INBOX_API']}/messages",
headers={
"Authorization": f"Bearer {os.environ['INBOX_KEY']}",
},
)
print(res.json())<?php
$ch = curl_init(getenv('INBOX_API') . '/messages');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INBOX_KEY'),
],
]);
$data = json_decode(curl_exec($ch), true);{
"data": [
{
"id": "a9691d7a-ee51-4da9-87b2-24cce79a9313",
"direction": "outbound",
"status": "sent",
"type": "location",
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "237611111111",
"waMessageId": "wamid.HBgMXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"content": {
"type": "location",
"location": {
"name": "Cabinet Dentaire Zokem",
"address": "Akwa, Douala",
"latitude": 4.0511,
"longitude": 9.7679
}
},
"error": null,
"createdAt": "2026-11-12T08:33:46.131Z",
"sentAt": "2026-11-12T08:33:46.251Z",
"deliveredAt": null,
"readAt": null
},
{
"id": "9e09a76b-537b-4d5f-8454-4305ba8748a6",
"direction": "outbound",
"status": "sent",
"type": "document",
"phoneNumberId": "4d8e654e-d744-4bf3-8211-9ceffcccfe87",
"to": "237611111111",
"waMessageId": "wamid.HBgMXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
"content": {
"type": "document",
"media": {
"link": "https://example.com/ordonnance.pdf",
"caption": "Your prescription",
"filename": "ordonnance.pdf"
}
},
"error": null,
"createdAt": "2026-11-12T08:33:46.098Z",
"sentAt": "2026-11-12T08:33:46.221Z",
"deliveredAt": null,
"readAt": null
}
],
"nextCursor": "MjAyNi0xMS0xMlQwODozMzo0Ni4wOThafGMwYmU4NDdlLWQyNDQtNGUyOS04N2M1LTAyNTE3ODk1Yzg5OQ"
}History is sorted from newest to oldest and cursor-paginated: as long as nextCursor is not null, pass it in ?cursor= to get the next page. Filters: phoneNumberId, direction (inbound, outbound), status, limit (1 to 100).
Sending errors#
| Code | HTTP | Cause |
|---|---|---|
invalid_message | 422 | Malformed number, empty text, ambiguous media (id and link together), non-https link |
outside_service_window | 422 | Free-form message outside the 24-hour window |
template_not_approved | 422 | The template exists but is not (or no longer) approved |
idempotency_conflict | 422 | Same Idempotency-Key for a different request |
quota_exceeded | 402 | Your plan's monthly quota has been reached (production) |
not_found | 404 | Unknown number, or one from another environment |
{
"error": {
"code": "invalid_message",
"message": "Invalid recipient number: use the international format with a \"+\", e.g. +237600000000",
"requestId": "req_72871759-6841-4dcf-8065-3425c4a44633"
}
}Meta's rejections during delivery (template not found, recipient unreachable…) don't show up in the 202 response: the message moves to failed, with Meta's reason, and you receive message.failed.