Template Message Payload Guide

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
FieldContract
template_nameExact approved WATI template name.
broadcast_nameRequest/campaign label; not an idempotency key.
channelOptional 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.
recipients1–10,000 entries. Split larger sets yourself and track each batch.
target / phone_numberSupply 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_idPer-recipient caller correlation when available; not a request-level deduplication guarantee.
custom_paramsPer-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:false and error; it is not automatic acceptance.
  • Even success:true can 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_name or local_message_id does 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.