Skip to main content
Send an approved WhatsApp template to many recipients at once. Each message is queued and sent through the same engine as the dashboard’s bulk campaigns, so you get the same pacing, retries, delivery tracking, and chat-history sync — driven entirely from the API. Two input modes: Both return immediately with a campaign_id (messages are queued, not sent synchronously). Track delivery via outbound webhooks or the Get message status endpoint. Required ability: messages.send.

How variable substitution works

A template’s body/header can contain positional placeholders {{1}}, {{2}}, … You map each placeholder to a recipient field using a @{key} expression in variables, and supply the value per recipient.
  • variables.body[0] → fills {{1}}, variables.body[1]{{2}}, and so on.
  • @{firstname} resolves to each recipient’s firstname value.
  • Static text is allowed too: "variables": { "body": ["Welcome", "@{order}"] }.
The same applies to variables.header, variables.footer, and variables.button.
Buttons: only send variables.button for templates whose button URL is dynamic (contains a {{n}} placeholder). Static URL buttons, quick-reply, and copy-code buttons take no parameter — if you send one anyway, it is automatically ignored (so you can’t trigger a parameter-count error).
Media-header templates (IMAGE / VIDEO / DOCUMENT header) require a pre-uploaded header_media_id. If the template needs one and you omit it, the request returns 422 with a clear message.
Not supported in bulk: carousel (multi-card) templates and authentication/OTP templates. Use the single-message endpoints for those.

Mode 1 — JSON inline

Endpoint
Headers Body
Body fields
string
required
A label for this campaign (shown in the dashboard).
string
required
An APPROVED template name for this workspace.
string
required
Template language code, e.g. en, en_US.
object
Per-section arrays of @{key} expressions. Keys: header, body, footer, button. Positional to the template’s {{1}}, {{2}}, …
string
Pre-uploaded WhatsApp media id — required only for templates with a media (IMAGE/VIDEO/DOCUMENT) header.
array
required
1–1000 objects. Each MUST have phone. Any other keys (firstname, order, …) are stored per recipient and are addressable from variables via @{key}.

Mode 2 — CSV upload

Endpoint
Form fields (multipart/form-data) CSV format — first row is the header; one column must be phone. All other columns become per-recipient values addressable via @{column}. Rows without a phone are skipped automatically.
variables.body[0] = "@{firstname}" fills {{1}}, "@{order}" fills {{2}}, resolved per row.

Response

Both modes return the same envelope (HTTP 200):
Per-message message_id (WhatsApp wamid) is not in this response — messages are queued, not yet sent. You receive each wamid in the message.sent webhook as the message goes out.

Tracking delivery

Every message — whether sent via this endpoint, the single-send endpoints, or the dashboard — emits delivery webhooks you can subscribe to: message.sentmessage.deliveredmessage.read (or message.failed). Each event carries the message_id so you can match it back to your records. See Webhook Events → Message delivery status. For one-off lookups, use Get message status.

Error responses

Sending speed beyond your token’s rate limit is handled internally by the queue — you don’t need to throttle the API call itself. Actual delivery pace to recipients is also subject to WhatsApp’s own per-number messaging tier.