Managing templates
What you can and cannot do
| Action | Supported | How |
|---|---|---|
| Create and submit for review | Yes | POST /api/templates — one call does both. |
| Upload a header sample | Yes | POST /api/templates/media, before creating. |
| List, filter and search | Yes | GET /api/templates |
| Read one | Yes | GET /api/templates/:id |
| Fetch the stored header sample | Yes | GET /api/templates/:id/media |
| Refresh one template’s status | Yes | POST /api/templates/:id/sync |
| Refresh every template on a number | Yes | POST /api/templates/waba/:wabaAccountId/sync |
| Import templates created outside WpAi | Yes | POST /api/templates/import |
| Delete | Yes | DELETE /api/templates/:id — removes it on Meta too. |
| Edit an existing template | No | There is no PUT or PATCH. Delete and recreate. |
| Bulk create | No | One call per template. |
| Bulk delete | No | One call per template. |
| Create a carousel or limited-time offer | No | Readable if imported; not creatable here. |
| Parameterise buttons | No | No dynamic URL suffixes or OTP copy-code buttons. |
There is no edit route
(number, name, language), so recreating before the delete has propagated returns 409. Delete first, confirm, then create.Creating one
/api/templatesAPI key or sessionPlan feature: campaignsCreates it here and submits it to Meta in the same call. Returns with status PENDING.
curl -X POST https://api.wpai.co.in/api/templates \
-H "X-API-Key: $WPAI_KEY" \
-H "Content-Type: application/json" \
-d '{
"wabaAccountId": "68a1f3…",
"name": "order_shipped",
"language": "en_US",
"category": "UTILITY",
"components": [
{ "type": "HEADER", "format": "TEXT", "text": "Order update" },
{
"type": "BODY",
"text": "Hi {{1}}, your order {{2}} has shipped.",
"example": { "body_text": [["Priya", "4471"]] }
},
{ "type": "FOOTER", "text": "Reply STOP to opt out" }
]
}'The name must be lowercase letters, numbers and underscores. Component rules are the substance of this — see /docs/templates/components for every combination.
{
"template": {
"_id": "68b7…",
"name": "order_shipped",
"language": "en_US",
"category": "UTILITY",
"status": "PENDING",
"metaTemplateId": "1234567890",
"components": [ /* as you sent them */ ]
}
}Statuses and categories
| Status | Meaning |
|---|---|
DRAFT | Created here but never submitted. Rare through the API. |
PENDING | With Meta for review. Usually minutes, sometimes hours. |
APPROVED | Sendable. The only status a send accepts. |
REJECTED | Meta refused it. `rejectedReason` says why. |
PAUSED | Suspended for poor quality. Fix the content and resubmit. |
DISABLED | Withdrawn or deleted at Meta. Not sendable. |
Categories are MARKETING | UTILITY | AUTHENTICATION, and the one Meta assigns may differ from the one you asked for. It decides the rate, so a re-categorisation changes what a send costs.
Learning the outcome
Review is asynchronous. There are two ways to find out what happened, and only one of them is a good idea.
Subscribe to the webhook — recommended
template.status fires the moment Meta reviews it, carrying the name, language, new status and the rejection reason. template.category fires on a re-categorisation, which is the one that changes your costs.
{
"event": "template.status",
"timestamp": "2026-08-26T09:14:22.104Z",
"data": {
"name": "order_shipped",
"language": "en_US",
"metaTemplateId": "1234567890",
"status": "APPROVED",
"reason": ""
}
}Poll — the fallback
/api/templates/:id/syncAPI key or sessionPlan feature: campaignsPulls the current status from Meta and returns the updated template.
Approval can take hours, so polling means either a long wait between checks or a lot of wasted calls. Prefer the webhook and use this to reconcile.
Listing and filtering
/api/templatesAPI key or sessionPlan feature: campaigns?status= ?category= ?wabaAccountId= ?search= plus page and limit. Cached 120 seconds.
searchmatches the template name and its body text.- Omitting
wabaAccountIdreturns templates across every number you own — excluding rows orphaned by a number that has since been removed. - Sorted by most recently updated. Default 20 per page, maximum 100.
Imported templates can contain more than you can create
CAROUSEL, LIMITED_TIME_OFFER or CARD components, and OTP, MPM, CATALOG or FLOW buttons. They are readable and sendable by id; they are not creatable through this API.Importing and syncing
/api/templates/importAPI key or sessionPlan feature: campaignsBody: { wabaAccountId }. Pulls every template Meta holds for that number and upserts it.
Use this when a number was managed elsewhere before, or after reconnecting one. It reads a single page of up to 200 templates and normalises Meta's wider vocabulary into ours — IN_APPEAL becomes PENDING, TRANSACTIONAL becomes UTILITY, and an unrecognised status becomes PENDING.
{ "ok": true, "total": 34, "imported": 12, "updated": 22 }Deleting
/api/templates/:idAPI key or sessionPlan feature: campaignsDeleting removes every language variant
order_shipped removes the Hindi one at Meta too, while only the row you named disappears here. Re-import afterwards to bring the local list back in step.A template already gone at Meta is not an error — the local row is cleaned up anyway. Any other Meta failure is returned with Meta's own status and the local row survives, so you can retry.