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:
ModeEndpointBest forLimit
JSON inlinePOST /api/v1/{subdomain}/campaigns/sendprogrammatic sends, small/medium lists1,000 recipients/request
CSV uploadPOST /api/v1/{subdomain}/campaigns/send-csvlarge lists, spreadsheet exports50,000 rows/file
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
POST /api/v1/{subdomain}/campaigns/send
Headers
HeaderValue
AuthorizationBearer <your-token>
Content-Typeapplication/json
Body
{
  "campaign_name": "June Promo",
  "template_name": "welcome_message_1",
  "template_language": "en",
  "variables": {
    "body": ["@{firstname}", "@{order}"]
  },
  "recipients": [
    { "phone": "+919909919284", "firstname": "Rahul", "order": "Order #4471" },
    { "phone": "+918160748516", "firstname": "Priya", "order": "Order #4472" }
  ]
}
Body fields
campaign_name
string
required
A label for this campaign (shown in the dashboard).
template_name
string
required
An APPROVED template name for this workspace.
template_language
string
required
Template language code, e.g. en, en_US.
variables
object
Per-section arrays of @{key} expressions. Keys: header, body, footer, button. Positional to the template’s {{1}}, {{2}}, …
header_media_id
string
Pre-uploaded WhatsApp media id — required only for templates with a media (IMAGE/VIDEO/DOCUMENT) header.
recipients
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}.
curl -X POST "https://dash.example.com/api/v1/acme/campaigns/send" \
  -H "Authorization: Bearer <your_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "campaign_name": "June Promo",
    "template_name": "welcome_message_1",
    "template_language": "en",
    "variables": { "body": ["@{firstname}"] },
    "recipients": [
      { "phone": "+919909919284", "firstname": "Rahul" },
      { "phone": "+918160748516", "firstname": "Priya" }
    ]
  }'

Mode 2 — CSV upload

Endpoint
POST /api/v1/{subdomain}/campaigns/send-csv
Form fields (multipart/form-data)
FieldRequiredNotes
campaign_nameyescampaign label
template_nameyesAPPROVED template
template_languageyese.g. en
variablesnoJSON string, e.g. {"body":["@{firstname}"]}
header_media_idnofor media-header templates
fileyesthe .csv (≤ 20 MB / 50,000 rows)
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.
phone,firstname,order
+919909919284,Rahul,Order #4471
+918160748516,Priya,Order #4472
919723275223,Mukesh,Order #4473
variables.body[0] = "@{firstname}" fills {{1}}, "@{order}" fills {{2}}, resolved per row.
curl -X POST "https://dash.example.com/api/v1/acme/campaigns/send-csv" \
  -H "Authorization: Bearer <your_token>" \
  -F "campaign_name=June Promo" \
  -F "template_name=welcome_message_1" \
  -F "template_language=en" \
  -F 'variables={"body":["@{firstname}","@{order}"]}' \
  -F "file=@recipients.csv"

Response

Both modes return the same envelope (HTTP 200):
{
  "status": "success",
  "message": "campaign_queued",
  "data": {
    "campaign_id": 88,
    "campaign_name": "June Promo",
    "template_name": "welcome_message_1",
    "total_recipients": 2,
    "queued": 2
  }
}
FieldTypeNotes
campaign_idintegerUse to look up the campaign later.
total_recipientsintegerRecipients accepted (rows without a phone are excluded).
queuedintegerMessages queued for sending.
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

StatusWhenExample body
401Missing / invalid token{"status":"error","message":"Invalid API token"}
403Missing ability{"status":"error","message":"Token does not have the required ability: messages.send"}
404Template not found / not approved{"status":"error","message":"Approved template not found for the given name and language"}
422Validation, or media-header template without header_media_id, or > limit recipients{"status":"error","message":"Template \"...\" has a IMAGE header — pass \"header_media_id\" ..."}
429Rate limit{"message":"Too many requests","retry_after":45}
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.