Guides
Templates
Submit templates for Meta review, track their status, sync them, and use them in your sends.
Updated November 12, 2026View as Markdown
A template is a message model pre-approved by Meta. It is the only way to message someone who hasn't written to you in the last 24 hours. Templates belong to a WhatsApp Business account (WABA): all the numbers in that account share the same templates.
The lifecycle#
You submit ──► pending ──► approved (usable)
└──► rejected (reason in rejectionReason)
paused / disabled (insufficient quality: decided by Meta)The status is Meta's, in lowercase. Only approved can be sent (usable: true).
Create a template#
curl -X POST "$INBOX_API/waba-connections/WABA_CONNECTION_ID/templates" \
-H "Authorization: Bearer $INBOX_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "appointment_reminder",
"language": "en",
"category": "UTILITY",
"components": [
{
"type": "BODY",
"text": "Hello {{1}}, your appointment is confirmed for {{2}}.",
"example": {
"body_text": [
[
"Awa",
"November 14 at 10 am"
]
]
}
}
]
}'const res = await fetch(`${process.env.INBOX_API}/waba-connections/WABA_CONNECTION_ID/templates`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INBOX_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "appointment_reminder",
"language": "en",
"category": "UTILITY",
"components": [
{
"type": "BODY",
"text": "Hello {{1}}, your appointment is confirmed for {{2}}.",
"example": {
"body_text": [
[
"Awa",
"November 14 at 10 am"
]
]
}
}
]
}),
});
const data = await res.json();import os
import requests
res = requests.post(
f"{os.environ['INBOX_API']}/waba-connections/WABA_CONNECTION_ID/templates",
headers={
"Authorization": f"Bearer {os.environ['INBOX_KEY']}",
},
json={
"name": "appointment_reminder",
"language": "en",
"category": "UTILITY",
"components": [
{
"type": "BODY",
"text": "Hello {{1}}, your appointment is confirmed for {{2}}.",
"example": {
"body_text": [
[
"Awa",
"November 14 at 10 am",
],
],
},
},
],
},
)
print(res.json())<?php
$ch = curl_init(getenv('INBOX_API') . '/waba-connections/WABA_CONNECTION_ID/templates');
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([
'name' => 'appointment_reminder',
'language' => 'en',
'category' => 'UTILITY',
'components' => [
[
'type' => 'BODY',
'text' => 'Hello {{1}}, your appointment is confirmed for {{2}}.',
'example' => [
'body_text' => [
[
'Awa',
'November 14 at 10 am',
],
],
],
],
],
]),
]);
$data = json_decode(curl_exec($ch), true);{
"id": "ff532b8f-2956-4f04-851f-db09d5eefbd4",
"name": "appointment_reminder",
"language": "en",
"category": "UTILITY",
"status": "pending",
"usable": false,
"rejectionReason": null,
"components": [
{
"type": "BODY",
"text": "Hello {{1}}, your appointment is confirmed for {{2}}.",
"example": {
"body_text": [
[
"Awa",
"November 14 at 10 am"
]
]
}
}
],
"updatedAt": "2026-11-12T08:33:45.595Z"
}| Rule | Detail |
|---|---|
name | Lowercase letters, digits, and underscores: appointment_reminder. |
language | fr, en, en_US… The same name can exist in several languages. |
category | MARKETING, UTILITY, or AUTHENTICATION. Meta decides which category applies, and the pricing that follows from it. |
components | In Meta's format. The BODY component is required; HEADER, FOOTER, and BUTTONS are optional. Variables are written with numbered double curly braces, and the example field provides a sample value for each variable. |
The API validates the structure of the draft (name, language, category, components, buttons) before calling Meta, and responds with 400 validation_error and details if something is wrong. Meta then applies its own content rules: a rejection arrives later, as a rejected status.
Components#
Each component is an object in components. The limits below are the ones Meta announces; Meta has the final say.
| Component | Content | Limits |
|---|---|---|
HEADER | format: TEXT, IMAGE, VIDEO, DOCUMENT, or LOCATION. | Text: 60 characters, at most one variable (example.header_text). Media: example.header_handle with a sample file. |
BODY | text with variables {{1}}, {{2}}… | 1,024 characters. Consecutive numbers starting at 1, and one example per variable (example.body_text). |
FOOTER | text. | 60 characters, no variables. |
BUTTONS | A buttons list. | At most 10 buttons. Text of 25 characters. |
Button types:
type | Fields | Rules |
|---|---|---|
QUICK_REPLY | text | Quick replies form a single block, at the start or at the end of the list. |
URL | text, url, example | At most 2. https:// is required. One {{1}} variable is allowed, at the end of the URL, with a full-URL example. |
PHONE_NUMBER | text, phone_number | At most 1, with a country code (+237…). |
COPY_CODE | example | At most 1. Example of 1 to 15 letters or digits. |
{
"name": "christmas_promo",
"language": "en",
"category": "MARKETING",
"components": [
{ "type": "HEADER", "format": "IMAGE", "example": { "header_handle": ["https://example.com/poster.jpg"] } },
{ "type": "BODY", "text": "Hello {{1}}, enjoy {{2}} storewide!", "example": { "body_text": [["Awa", "20% off"]] } },
{ "type": "FOOTER", "text": "Offer valid until 12/31" },
{ "type": "BUTTONS", "buttons": [
{ "type": "QUICK_REPLY", "text": "Interested" },
{ "type": "URL", "text": "See the offer", "url": "https://example.com/offer/{{1}}", "example": ["https://example.com/offer/42"] },
{ "type": "COPY_CODE", "example": "XMAS25" }
] }
]
}Authentication templates#
For AUTHENTICATION, Meta provides the message text itself: no free text, no header. The BODY only carries add_security_recommendation, the FOOTER only code_expiration_minutes (1 to 90), and an OTP button is required:
[
{ "type": "BODY", "add_security_recommendation": true },
{ "type": "FOOTER", "code_expiration_minutes": 10 },
{ "type": "BUTTONS", "buttons": [{ "type": "OTP", "otp_type": "COPY_CODE", "text": "Copy code" }] }
]Track the review#
You have two options:
- Webhooks: the
template.status_updatedevent notifies you as soon as Meta rules on a template. - Sync, a repair path rather than a polling loop:
curl -X POST "$INBOX_API/waba-connections/WABA_CONNECTION_ID/templates/sync" \
-H "Authorization: Bearer $INBOX_KEY"const res = await fetch(`${process.env.INBOX_API}/waba-connections/WABA_CONNECTION_ID/templates/sync`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INBOX_KEY}`,
},
});
const data = await res.json();import os
import requests
res = requests.post(
f"{os.environ['INBOX_API']}/waba-connections/WABA_CONNECTION_ID/templates/sync",
headers={
"Authorization": f"Bearer {os.environ['INBOX_KEY']}",
},
)
print(res.json())<?php
$ch = curl_init(getenv('INBOX_API') . '/waba-connections/WABA_CONNECTION_ID/templates/sync');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INBOX_KEY'),
],
]);
$data = json_decode(curl_exec($ch), true);[
{
"id": "ff532b8f-2956-4f04-851f-db09d5eefbd4",
"name": "appointment_reminder",
"language": "en",
"category": "UTILITY",
"status": "approved",
"usable": true,
"rejectionReason": null,
"components": [
{
"text": "Hello {{1}}, your appointment is confirmed for {{2}}.",
"type": "BODY"
}
],
"updatedAt": "2026-11-12T08:33:45.640Z"
}
]Sync aligns the database with Meta: statuses are updated, and templates created elsewhere (directly with Meta, for example) are added. Status changes also trigger template.status_updated.
curl "$INBOX_API/waba-connections/WABA_CONNECTION_ID/templates" \
-H "Authorization: Bearer $INBOX_KEY"const res = await fetch(`${process.env.INBOX_API}/waba-connections/WABA_CONNECTION_ID/templates`, {
headers: {
Authorization: `Bearer ${process.env.INBOX_KEY}`,
},
});
const data = await res.json();import os
import requests
res = requests.get(
f"{os.environ['INBOX_API']}/waba-connections/WABA_CONNECTION_ID/templates",
headers={
"Authorization": f"Bearer {os.environ['INBOX_KEY']}",
},
)
print(res.json())<?php
$ch = curl_init(getenv('INBOX_API') . '/waba-connections/WABA_CONNECTION_ID/templates');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INBOX_KEY'),
],
]);
$data = json_decode(curl_exec($ch), true);[
{
"id": "ff532b8f-2956-4f04-851f-db09d5eefbd4",
"name": "appointment_reminder",
"language": "en",
"category": "UTILITY",
"status": "approved",
"usable": true,
"rejectionReason": null,
"components": [
{
"text": "Hello {{1}}, your appointment is confirmed for {{2}}.",
"type": "BODY"
}
],
"updatedAt": "2026-11-12T08:33:45.640Z"
}
]Send a template#
Reference it by name and language. Variables are passed in components, in the order of their numbers:
{
"phoneNumberId": "…",
"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" }
] }
]
}
}If the template exists in Bref Point Inbox but is not approved, the send is rejected immediately, with the status and reason:
{ "error": { "code": "template_not_approved",
"message": "Template “appointment_reminder” (en) is “rejected”: INVALID_FORMAT. Only an approved template can be sent" } }A template unknown to Bref Point Inbox (created directly with Meta and not yet synced) is not blocked upfront: Meta decides, and the message goes to failed if the template doesn't exist.
Delete#
curl -X DELETE "$INBOX_API/waba-connections/WABA_CONNECTION_ID/templates/NAME" \
-H "Authorization: Bearer $INBOX_KEY"const res = await fetch(`${process.env.INBOX_API}/waba-connections/WABA_CONNECTION_ID/templates/NAME`, {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.INBOX_KEY}`,
},
});
const data = res.status;import os
import requests
res = requests.delete(
f"{os.environ['INBOX_API']}/waba-connections/WABA_CONNECTION_ID/templates/NAME",
headers={
"Authorization": f"Bearer {os.environ['INBOX_KEY']}",
},
)
print(res.status_code)<?php
$ch = curl_init(getenv('INBOX_API') . '/waba-connections/WABA_CONNECTION_ID/templates/NAME');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => 'DELETE',
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('INBOX_KEY'),
],
]);
curl_exec($ch);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE);(no content)Deletion removes the template in all its languages, first with Meta, then in Bref Point Inbox.