/v1/messages/templatestableSend 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 · 20 an hour · 100 a day · 3 per contact a day
- Dry run
- Yes — a full preview with ?dry_run=true
- Undo
- Cannot be undone: the customer receives the message.
Sends one approved WhatsApp template to one contact, now. The customer receives it and it cannot be undone.
The variables are checked against the template first. Opt-outs and do-not-disturb are always respected. For bulk sends, use a broadcast in the app instead.
Try it with ?dry_run=true: every check runs and the preview tells you which number it would send from, but nothing is sent.
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/messages/template?dry_run=true' \
-H "Authorization: Bearer $API_KEY" \
-H 'Content-Type: application/json' \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"contact_id": "48213",
"template_id": "9120",
"channel_id": "301",
"body_variables": [
"Jane",
"#1001",
"https://track.example.com/1001"
]
}'The code reads your key from $API_KEY.
Body
| Field | Type | What it is |
|---|---|---|
| contact_idrequired | string | integer | Who receives it.Signed in? Pick one from your data with “My data”. |
| template_idrequired | string | integer | An APPROVED template.Signed in? Pick one from your data with “My data”. |
| channel_id | string | Send from this WhatsApp channel. Default: your default WhatsApp channel.0–20 charactersSigned in? Pick one from your data with “My data”. |
| body_variables | array of string | One value per {{n}} in the body, in order. No new lines or tabs.0–100 items · default [] |
| header_text | string | Value for a {{1}} in a TEXT header.0–60 characters |
| header_media_url | string | https link to the image, video or document for a media header.uri · 0–2000 characters |
| button_url_suffixes | array of object | One per dynamic URL button.0–2 items · default [] |
| button_indexrequired | integer | Button position (0-based, from GET /v1/templates/{template_id}).0–9 |
| valuerequired | string | Suffix replacing {{1}} in the URL.0–2000 characters |
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 |
Response 202
Accepted for delivery (or the dry-run preview).
| Field | Type | What it is |
|---|---|---|
| message_idrequired | string or null | |
| conversation_idrequired | string or null | |
| statusrequired | string | queued (accepted for delivery) or failed. |
| templaterequired | object | |
| idrequired | string | |
| namerequired | string | |
| languagerequired | string or null | |
| sent_atrequired | string or null | |
| would_send_from | object or null | Dry run only: the channel it would send from. |
| idrequired | string | |
| namerequired | string or null | |
| identifierrequired | string or null | |
| 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 | forbidden | AUTHENTICATION templates cannot be sent through the API. |
| 403 | insufficient_scope | The key does not have the permission this operation needs. |
| 404 | not_found | The contact, template or channel does not exist. |
| 409 | conflict | The template is not APPROVED, or the channel is not connected. Also returned while a request with the same Idempotency-Key is still running. |
| 429 | rate_limited | The key or workspace went over its rate limit. Wait for |
| 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
Tell a customer their order shipped
Request body
{
"contact_id": "48213",
"template_id": "9120",
"channel_id": "301",
"body_variables": [
"Jane",
"#1001",
"https://track.example.com/1001"
]
}Response 202
{
"message_id": null,
"conversation_id": "77410",
"status": "dry_run",
"template": {
"id": "9120",
"name": "order_shipped",
"language": "en_US"
},
"sent_at": null,
"would_send_from": {
"id": "301",
"name": "Main WhatsApp",
"identifier": "+15555550100"
},
"dry_run": true
}Operation path
The same operation is also at POST /v1/ops/messaging_send_template_message, with every field in the JSON body.