/v1/templatesbetaCreate a WhatsApp template
- Needs
- Send WhatsApp messages; manage templates, quick messages and WhatsApp short links
(messaging:write) - Plan
- Any plan with API access
- Limits
- 60 a minute per key
- Dry run
- Yes — a full preview with ?dry_run=true
- Confirm
- Header
Api-Confirm: submit - Undo
- A submission cannot be withdrawn. Archive the template with Archive templates once it is reviewed.
Checks a template draft against WhatsApp's rules, then submits it to WhatsApp for review. The submission is visible to WhatsApp and counts against your account's template limits, so it needs Api-Confirm: submit. Review takes minutes to hours: read the template with Get a template until it is APPROVED or REJECTED.
name: lower-case letters, digits and_; the same name can exist once per language. A name that exists in that language is refused with409(reason: duplicate).language: a WhatsApp language code such asen,en_USorpt_BR.- A draft that breaks a rule is refused with
400(reason: template_rules);errors[]names each field and rule. Check a template draft explains every rule with a fix. - An IMAGE, VIDEO or DOCUMENT header needs a sample handle from Upload a template header sample.
Run it with ?dry_run=true first: every check runs and payload_preview shows exactly what would be submitted, but nothing is.
Try it
Protects against doing it twice if you retry: a retry with the same key gets the first answer back instead of running again.
Code and response
curl -X POST 'https://mcp.wa-api.cloud/v1/templates?dry_run=true' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"template": {
"name": "order_ready",
"language": "en_US",
"category": "UTILITY",
"body": {
"text": "Hi {{1}}, your order {{2}} is ready for pickup today.",
"examples": [
"Jane",
"#1001"
]
},
"footer": {
"text": "Reply STOP to opt out"
},
"buttons": [
{
"type": "QUICK_REPLY",
"text": "On my way"
}
]
}
}'The code reads your key from $API_KEY.
Body
| Field | Type | What it is |
|---|---|---|
| templaterequired | object | The draft: name, language, category, optional header, body, footer and buttons. |
| namerequired | string | Template name: lowercase letters, digits, underscores (e.g. order_shipped_v2).0–1000 characters |
| languagerequired | string | Meta language code, e.g. en, en_US, ar, hi, pt_BR.0–20 characters |
| categoryrequired | string | MARKETING | UTILITY | AUTHENTICATION.0–40 characters |
| header | object | Optional header. |
| formatrequired | string | TEXT | IMAGE | VIDEO | DOCUMENT | LOCATION.one of: TEXT, IMAGE, VIDEO, DOCUMENT, LOCATION |
| text | string | TEXT header: ≤ 60 chars, at most one {{1}}.0–1000 characters |
| example | string | TEXT header with {{1}}: its sample value.0–1000 characters |
| media_handle | string | IMAGE/VIDEO/DOCUMENT header: the Meta upload handle used as the sample (POST /v1/templates/samples).0–4000 characters |
| body | object | Body.default {} |
| text | string | Body text (≤ 1024 chars) with {{1}}, {{2}} … variables. Omit for AUTHENTICATION (Meta fixes it).0–5000 characters |
| examples | array of string | One sample per body variable, in order ({{1}} first).0–100 items |
| add_security_recommendation | boolean | AUTHENTICATION only: append "For your security, do not share this code." |
| footer | object | Optional footer. |
| text | string | Footer text (≤ 60 chars, no variables).0–1000 characters |
| code_expiration_minutes | integer | AUTHENTICATION only: code expiry shown in the footer (1-90).-9007199254740991–9007199254740991 |
| buttons | array of object | Buttons in display order (≤ 10).0–30 items |
| typerequired | string | QUICK_REPLY | URL | PHONE_NUMBER | COPY_CODE | FLOW | VOICE_CALL | OTP.one of: QUICK_REPLY, URL, PHONE_NUMBER, COPY_CODE, FLOW, VOICE_CALL, OTP |
| text | string | Button label (≤ 25 chars). Not used for COPY_CODE.0–200 characters |
| url | string | URL buttons: https URL; for a dynamic URL end it with {{1}} (e.g. https://shop.example/track/{{1}}).0–4000 characters |
| url_example | string | Dynamic URL buttons: a full example URL with the variable filled in.0–4000 characters |
| phone_number | string | PHONE_NUMBER buttons: E.164 number, e.g. +971501234567.0–64 characters |
| coupon_code | string | COPY_CODE buttons: the coupon code to copy (≤ 20 chars).0–64 characters |
| flow_id | string | FLOW buttons: the WhatsApp flow id.0–64 characters |
| flow_action | string | FLOW buttons: NAVIGATE (default) or DATA_EXCHANGE for endpoint flows.one of: NAVIGATE, DATA_EXCHANGE |
| navigate_screen | string | FLOW buttons with NAVIGATE: the first screen id.0–200 characters |
| otp_type | string | OTP buttons: COPY_CODE (default), ONE_TAP or ZERO_TAP.one of: COPY_CODE, ONE_TAP, ZERO_TAP |
| supported_apps | array of object | ONE_TAP / ZERO_TAP OTP buttons: your Android app(s).0–5 items |
| package_namerequired | string | Android package name.0–255 characters |
| signature_hashrequired | string | App signing key hash.0–64 characters |
| limited_time_offer | object | Limited-time offer (MARKETING; needs a COPY_CODE button). |
| textrequired | string | Offer text shown on the chip (≤ 16 chars).0–200 characters |
| has_expiration | boolean | Show a countdown (the expiry is set when sending). |
| carousel | object | Media carousel (MARKETING): the body is the message above the cards; no top-level header, footer or buttons. |
| cardsrequired | array of object | 2-10 cards.0–20 items |
| header_formatrequired | string | Card media type (the same on every card).one of: IMAGE, VIDEO |
| media_handlerequired | string | Meta upload handle for the card media (POST /v1/templates/samples).0–4000 characters |
| body | object | Card text. |
| buttonsrequired | array of object | 1-2 buttons (QUICK_REPLY, URL, PHONE_NUMBER), the same layout on every card.0–5 items |
| business_account_id | string | The WhatsApp account to create it in. Needed only when your workspace has several.0–64 characters |
| channel_id | string | Instead of business_account_id: a connected WhatsApp channel; its account is used.0–64 charactersSigned in? Pick one from your data with “My data”. |
| allow_category_change | boolean | Let WhatsApp change the category instead of rejecting the template (default true). |
Headers
| Field | Type | What it is |
|---|---|---|
| Authorizationrequired | header | Bearer $API_KEY — your API key. |
| Api-Version | header | The API version to use, e.g. 2026-10-01. Default: the version your key is pinned to.one of: 2026-10-01 · pattern ^\d{4}-\d{2}-\d{2}$ |
| Idempotency-Keyrequired | header | Required here. Any unique string (8–128 characters), e.g. your order id plus the step. Kept 24 hours. Not needed with dry_run=true.pattern ^[A-Za-z0-9._:-]+$ · 8–128 characters |
| Api-Confirmrequired | header | Type the operation's verb (e.g. delete, submit) to confirm a change that cannot be undone or that WhatsApp or your customers see. Not needed with dry_run=true. |
Response 201
Submitted for review (or, on a dry run, the checked draft and the exact payload).
| Field | Type | What it is |
|---|---|---|
| submittedrequired | boolean | true when the template was sent to Meta for review. |
| idrequired | string or null | Template id when submitted. |
| namerequired | string | |
| languagerequired | string | |
| categoryrequired | string or null | |
| statusrequired | string or null | |
| business_account_idrequired | string | |
| business_account_namerequired | string | The account, by name — say it to the user. |
| lintrequired | object | |
| validrequired | boolean | true when there are no errors (warnings may remain). |
| errorsrequired | number | |
| warningsrequired | number | |
| issuesrequired | array of object | |
| rulerequired | string | |
| severityrequired | string | one of: error, warning, info |
| pathrequired | string | |
| messagerequired | string | |
| fixrequired | string | |
| variablesrequired | object | |
| headerrequired | number | |
| bodyrequired | number | |
| previewrequired | string or null | Body with samples filled in. |
| payload_previewrequired | array of object | The exact components that are (or would be) submitted. |
| dry_run | boolean | true when this was a dry run: every check ran and nothing changed. |
Errors
Errors are application/problem+json. Branch on code.
| Status | Code | When |
|---|---|---|
| 400 | invalid_input | A field is missing or has the wrong format. |
| 401 | unauthenticated | The Authorization header is missing, the key is unknown, expired or revoked. |
| 403 | entitlement_required | The workspace's plan does not include API access ( |
| 403 | insufficient_scope | The key does not have the permission this operation needs. |
| 404 | not_found | The channel or WhatsApp account does not exist in your workspace. |
| 409 | conflict | A template with this name already exists in this language ( |
| 428 | confirm_required | Send the header |
| 429 | rate_limited | The key or workspace went over its rate limit. Wait for |
| 502 | upstream_error | WhatsApp refused the submission. |
| 503 | upstream_unavailable | A service behind the API is briefly unavailable. Safe to retry with backoff. |
| 504 | timeout | The change did not finish in time. Retry with the same Idempotency-Key: it never runs twice. |
Examples
An order-ready utility template
Request body
{
"template": {
"name": "order_ready",
"language": "en_US",
"category": "UTILITY",
"body": {
"text": "Hi {{1}}, your order {{2}} is ready for pickup today.",
"examples": [
"Jane",
"#1001"
]
},
"footer": {
"text": "Reply STOP to opt out"
},
"buttons": [
{
"type": "QUICK_REPLY",
"text": "On my way"
}
]
}
}Response 201
{
"submitted": false,
"id": null,
"name": "order_ready",
"language": "en_US",
"category": "UTILITY",
"status": null,
"business_account_id": "4410",
"business_account_name": "Main WhatsApp",
"lint": {
"valid": true,
"errors": 0,
"warnings": 0,
"issues": [],
"variables": {
"header": 0,
"body": 2
},
"preview": null
},
"payload_preview": [
{
"type": "BODY",
"text": "Hi {{1}}, your order {{2}} is ready for pickup today.",
"example": {
"body_text": [
[
"Jane",
"#1001"
]
]
}
},
{
"type": "FOOTER",
"text": "Reply STOP to opt out"
},
{
"type": "BUTTONS",
"buttons": [
{
"type": "QUICK_REPLY",
"text": "On my way"
}
]
}
],
"dry_run": true
}Operation path
The same operation is also at POST /v1/ops/messaging_create_template, with every field in the JSON body.