Skip to content

Campaigns

Campaigns allow you to send bulk WhatsApp messages to multiple contacts using approved templates. The API handles rate limiting and delivery tracking automatically.

Each endpoint requires a campaigns permission and returns 403 without it. See Roles & Permissions for the permission each endpoint requires.

Retrieve all campaigns.

Terminal window
GET /api/campaigns
Parameter Type Description
page integer Page number (default: 1)
limit integer Page size, 1–100. Default 50.
status string Filter by status
whatsapp_account string Filter by WhatsApp account name
search string Case-insensitive match on the campaign name
from / to string YYYY-MM-DD range filter on created_at (to is inclusive of the whole day)

Ordered by created_at DESC.

{
"status": "success",
"data": {
"campaigns": [
{
"id": "uuid",
"name": "Holiday Promotion",
"whatsapp_account": "Main Business",
"template_id": "uuid",
"template_name": "holiday_offer",
"status": "completed",
"total_recipients": 1000,
"sent_count": 1000,
"delivered_count": 950,
"read_count": 500,
"failed_count": 50,
"scheduled_at": "2024-01-01T10:00:00Z",
"started_at": "2024-01-01T10:00:05Z",
"completed_at": "2024-01-01T10:30:00Z",
"created_by_name": "Jane Admin",
"updated_by_name": "Jane Admin",
"created_at": "2024-01-01T09:00:00Z",
"updated_at": "2024-01-01T10:30:00Z"
}
],
"total": 25,
"page": 1,
"limit": 50
}
}

Retrieve a single campaign with detailed progress.

Terminal window
GET /api/campaigns/{id}
{
"status": "success",
"data": {
"id": "uuid",
"name": "Holiday Promotion",
"template_id": "uuid",
"template_name": "holiday_offer",
"whatsapp_account": "Main Business",
"status": "processing",
"total_recipients": 1000,
"sent_count": 450,
"delivered_count": 400,
"read_count": 100,
"failed_count": 10,
"scheduled_at": null,
"started_at": "2024-01-01T10:00:05Z",
"created_at": "2024-01-01T09:00:00Z",
"updated_at": "2024-01-01T10:05:00Z"
}
}

Create a new campaign. Per-recipient template values are supplied when you import recipients, not on the campaign itself — there is no variable_mapping field.

Terminal window
POST /api/campaigns
{
"name": "New Year Sale",
"whatsapp_account": "Main Business",
"template_id": "uuid",
"header_media_id": "",
"scheduled_at": "2024-01-01T00:00:00Z"
}
Field Type Required Description
name string Yes Campaign name
whatsapp_account string Yes Name of the sending WhatsApp account
template_id uuid Yes Template to send
header_media_id string No Meta media ID for a media-header template — upload it first with POST /api/campaigns/{id}/media
scheduled_at string No ISO-8601 time to start the campaign
{
"status": "success",
"data": {
"id": "uuid",
"name": "New Year Sale",
"status": "draft",
"created_at": "2024-01-01T00:00:00Z"
}
}

Update a draft campaign.

Terminal window
PUT /api/campaigns/{id}

Delete a campaign.

Terminal window
DELETE /api/campaigns/{id}

Import recipients from a CSV or JSON file.

Terminal window
POST /api/campaigns/{id}/recipients/import
{
"recipients": [
{
"phone_number": "+1234567890",
"recipient_name": "John Doe",
"template_params": { "customer_name": "John", "coupon": "SAVE20" },
"header_params": { "season": "Summer" }
},
{
"phone_number": "+0987654321",
"recipient_name": "Jane Smith",
"template_params": { "customer_name": "Jane", "coupon": "SAVE15" },
"header_params": { "season": "Winter" }
}
]
}

header_params is only needed for templates with a TEXT header variable (Meta allows at most one). It’s kept separate from template_params so a positional header {{1}} doesn’t collide with body {{1}}. Omit the field entirely for templates without a header variable.

{
"status": "success",
"data": {
"message": "Recipients added successfully",
"added_count": 2,
"total_recipients": 1002
}
}

added_count is the number of recipients in this request; total_recipients is the campaign’s new total.

Add saved contacts to a draft campaign by tag, by contact ID, or all at once. Requires campaigns:write; users without contacts:read can only add contacts assigned to them.

Terminal window
POST /api/campaigns/{id}/recipients/from-contacts
{
"tags": ["vip"],
"contact_ids": ["2b7e1d36-1c1f-4f5e-9b2a-6c1d2e3f4a5b"],
"header": { "source": "name", "value": "friend" },
"params": {
"1": { "source": "name", "value": "there" },
"2": { "source": "value", "value": "AUTUMN10" }
},
"dry_run": true
}
Field Description
tags Contacts with any of these tags are added
contact_ids Individual contacts, added together with the tagged ones
all true adds every contact the user can see; tags and contact_ids are then ignored
search With all, keeps only contacts whose name or phone matches, like the contact list search
header Value source for a TEXT header variable; required when the template has one
params Value source for each body variable, keyed by variable name; every variable is required
dry_run true returns the counts without adding anything

