How to Create and Manage Templates in WhatsApp Business API: Complete Guide
End-to-end guide covering the full template lifecycle in WhatsApp Business API: creation via API, JSON structure, variables, categories, Meta approval process, and best practices for first-time approval.
Templates are the backbone of any serious WhatsApp Business API integration. They're required to initiate conversations with users (when the 24-hour window is closed), and any proactive message — billing, order notification, appointment reminder — must go through them.
This guide covers the complete lifecycle: creating, structuring, submitting for approval, and managing templates, with production-ready code examples.
What Are Templates and When to Use Them
Templates (officially Message Templates) are Meta-pre-approved messages you can send outside the 24-hour customer service window. When an active conversation exists (user replied in the last 24h), you can send free-form text. Outside that window, only approved templates work.
Common use cases:
- Order notifications — confirmation, shipment, delivery
- Appointment reminders — consultation confirmation, follow-up
- Billing and invoices — payment due, invoice available
- OTP authentication — two-factor verification codes
- Marketing and promotions — offers, coupons, launches (Marketing category)
Template Categories
Meta classifies all templates into three categories. The category affects the cost per conversation and content rules:
Warning: Meta can automatically reclassify your template if the content doesn't match the declared category. Declare correctly from the start.
Template JSON Structure
Every template is composed of components. Available components are:
{
"name": "order_update",
"language": "en_US",
"category": "UTILITY",
"components": [
{
"type": "HEADER",
"format": "TEXT",
"text": "Order #{{1}} Updated"
},
{
"type": "BODY",
"text": "Hi {{1}}! Your order #{{2}} has been {{3}} and will arrive by {{4}}. Track it in real time via the link below."
},
{
"type": "FOOTER",
"text": "Reply STOP to unsubscribe from notifications."
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "URL",
"text": "Track Order",
"url": "https://yoursite.com/track/{{1}}"
}
]
}
]
}
HEADER Component
The header is optional but significantly increases open rates. It can be:
TEXT— Plain text (supports one variable{{1}})IMAGE— Public image URL (JPEG/PNG, up to 5MB)VIDEO— Public video URL (MP4, up to 16MB)DOCUMENT— Public PDF URL
For media headers, you don't provide the URL in the template itself — it's passed dynamically when sending the message.
BODY Component
The body is the only required component. 1024-character limit. Supports:
- Dynamic variables:
{{1}},{{2}},{{3}}... (indexed starting at 1) - Basic formatting:
*bold*,_italic_,~strikethrough~,``code``
FOOTER Component
Static text (no variables), maximum 60 characters. Ideal for opt-out notices.
BUTTONS Component
Up to 10 buttons per template. Available types:
Variable Rules
Variables follow the {{N}} pattern where N is a sequential integer starting at 1.
Critical rules:
- No index gaps — If you use
{{1}}and{{3}},{{2}}must also exist - Examples required — Every variable needs an example in the
examplefield - No variables in footers — Footers are always static
- URL variables — Only the suffix can be a variable:
https://yoursite.com/{{1}}✅ |https://{{1}}.com❌
Correct example with example field:
{
"type": "BODY",
"text": "Hi {{1}}! Your order #{{2}} has been confirmed.",
"example": {
"body_text": [["John Smith", "US-456789"]]
}
}
Creating a Template via API
Endpoint
POST https://graph.facebook.com/v21.0/{WABA_ID}/message_templates
Authentication
Use a System User Token with whatsapp_business_management permission. The temporary test token from the dashboard is not suitable for production.
curl -X POST \
"https://graph.facebook.com/v21.0/YOUR_WABA_ID/message_templates" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "appointment_confirmation",
"language": "en_US",
"category": "UTILITY",
"components": [
{
"type": "BODY",
"text": "Hi {{1}}! Your appointment for {{2}} at {{3}} has been confirmed. To cancel, reply CANCEL.",
"example": {
"body_text": [["Maria Smith", "medical consultation", "2:30 PM"]]
}
},
{
"type": "FOOTER",
"text": "Reply STOP to unsubscribe."
}
]
}'
Success Response
{
"id": "123456789012345",
"status": "PENDING",
"category": "UTILITY"
}
The initial status is always PENDING. After Meta's review, it changes to APPROVED or REJECTED.
Practical Examples
Template with Image Header
{
"name": "weekly_promotion",
"language": "en_US",
"category": "MARKETING",
"components": [
{
"type": "HEADER",
"format": "IMAGE",
"example": {
"header_handle": ["https://yoursite.com/promo-banner.jpg"]
}
},
{
"type": "BODY",
"text": "🔥 *Exclusive offer for {{1}}!*\n\nToday only: {{2}}% off storewide. Use code *{{3}}* at checkout.",
"example": {
"body_text": [["you", "30", "FLASH30"]]
}
},
{
"type": "FOOTER",
"text": "Valid today only."
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "URL",
"text": "Shop the Sale",
"url": "https://store.yoursite.com/promo/{{1}}",
"example": ["flash-sale"]
},
{
"type": "QUICK_REPLY",
"text": "Not interested"
}
]
}
]
}
Template with Quick Reply and CTA Buttons
{
"name": "satisfaction_survey",
"language": "en_US",
"category": "UTILITY",
"components": [
{
"type": "BODY",
"text": "Hi {{1}}! How was your experience with our support on {{2}}?\n\nYour feedback helps us improve. 🙏",
"example": {
"body_text": [["Carlos", "today"]]
}
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "QUICK_REPLY",
"text": "😊 Great"
},
{
"type": "QUICK_REPLY",
"text": "😐 Average"
},
{
"type": "QUICK_REPLY",
"text": "😞 Poor"
}
]
}
]
}
OTP Authentication Template
{
"name": "verification_code",
"language": "en_US",
"category": "AUTHENTICATION",
"components": [
{
"type": "BODY",
"text": "Your verification code is *{{1}}*. Valid for 10 minutes. Do not share it with anyone.",
"example": {
"body_text": [["483920"]]
}
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "COPY_CODE",
"example": "483920"
}
]
}
]
}
For authentication templates with
COPY_CODEbuttons, Meta requires the body to follow a specific format. The structure above guarantees approval.
Meta's Approval Process
After submitting, the template goes through automated (and eventually human) review:
- PENDING — Awaiting review (0–24h normally, can take up to 72h)
- APPROVED — Approved and ready to use
- REJECTED — Rejected with reason (
rejection_reason) - PAUSED — Paused due to low quality (high user block rate)
- DISABLED — Permanently disabled for serious violations
Average approval times:
- Simple
AUTHENTICATIONandUTILITYtemplates: usually < 1 hour MARKETINGtemplates: 2–24 hours- Templates with media or URL buttons: may take longer
Querying Status via API
curl "https://graph.facebook.com/v21.0/YOUR_WABA_ID/message_templates?name=verification_code" \
-H "Authorization: Bearer YOUR_TOKEN"
{
"data": [
{
"name": "verification_code",
"status": "APPROVED",
"id": "123456789012345",
"language": "en_US",
"category": "AUTHENTICATION"
}
]
}
Editing Existing Templates
You can edit the content of an approved template, but you cannot change the name, language, or category. Editing puts the template back to PENDING.
curl -X POST \
"https://graph.facebook.com/v21.0/TEMPLATE_ID" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"components": [
{
"type": "BODY",
"text": "Hi {{1}}! Your appointment for {{2}} at {{3}} is confirmed. Please arrive 10 minutes early. To cancel, reply CANCEL.",
"example": {
"body_text": [["Ana Smith", "physiotherapy", "9:00 AM"]]
}
}
]
}'
Deleting Templates
Templates can only be deleted if they are not being used in active conversations. After deletion, the name is blocked for 30 days.
curl -X DELETE \
"https://graph.facebook.com/v21.0/YOUR_WABA_ID/message_templates?name=verification_code&hsm_id=TEMPLATE_ID" \
-H "Authorization: Bearer YOUR_TOKEN"
Best Practices for Fast Approval
What Meta Rejects
These patterns cause immediate rejection:
- Variables without examples — Never submit a template without the
examplefield filled in - Ambiguous content —
"Your update has arrived"without context → rejected. Be specific:"Your order #{{1}} has shipped" - Suspicious or shortened URLs —
bit.ly/xyz→ rejected. Always use your real domain - Mixed languages — Template declared as
en_USwith chunks in another language → inconsistency - Promotions disguised as utility —
UTILITYtemplate with clearly promotional language → reclassified and potentially rejected - Explicit sensitive data — Don't ask for SSN, passwords, or card numbers in the body
- Excessive urgency —
"LAST DAY!!!","HURRY!!!"— raises spam flags
What Guarantees Approval
- Realistic examples — Use example data that actually makes sense for the template
- Clear opt-out — For marketing, include cancellation instructions in the footer
- Specific CTA —
"View my order"is better than"Click here" - Correct category — Don't try to save costs by putting marketing as utility
- Natural text — Write like a human. Avoid text that sounds bot-generated or spammy
Monitoring Quality Post-Approval
After approval, Meta monitors whether users are blocking messages sent via your template. If the block rate exceeds the threshold, the template moves to PAUSED and may become DISABLED.
To maintain quality:
- Send only to users who opted in to receive messages
- Don't overwhelm with daily marketing messages
- Personalize variables — generic messages have higher block rates
Complete Node.js Flow
const axios = require('axios')
const WABA_ID = process.env.WHATSAPP_WABA_ID
const TOKEN = process.env.WHATSAPP_TOKEN
async function createTemplate(templateData) {
try {
const response = await axios.post(
`https://graph.facebook.com/v21.0/${WABA_ID}/message_templates`,
templateData,
{
headers: {
Authorization: `Bearer ${TOKEN}`,
'Content-Type': 'application/json',
},
}
)
console.log('Template created:', response.data)
return response.data
} catch (error) {
const errorData = error.response?.data?.error
console.error('Error creating template:', errorData)
throw error
}
}
async function getTemplateStatus(templateName) {
const response = await axios.get(
`https://graph.facebook.com/v21.0/${WABA_ID}/message_templates`,
{
params: { name: templateName },
headers: { Authorization: `Bearer ${TOKEN}` },
}
)
return response.data.data[0]
}
// Usage example
const template = {
name: 'delivery_notification',
language: 'en_US',
category: 'UTILITY',
components: [
{
type: 'BODY',
text: 'Your order #{{1}} was delivered on {{2}}. Rate your experience at: {{3}}',
example: {
body_text: [['US-12345', 'Feb 15, 2026', 'https://rate.yoursite.com']],
},
},
],
}
createTemplate(template).then(async (created) => {
// Wait for approval (simple polling)
let status = 'PENDING'
while (status === 'PENDING') {
await new Promise((r) => setTimeout(r, 30000)) // 30s
const info = await getTemplateStatus(template.name)
status = info.status
console.log(`Status: ${status}`)
}
console.log(`Template ${status === 'APPROVED' ? '✅ approved!' : '❌ rejected.'}`)
})
Summary
Templates are the entry point for proactive communication on WhatsApp. Mastering their creation and management is fundamental for any production integration.
The most critical points:
- Always fill in the
examplefield — it's the most common rejection reason - Use the correct category — Meta reclassifies and may penalize
- Monitor quality — paused templates affect the entire account
- Names and languages are immutable — plan carefully before creating
With these fundamentals, you have everything you need to create templates that pass on the first try and work reliably in production.