Template Message Payload Guide: supported contracts, practical examples and recovery.
Use POST /api/ext/v3/messageTemplates/send for new single-recipient and batch integrations. One recipient is a batch of one. Use Authentication first; a read-only template scope does not authorize sending.
1. Read the approved template
Read the actual template through GET /api/ext/v3/messageTemplates. Confirm the exact name, approved status, sending channel and required custom_params. These are WATI parameter names; do not build the request from Meta placeholder positions alone.
For example, the request below assumes your approved template is named order_update and requires parameters named customer_name and order_number. Replace those names and values with your actual template definition; this does not create or approve a template.
2. Build a V3 request
{
"template_name": "order_update",
"broadcast_name": "order-updates-20261103",
"recipients": [
{
"target": "15551234567",
"local_message_id": "order-1001-notice-1",
"custom_params": [
{"name": "customer_name", "value": "Sam"},
{"name": "order_number", "value": "1001"}
]
}
]
}Save the adapted JSON as template-request.json. The following command sends a real message: confirm the approved template, intended recipient and sending account before executing it.
curl --silent --show-error --include \
--request POST "${WATI_API_HOST}/api/ext/v3/messageTemplates/send" \
--header "Authorization: Bearer ${WATI_API_TOKEN}" \
--header 'Content-Type: application/json' \
--data-binary @template-request.json| Field | Contract |
|---|---|
template_name | Exact approved WATI template name. |
broadcast_name | Request/campaign label; not an idempotency key. |
channel | Optional top-level sending-channel selector. Use the supported channel name/number for your account; a channel ID is not interchangeable. If omitted, the applicable default channel is used. |
recipients | 1–10,000 entries. Split larger sets yourself and track each batch. |
target / phone_number | Supply a non-empty supported recipient identifier. A non-whitespace target takes precedence if both are present; do not provide conflicting values. A contact ID or business-scoped WhatsApp identifier is not an Instagram/Messenger identifier. |
local_message_id | Per-recipient caller correlation when available; not a request-level deduplication guarantee. |
custom_params | Per-recipient array of {name,value} strings using the actual template parameter mapping. Do not send top-level parameters or raw Meta components as the V3 substitute. |
The example uses the default channel. For another channel, add its supported value as top-level channel. A parameter-free template can omit custom_params; do not add sample names that the template does not use.
3. Supply parameter and media values
Use each required parameter's exact name. Body parameter names must match the template mapping; array order alone is not the binding contract. Values are strings. Do not send null, an object or an array where a string is required.
For a dynamic media header, inspect the actual template's header.link mapping. Only if it contains a placeholder such as {{image_url}}, include its matching name/value pair in that recipient's custom_params:
[
{"name": "customer_name", "value": "Sam"},
{"name": "order_number", "value": "1001"},
{"name": "image_url", "value": "https://example.com/order-1001.jpg"}
]image_url is an example mapping name, not a universal header field. Use your template's actual placeholder and a URL usable by the messaging service/provider. Do not invent header_image, header_document or a fixed media-parameter name. Static-media headers and other template types keep their own approved-template requirements.
For Flow-specific parameters, follow the endpoint's documented fields; a JSON value such as flow_action_data is serialized inside a string, not inserted as a nested object. Do not add optional Flow values to an unrelated template.
4. Reconcile every recipient
Inspect HTTP status, then the business response:
- HTTP 200 can contain
success:falseanderror; it is not automatic acceptance. - Even
success:truecan contain recipient errors or omit an unresolved/unavailable recipient. Compare the returned recipient set with the requested set; missing is unresolved, not successful. - Save the returned
broadcast_id, recipient results and your correlation IDs. Acceptance is not delivery. Track applicable sent/delivered/read/failure events using the webhook migration and recovery guide. - Correct deterministic validation/eligibility failures before retrying. For a timeout, connection loss, 5xx or missing recipient outcome, reconcile the previous attempt first. Reusing
broadcast_nameorlocal_message_iddoes not make a repeated send safe.
For scheduling, use the schedule operation and its date/time contract. Do not add scheduling fields to the send example or treat scheduled acceptance as delivery.
Existing V1 integrations
The retained V1 single-send query route accepts whatsappNumber or a supported target; a non-whitespace target takes precedence. Business-scoped identity targets depend on account support. The body requires template_name and broadcast_name; parameters supplies the template values and channel_number is optional. Its parameters are WATI name/value entries resolved through the template mapping; they are not a bare positional Meta array. A bulk legacy contract is different again: follow its own schema.
When moving to V3, put each recipient's values in recipients[].custom_params, select the top-level channel where needed, and update the response parser to V3 success/recipient results. Do not copy legacy field names or ok/result handling into V3. Use the five-language V3 recipe for a minimal executable starting point.