Each value source is one of:

  • {"source": "name", "value": "..."}: the contact’s name, or value when the contact has no name (value is required)
  • {"source": "phone"}: the contact’s phone number
  • {"source": "value", "value": "..."}: the same value for every recipient

Numbers already in the campaign are skipped, and contacts with marketing_opt_out are skipped for MARKETING templates. At most 50000 contacts can match one request.

{
"status": "success",
"data": {
"matched_count": 3,
"added_count": 2,
"skipped_existing": 1,
"skipped_opted_out": 0,
"total_recipients": 3
}
}

total_recipients is only returned when recipients were added. dry_run is returned when nothing was added.

Get all recipients of a campaign with their per-recipient delivery status. This endpoint returns the full recipient list for the campaign — it is not paginated.

Terminal window
GET /api/campaigns/{id}/recipients

The recipients array holds every recipient; total is the count.

{
"status": "success",
"data": {
"recipients": [
{
"id": "uuid",
"campaign_id": "uuid",
"phone_number": "1234567890",
"recipient_name": "John Doe",
"status": "delivered",
"template_params": { "customer_name": "John", "coupon": "SAVE20" },
"header_params": { "season": "Summer" },
"whatsapp_message_id": "wamid.xxx",
"error_message": "",
"sent_at": "2024-01-01T10:00:10Z",
"delivered_at": "2024-01-01T10:00:15Z",
"read_at": "2024-01-01T10:05:00Z"
}
],
"total": 1000
}
}
Field Type Description
phone_number string Recipient phone number
recipient_name string Recipient display name
status string pending, sent, delivered, read, or failed
template_params object Body parameter values used for this recipient
header_params object TEXT-header parameter values (if any)
whatsapp_message_id string Meta message ID once sent
error_message string Failure reason when status is failed
sent_at / delivered_at / read_at string Delivery timestamps (null until reached)

Remove a single recipient from a campaign. Only allowed while the campaign is in draft status.

Terminal window
DELETE /api/campaigns/{id}/recipients/{recipientId}
{
"status": "success",
"data": {
"message": "Recipient deleted successfully"
}
}

Upload the media asset for a campaign whose template has a media header (image, video, or document). Allowed only for draft campaigns whose template has a non-TEXT header. The file is uploaded to WhatsApp and stored locally for preview.

Terminal window
POST /api/campaigns/{id}/media

Send the request as multipart/form-data with a file field (max 16 MB).

Terminal window
curl -X POST "http://your-server:8080/api/campaigns/{id}/media" \
-H "X-API-Key: whm_your_api_key" \
-F "file=@banner.jpg"
{
"status": "success",
"data": {
"media_id": "meta-media-id",
"filename": "banner.jpg",
"mime_type": "image/jpeg",
"local_path": "campaigns/uuid.jpg",
"message": "Media uploaded successfully"
}
}

Serve the previously uploaded campaign media file (used for previews). Returns the raw media bytes, not a JSON envelope.

Terminal window
GET /api/campaigns/{id}/media

Begin sending messages.

Terminal window
POST /api/campaigns/{id}/start

Pause a running campaign.

Terminal window
POST /api/campaigns/{id}/pause

Cancel a campaign (cannot be resumed).

Terminal window
POST /api/campaigns/{id}/cancel

Re-queue every recipient whose delivery failed. Allowed only on completed, paused, or failed campaigns. Failed recipients are reset to pending, the campaign moves back to processing, and the messages are re-enqueued.

Terminal window
POST /api/campaigns/{id}/retry-failed
{
"status": "success",
"data": {
"message": "Retrying failed messages",
"retry_count": 12,
"status": "processing"
}
}

Fetch live progress for a campaign. This returns the same payload as Get Campaign (status plus sent/delivered/read/failed counts) and is convenient for polling while a campaign is sending.

Terminal window
GET /api/campaigns/{id}/progress
Status Description
draft Campaign created, not yet started
scheduled Campaign scheduled for future sending
queued Recipients have been pushed onto the Redis job queue
processing Workers are actively sending messages
paused Campaign is paused
completed All messages have been processed
cancelled Campaign was cancelled
failed The campaign could not be processed

Sends are dispatched through a Redis Streams job queue and consumed by worker processes. Throughput is governed by how many workers you run (-workers=N on server or worker), not by a built-in per-second throttle — KiteGreet does not shape traffic to your Meta messaging tier. If you exceed your tier, Meta rejects the sends and the affected recipients are marked failed; use Retry Failed once your limits allow.