Create WhatsApp templates, follow Meta's review, fill variables, media headers and buttons, and send a template to start a conversation.
A template is a pre-approved message. It is the only thing you can send to someone who has not messaged you in the last 24 hours, so every outbound campaign, reminder and one-time code on WhatsApp starts with one. You create the template, Meta reviews it, and once it is APPROVED you can send it as often as you like with different values filled in.
#Create a template
Send POST /v1/whatsapp/templates with the account, a name, a category, a language and the components that make up the message.
curl -X POST https://api.pozy.ai/v1/whatsapp/templates \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "6712a9d4c0e3b58f1a7d2e64",
"name": "pickup_ready",
"category": "UTILITY",
"language": "en",
"components": [
{
"type": "body",
"text": "Hi {{1}}, your order {{2}} is ready for pickup until {{3}}.",
"example": { "body_text": [["Marta", "#4821", "6pm"]] }
},
{ "type": "footer", "text": "Reply STOP to opt out" }
]
}'const res = await fetch("https://api.pozy.ai/v1/whatsapp/templates", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
accountId: "6712a9d4c0e3b58f1a7d2e64",
name: "pickup_ready",
category: "UTILITY",
language: "en",
components: [
{
type: "body",
text: "Hi {{1}}, your order {{2}} is ready for pickup until {{3}}.",
example: { body_text: [["Marta", "#4821", "6pm"]] },
},
{ type: "footer", text: "Reply STOP to opt out" },
],
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.template.id, data.template.status); // "PENDING"import os
import requests
resp = requests.post(
"https://api.pozy.ai/v1/whatsapp/templates",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
json={
"accountId": "6712a9d4c0e3b58f1a7d2e64",
"name": "pickup_ready",
"category": "UTILITY",
"language": "en",
"components": [
{
"type": "body",
"text": "Hi {{1}}, your order {{2}} is ready for pickup until {{3}}.",
"example": {"body_text": [["Marta", "#4821", "6pm"]]},
},
{"type": "footer", "text": "Reply STOP to opt out"},
],
},
timeout=30,
)
resp.raise_for_status()
print(resp.json()["template"]){
"success": true,
"template": {
"id": "781203456789012",
"name": "pickup_ready",
"status": "PENDING",
"category": "UTILITY",
"language": "en"
}
}Rules for the request:
nameuses lowercase letters, digits and underscores and starts with a letter.- Meta stores one template per name and language. The same
namein three languages is three templates, each with its ownid. - Every variable needs an
examplevalue. Reviewers reject templates they cannot read with real values.
#Pick the category
The category decides what Meta charges per delivery and how long Meta keeps trying to deliver. Pick the one that matches the content honestly; Meta reclassifies templates that are filed under the wrong one.
category |
Use it for | Delivery window you can set | Meta's default |
|---|---|---|---|
UTILITY |
Order updates, receipts, reminders, account notices | 30 seconds to 12 hours | 30 days |
MARKETING |
Promotions, offers, newsletters, re-engagement | 12 hours to 30 days | 30 days |
AUTHENTICATION |
One-time passcodes | 30 seconds to 15 minutes | 10 minutes |
Set the window with message_send_ttl_seconds. A message not delivered inside it is dropped rather than delivered hours afterwards. That is right for a passcode and wrong for a promotion. On create you can pass -1 to keep the 30-day default for UTILITY and AUTHENTICATION.
When Meta changes a category it sends whatsapp.template.category_updated twice: once with changeType: "scheduled" as a 24-hour notice, and again with changeType: "applied" when it takes effect. Meta clears any custom delivery window at that point, so read it back afterwards.
#Build the components
A template is a list of components. Only body is required.
| Component | What it holds | Notes |
|---|---|---|
header |
format is text, image, video, gif, document or location |
A text header can carry one variable. A media header needs an example.header_handle for review and real media at send time. |
body |
The main text with variables | add_security_recommendation adds Meta's standard warning on authentication templates. |
footer |
Short plain text | code_expiration_minutes on authentication templates. |
buttons |
Quick replies, links, phone, code and flow buttons | Types: quick_reply, url, phone_number, otp, copy_code, flow, mpm, catalog. |
carousel |
Up to 10 cards, each with its own header, body and buttons | Every card must use the same structure. |
limited_time_offer |
Offer text (16 characters) and an optional expiry | Marketing templates only. |
#Variables
Variables are positional by default: {{1}}, {{2}} and so on. To use names, set parameter_format: "NAMED" and write {{customer_name}} in the text, with the examples in body_text_named_params.
Broadcasts only fill positional variables. Use positional ones if you plan to broadcast the template.
#Buttons
{
"type": "buttons",
"buttons": [
{ "type": "quick_reply", "text": "Change pickup time" },
{ "type": "url", "text": "View order", "url": "https://example.com/orders/{{1}}", "example": ["4821"] },
{ "type": "phone_number", "text": "Call the shop", "phone_number": "+351210000123" }
]
}A url button can end in one variable, filled at send time. A flow button opens a WhatsApp Flow by flow_id or flow_name.
#Wait for approval
Review can take up to 24 hours. Subscribe to whatsapp.template.status_updated instead of polling. It fires whenever Meta finishes a review or re-review and carries the new status and Meta's reason ("NONE" on approval).
status |
Meaning | Can you send it? |
|---|---|---|
PENDING |
In review | No |
APPROVED |
Live | Yes |
REJECTED |
Declined. Fix the content and create it again, or appeal in WhatsApp Manager. | No |
IN_APPEAL |
An appeal is being reviewed | No |
PAUSED |
Meta paused it after poor recipient feedback | No |
DISABLED |
Switched off by Meta | No |
PENDING_DELETION |
Delete requested; removed after 24 hours | No |
To check by hand, list templates with GET /v1/whatsapp/templates and filter by status, name or language. The list is read live from Meta.
Meta keeps a library of pre-approved utility templates. Look one up with GET /v1/whatsapp/template-library, then create it with library_template_name instead of components. It comes back APPROVED immediately. If the library template has URL or phone buttons, send one library_template_button_inputs entry per button, in order, or Meta rejects it.
#Send a template
To open a conversation, call POST /v1/inbox/conversations. The recipient goes in participantId as digits with the country code and no +. templateParams is one flat list in the order the variables appear: header variable first, then body variables.
curl -X POST https://api.pozy.ai/v1/inbox/conversations \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"accountId": "6712a9d4c0e3b58f1a7d2e64",
"participantId": "351912345678",
"templateName": "pickup_ready",
"templateLanguage": "en",
"templateParams": ["Marta", "#4821", "6pm"]
}'const res = await fetch("https://api.pozy.ai/v1/inbox/conversations", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
accountId: "6712a9d4c0e3b58f1a7d2e64",
participantId: "351912345678",
templateName: "pickup_ready",
templateLanguage: "en",
templateParams: ["Marta", "#4821", "6pm"],
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data.data.conversationId, data.data.messageId);resp = requests.post(
"https://api.pozy.ai/v1/inbox/conversations",
headers={"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"},
json={
"accountId": "6712a9d4c0e3b58f1a7d2e64",
"participantId": "351912345678",
"templateName": "pickup_ready",
"templateLanguage": "en",
"templateParams": ["Marta", "#4821", "6pm"],
},
timeout=30,
)
print(resp.json()["data"])Other parts of the template are filled with their own fields on the same call:
| Field | Fills |
|---|---|
headerMedia |
The image, video or document of a media header (type plus a public link or an uploaded media id) |
headerLocation |
A location header (latitude, longitude, optional name and address) |
templateButtonParams |
Button variables by index, with subType url, copy_code or flow |
templateCards |
Per-card values for a carousel (cardIndex, params, headerMedia, buttons) |
templateLanguage must match exactly. en and en_US are two different templates.
Inside an existing conversation, send a template with POST /v1/inbox/conversations/{conversationId}/messages. To send the same template to many people with per-person values, use a broadcast.
#Edit or delete a template
Update by name with PATCH /v1/whatsapp/templates/{templateName} or by Meta id with PATCH /v1/whatsapp/templates/id/{templateId}. Put language in the body to say which variant you mean. A name that exists in several languages without language returns 409 with code ambiguous_template.
- Changing
componentssends the template back to review and returnsstatus: "PENDING". - Changing only
message_send_ttl_secondskeeps an approved template approved.-1is not allowed here. - Meta allows one edit per 24 hours and 10 per 30 days, and only from
APPROVED,REJECTEDorPAUSED.
Deleting moves a template to PENDING_DELETION for 24 hours before Meta removes it.
#Common errors
| Error | Cause | Fix |
|---|---|---|
400 on create |
Name format, missing example or a category outside the three values | Check the rules above and resend |
400 with param: message_send_ttl_seconds |
Window outside the range for the category | Use a value from the category table |
Meta code 132001, template not found |
Name or language code does not match | Use the exact name and language the template was created with |
Meta code 132000, parameter count |
Fewer or more templateParams than variables |
Send one value per variable, in order |
REJECTED status |
Review failed; not an API error | Read reason on whatsapp.template.status_updated, fix the text, create it again |
#Endpoints
| Endpoint | What it does |
|---|---|
GET /v1/whatsapp/templates |
List templates |
POST /v1/whatsapp/templates |
Create template |
GET /v1/whatsapp/templates/{templateName} |
Get template |
PATCH /v1/whatsapp/templates/{templateName} |
Update template |
DELETE /v1/whatsapp/templates/{templateName} |
Delete template |
GET /v1/whatsapp/templates/id/{templateId} |
Get template by id |
PATCH /v1/whatsapp/templates/id/{templateId} |
Update template by id |
DELETE /v1/whatsapp/templates/id/{templateId} |
Delete template by id |
GET /v1/whatsapp/template-library |
Look up a library template |
POST /v1/inbox/conversations |
Create conversation |
POST /v1/inbox/conversations/{conversationId}/messages |
Send message |
#Webhook events
| Event | When it fires |
|---|---|
whatsapp.template.status_updated |
WhatsApp template status updated. |
whatsapp.template.category_updated |
WhatsApp template category updated. |