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’sfirstnamevalue.- Static text is allowed too:
"variables": { "body": ["Welcome", "@{order}"] }.
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.Mode 1 — JSON inline
Endpoint
Body
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
Endpointmultipart/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 (HTTP200):
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.sent → message.delivered → message.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.