Templates
Pre-approved messages for WhatsApp Business outside the 24-hour window
Templates are pre-approved message formats required for contacting users outside the 24-hour conversation window. They must be submitted to Meta for approval before use.
Why Templates?
- Required by WhatsApp - Only way to message users outside 24-hour window
- Consistency - Ensure uniform messaging across your team
- Compliance - Meta reviews content for quality and policy compliance
- Analytics - Track performance by template
Quick Start: Send a Template Message
If you already have an approved template, here's how to send it with variables:
const message = await zavu.messages.send({
to: "+56912345678",
messageType: "template",
content: {
templateId: "tpl_abc123",
templateVariables: {
"1": "John",
"2": "ORD-12345",
"3": "January 20, 2025"
}
}
});
How Variables Work
A template can use one of two placeholder formats, and you pass values the same way for both:
- Positional:
{{1}},{{2}},{{3}}— keyed by position. - Named:
{{customer_name}},{{order_id}}— keyed by name.
CodeTemplate body: "Hi {{1}}, your order #{{2}} is confirmed. Delivery: {{3}}." ^ ^ ^ | | | Variables: "1": "John" "2": "ORD-12345" "3": "January 20, 2025"
In both cases you provide the values through templateVariables. The keys match
the placeholders in the template body — numbers for positional templates, names
for named templates:
JSON// Positional template "templateVariables": { "1": "John", "2": "ORD-12345" } // Named template "templateVariables": { "customer_name": "John", "order_id": "ORD-12345" }
Named vs Positional — which does my template use?
You normally don't need to care: Zavu detects the format per template and builds the correct Meta payload. The distinction matters only when you decide what keys to use in templateVariables:
| How the template was created | Format at Meta | Keys to use |
|---|---|---|
Created in Zavu (POST /v1/templates) | Positional — Zavu converts {{name}} → {{1}} at submit time | Position numbers ("1", "2") or the original names |
| Imported from an existing WhatsApp Business Account | Whatever Meta has on file — named or positional | Match the placeholders in the template body |
Text Header Variables
A template can have a text header with its own variable, e.g. Invitación Boda {{novios}}. The header variable is independent from the body variables, but you pass its value through the same templateVariables object, keyed by the header placeholder's name (or {{1}} for a positional header):
// Template header: "Invitación Boda {{novios}}"
// Template body: "Estimado/a {{nombre}}, te corresponden {{pases}} boletos."
await zavu.messages.send({
to: "+5212345678901",
messageType: "template",
content: {
templateId: "tpl_wedding",
templateVariables: {
novios: "Jorge y Laura", // header variable
nombre: "Nestor Alvares", // body variables
pases: "4"
}
}
});
Media headers
When a template's header is an image, video or document, Zavu sends the file stored on the template. To send a different file in one message (for example a per-customer PDF), pass templateHeaderMediaUrl. For a document header you can also set the file name the recipient sees with templateHeaderFilename.
await zavu.messages.send({
to: "+56912345678",
messageType: "template",
content: {
templateId: "tmpl_abc123", // header type: document
templateVariables: { "1": "Maria", "2": "INV-1042" },
templateHeaderMediaUrl: "https://files.example.com/invoices/INV-1042.pdf?sig=abc123",
templateHeaderFilename: "invoice-1042.pdf",
},
});
templateHeaderMediaUrlmust be an HTTPS URL that WhatsApp can fetch when the message is sent. It replaces the template's stored file for this message only.templateHeaderFilenameis only valid when the template's header isdocument. On its own it names the template's stored file.- Both fields are refused with
400 invalid_request(details.fieldnames the field) when the template's header does not match, before anything is sent or charged. - Without these fields, the file stored on the template is sent, as before.
Full Workflow
Templates follow a 3-step process: Create -> Submit for Approval -> Send.
Step 1: Create a Template
const template = await zavu.templates.create({
name: "order_confirmation",
language: "en",
body: "Hi {{1}}, your order #{{2}} has been confirmed! Estimated delivery: {{3}}.",
whatsappCategory: "UTILITY",
variables: ["customer_name", "order_id", "delivery_date"]
});
console.log(template.id); // "tpl_abc123"
console.log(template.status); // "draft"
The variables array is optional and purely for documentation - it helps you remember what each position represents.
Step 2: Submit for Approval
Templates start in draft status. You need to explicitly submit them to Meta for review:
const submitted = await zavu.templates.submit("tpl_abc123", {
senderId: "sender_12345",
category: "UTILITY"
});
console.log(submitted.status); // "pending"
Step 3: Check Approval Status
terminalcurl https://api.zavu.dev/v1/templates/tpl_abc123 \ -H "Authorization: Bearer zv_live_xxx"
JSON{ "id": "tpl_abc123", "name": "order_confirmation", "status": "approved", "category": "UTILITY", "body": "Hi {{1}}, your order #{{2}} has been confirmed! Estimated delivery: {{3}}.", "variables": ["customer_name", "order_id", "delivery_date"] }
| Status | Description |
|---|---|
draft | Created but not yet submitted to Meta |
pending | Submitted, awaiting Meta review |
approved | Ready to use |
rejected | Not approved - check rejection reason |
Step 4: Send the Template
Once approved, send it using the positional variables (see Quick Start above).
Template Categories
| Category | Use Case | Approval Speed |
|---|---|---|
UTILITY | Order updates, shipping, account alerts | Fast (hours) |
MARKETING | Promotions, offers, newsletters | Slower (days) |
AUTHENTICATION | OTPs, verification codes | Fast (hours) |
Templates with Buttons
Add call-to-action or quick reply buttons:
terminalcurl -X POST https://api.zavu.dev/v1/templates \ -H "Authorization: Bearer zv_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "name": "order_delivered", "language": "en", "body": "Hi {{1}}, your order has been delivered!", "whatsappCategory": "UTILITY", "variables": ["customer_name"], "buttons": [ { "type": "url", "text": "Track Order", "url": "https://example.com/track/{{1}}" }, { "type": "quick_reply", "text": "Rate Delivery" } ] }'
Button Types
| Type | Description | Max |
|---|---|---|
quick_reply | User response button | 3 buttons |
url | Opens a URL (can include variables) | - |
phone | Initiates a phone call | - |
otp | Copy code / one-tap autofill for authentication | - |
Sending Templates with Dynamic URL Buttons
When a template has a URL button with a {{1}} placeholder, pass the substitution under templateButtonVariables keyed by the button's position in the template's buttons array.
templateVariables is for body placeholders. templateButtonVariables is for buttons. They use different keys:
templateVariableskeys → position of the placeholder in the body text ("1","2", ...).templateButtonVariableskeys → index of the button in thebuttonsarray ("0","1","2").
Example. Suppose your approved template looks like this:
JSON{ "id": "tpl_discipline_report", "body": "Hi {{1}}, your child received a {{2}}.", "buttons": [ { "type": "url", "text": "View report", "url": "https://reporte.link/{{1}}" } ] }
Send it like this:
await zavu.messages.send({
to: "+56912345678",
messageType: "template",
content: {
templateId: "tpl_discipline_report",
templateVariables: {
"1": "Marco Ordaz",
"2": "report for inappropriate behavior"
},
templateButtonVariables: {
"0": "abc-report-token"
}
}
});
The recipient receives a button that opens https://reporte.link/abc-report-token.
Multiple buttons
For templates with several buttons, supply one entry per dynamic button. Static URL buttons (no {{1}}) and quick_reply buttons are not included in templateButtonVariables.
JSON{ "buttons": [ { "type": "url", "text": "View order", "url": "https://shop.example.com/orders/{{1}}" }, { "type": "quick_reply", "text": "Track" }, { "type": "url", "text": "Help", "url": "https://help.example.com/" } ] }
JSON{ "templateButtonVariables": { "0": "ORD-12345" } }
Index 1 (quick reply) and index 2 (static URL) are skipped.
Validation rules
| Rule | Behavior |
|---|---|
URL has {{1}} and templateButtonVariables[index] provided | ✅ delivered with substitution |
URL has {{1}} and templateButtonVariables[index] missing | ❌ 400 invalid_request: Missing URL button parameter at index N |
URL has {{name}}, {{ 1 }}, {{token}}, etc. | ❌ 400 invalid_request: ... WhatsApp treats it as literal text. Recreate the template with "{{1}}" and resubmit it for approval. |
URL has more than one placeholder (e.g. /{{1}}/{{2}}) | ❌ 400 invalid_request: at most one positional variable |
| URL has no placeholder (static link) | ✅ ignored — do not include the index in templateButtonVariables |
Authentication Templates (OTP)
For verification codes, use AUTHENTICATION category with OTP buttons:
terminalcurl -X POST https://api.zavu.dev/v1/templates \ -H "Authorization: Bearer zv_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "name": "login_otp", "language": "en", "body": "{{1}} is your verification code.", "whatsappCategory": "AUTHENTICATION", "variables": ["otp_code"], "buttons": [ { "type": "otp", "text": "Copy Code", "otpType": "COPY_CODE" } ], "addSecurityRecommendation": true, "codeExpirationMinutes": 5 }'
Send the OTP:
terminalcurl -X POST https://api.zavu.dev/v1/messages \ -H "Authorization: Bearer zv_live_xxx" \ -H "Content-Type: application/json" \ -d '{ "to": "+56912345678", "messageType": "template", "content": { "templateId": "tpl_otp123", "templateVariables": { "1": "482910" } } }'
See the OTP Templates guide for more details.
Managing Templates
List Templates
terminalcurl https://api.zavu.dev/v1/templates \ -H "Authorization: Bearer zv_live_xxx"
Delete Template
terminalcurl -X DELETE https://api.zavu.dev/v1/templates/tpl_abc123 \ -H "Authorization: Bearer zv_live_xxx"
Best Practices
Naming Conventions
Use descriptive, lowercase names with underscores:
Codeorder_confirmation shipping_update password_reset appointment_reminder promotional_offer
Content Guidelines
Do:
- Use clear, concise language
- Include dynamic variables for personalization
- Provide value to the recipient
- Test with real data before production use
Don't:
- Use placeholder text like "[insert name]"
- Include excessive capitalization or punctuation
- Send promotional content without consent
- Use misleading or clickbait content
Common Rejection Reasons
| Reason | Solution |
|---|---|
| Variable syntax error | Use {{1}}, {{2}} positional format |
| Category mismatch | Choose correct category for content |
| Promotional in UTILITY | Use MARKETING category for promotions |
| Missing opt-out | Include unsubscribe option for marketing |
| Policy violation | Review Meta's commerce and messaging policies |
Testing Templates
Test with your own phone number before sending to customers:
terminalcurl -X POST https://api.zavu.dev/v1/messages \ -H "Authorization: Bearer zv_test_xxx" \ -H "Content-Type: application/json" \ -d '{ "to": "+your_phone_number", "messageType": "template", "content": { "templateId": "tpl_abc123", "templateVariables": { "1": "Test User", "2": "TEST-001", "3": "Tomorrow" } } }'