Skip to content

Template Approval Process

Understand how WhatsApp templates are reviewed and approved by Meta

View .md

All WhatsApp templates must be approved by Meta before they can be used to send messages. This guide explains the approval process, common rejection reasons, and how to track template status.

Approval Workflow

stateDiagram-v2
    [*] --> draft: Create template
    draft --> pending: Submit for approval
    pending --> approved: Meta approves
    pending --> rejected: Meta rejects
    rejected --> draft: Edit and fix
    approved --> [*]: Ready to use

Submitting for Approval

Via Dashboard

  1. Go to Senders > Select sender > Templates tab
  2. Find the template with "Draft" status
  3. Click the ... menu and select Submit for Approval
  4. The status will change to "Pending"

Via API

// Submit a draft template for approval await zavu.templates.submit({ templateId: 'tmpl_abc123', });

Sample Values for Variables

Meta reviews a template against a sample value for each body variable. Send them as variableExamples when you submit: key a named variable ({{customer_name}}) by its name, and a positional one ({{1}}, {{2}}) as var1, var2.

terminal
curl -X POST https://api.zavu.dev/v1/templates/tmpl_abc123/submit \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "senderId": "sender_abc123", "category": "UTILITY", "variableExamples": { "var1": "Maria", "var2": "ORD-12345" } }'

Any variable you leave out gets a generic placeholder such as Sample var1, which Meta reviews less favourably. Send a realistic value for every variable. The SDKs do not include this field yet, so use the REST endpoint for it.

Template Status

StatusDescriptionCan Send?
draftNot yet submitted to MetaNo
pendingSubmitted, awaiting Meta reviewNo
approvedApproved by MetaYes
rejectedRejected by MetaNo

Approval Timeline

  • Typical review time: 24-48 hours
  • Complex templates: Up to 72 hours
  • Peak periods: May take longer during high volume

Tracking Status Changes

Webhooks

Configure webhooks to receive real-time notifications when template status changes:

JSON
{ "id": "evt_1702000000000_abc123", "type": "template.status_changed", "timestamp": 1702000000000, "senderId": "sender_xyz", "projectId": "proj_abc", "data": { "templateId": "tmpl_123", "name": "order_confirmation", "previousStatus": "pending", "currentStatus": "approved", "rejectionReason": null } }

Enable the webhook event:

  1. Go to Senders > Select sender > Webhooks tab
  2. Add template.status_changed to your webhook events

A template belongs to the WhatsApp Business Account, not to one sender, so when several senders share that account the event is delivered once, to a single sender: the first one, by creation date, that has an active webhook configured. Configure the webhook on the sender you use and it will arrive there. If two senders on the same account point at different endpoints, only the older of the two receives it — split them across accounts if both need to know.

Polling

Check template status via API:

TypeScript
const template = await zavu.templates.retrieve({ templateId: 'tmpl_abc123' }); console.log('Status:', template.whatsappStatus); if (template.rejectionReason) { console.log('Rejection reason:', template.rejectionReason); }

Syncing from WhatsApp

Two cases the webhook does not cover: a template created outside Zavu (in Meta Business Manager, or by another tool) never appears in Zavu, and a missed template.status_changed webhook leaves a template stuck in pending.

POST /v1/templates/sync reconciles both. It imports Meta templates Zavu does not have — or links them to an existing template with the same name — and refreshes the status of the ones it already knows about.

terminal
curl -X POST https://api.zavu.dev/v1/templates/sync \ -H "Authorization: Bearer $ZAVUDEV_API_KEY" \ -H "Content-Type: application/json" \ -d '{}'
JSON
{ "accountsSynced": 1, "imported": 2, "linked": 1, "updated": 3, "skipped": 12, "errors": [] }

Every WhatsApp sender in the project is synced by default; pass {"senderId": "sender_xyz"} to sync just one. The call waits for Meta, so it takes a few seconds per account — list your templates right after it returns. Templates that Meta reports as rejected or disabled are not imported and are counted in skipped.

Common Rejection Reasons

Content Policy Violations

Formatting Issues

IssueSolution
Invalid variable formatUse {{1}}, {{2}} format
Too many variablesLimit to 10 variables per component
Variable in wrong positionVariables can't be at start of template
Button URL without variableDynamic URLs must include variables

Category Mismatch

Templates may be rejected if the category doesn't match the content:

  • UTILITY templates shouldn't contain promotional content
  • MARKETING templates need clear opt-out language
  • AUTHENTICATION templates should only contain OTP/verification content

Rejection is not the only outcome. More often Meta recategorizes the template instead — typically UTILITY to MARKETING — and approves it under the new category. It can also recategorize a template months after approval. Either way the template keeps working and every send on it is billed at the new category's rate.

template.category reports the category Meta currently assigns, not the one you submitted. A recategorization is delivered as a template.status_changed webhook carrying data.category, and it fires even when the approval status did not move — so previousStatus and currentStatus can be identical on an event that changes what you pay. Branch on category, not only on currentStatus.

To reconcile templates that were recategorized before you were listening, run POST /v1/templates/sync; it refreshes the category alongside the status.

Language Issues

  • Template body must match the declared language
  • Special characters may cause issues in some languages
  • Emojis should be used sparingly

Fixing Rejected Templates

When a template is rejected:

  1. Check the rejection reason in the dashboard or API response
  2. Edit the template to fix the issues
  3. Resubmit for approval
TypeScript
// Create the corrected template const fixed = await zavu.templates.create({ name: 'order_confirmation_v2', language: 'en', body: 'Updated message content that addresses the rejection reason', whatsappCategory: 'UTILITY', }); // Submit it to the WhatsApp Business Account on this sender await zavu.templates.submit(fixed.id, { senderId: 'sender_abc123' }); // Once the replacement is approved, drop the rejected one await zavu.templates.delete('tmpl_abc123');

Best Practices for Approval

Be Clear and Specific

Vague templates are more likely to be rejected. Be specific about the purpose.

Match Category to Content

Ensure your template category accurately reflects the message type.

Avoid Promotional Language in UTILITY

UTILITY templates should be purely transactional without marketing content.

Include Opt-Out for MARKETING

Marketing templates should include clear unsubscribe instructions.

Template Examples

Good UTILITY Template

Code
Hi {{1}}, your order #{{2}} has shipped! Tracking number: {{3}} Estimated delivery: {{4}} Track your package: https://example.com/track/{{2}}

Good MARKETING Template

Code
Hi {{1}}! We have a special offer just for you: {{2}}% off your next purchase! Use code: {{3}} Valid until: {{4}} Reply STOP to unsubscribe.

Good AUTHENTICATION Template

Code
Your verification code is {{1}}. This code expires in 10 minutes. Do not share this code with anyone.

Rate Limits

ActionLimit
Submit templates100/minute
Templates per WABA250 total

Next Steps