Skip to main content

Create Campaign

Create a WhatsApp campaign that sends an approved template to a list of recipients. Phone numbers are normalized and upserted as contacts in your account automatically — you don't need to call Create Contact first.

POST /api/v1/campaigns/

After creating a campaign, call Send Campaign to dispatch it, then Campaign Stats to track delivery.

Scheduling vs. immediate send

If you pass scheduled_at, the campaign is created in scheduled state and runs automatically at that time. Otherwise it's created as a draft and you must explicitly send it.

Request body

FieldTypeRequiredDescription
namestringYesCampaign name (max 255 chars)
descriptionstringNoCampaign description (max 1000 chars)
template_namestringYesName of an APPROVED template on your account
contactsarrayYes1–10,000 recipient entries: { "phone": "...", "name": "..." }
body_dataobjectNoTemplate body variables applied to every contact — same format as Send Template Message
header_dataobjectNoHeader variables for templates with a variable TEXT header
media_urlstringConditionalPublic URL for header media — required if the template has an IMAGE/VIDEO/DOCUMENT header
filenamestringNoFilename for DOCUMENT media
waba_phone_idstringNoWABA phone ID to use. Falls back to the template's, then your account's first.
scheduled_atstringNoISO 8601 send time. If omitted, the campaign is created as a draft.
scheduled_timezonestringNoIANA timezone string (e.g., Asia/Kolkata)
expiration_time_msnumberConditionalRequired when the template is a limited-time offer (milliseconds)
carousel_dataarrayConditionalPer-card data for carousel templates — see below

contacts array items

FieldTypeRequiredDescription
phonestringYesPhone number with country code, with or without + (e.g., 911234567890 or +911234567890)
namestringNoContact name. Only used when creating a new contact.

Duplicate phone numbers (after normalization) are automatically deduplicated. Phone numbers that fail normalization are returned in the invalid_phones response field and skipped.

One entry per card, in card order. Must match the template's card count.

FieldTypeDescription
media_urlstringPublic image/video URL for this card (falls back to template's stored media if omitted)
body_dataobjectPer-card body variables (for carousel templates that use per-card bodies)
button_payloadsarrayPayloads for QUICK_REPLY buttons, in button order
url_button_paramsarray{{1}} substitutions for URL buttons, in button order

Examples

Immediate draft campaign

curl -X POST https://server.waplify.io/api/v1/campaigns/ \
-H "Authorization: Bearer wapl_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Summer Sale Blast",
"description": "Announce the summer sale to loyalty members",
"template_name": "summer_sale_promo",
"contacts": [
{ "phone": "911234567890", "name": "John Doe" },
{ "phone": "919876543210", "name": "Priya Singh" }
],
"body_data": { "1": "Loyalty member", "2": "40%" }
}'

Campaign with image header

curl -X POST https://server.waplify.io/api/v1/campaigns/ \
-H "Authorization: Bearer wapl_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Diwali Promo",
"template_name": "festive_banner",
"contacts": [{ "phone": "911234567890", "name": "John" }],
"body_data": { "1": "John", "2": "30%" },
"media_url": "https://example.com/diwali-banner.jpg"
}'

Scheduled campaign

curl -X POST https://server.waplify.io/api/v1/campaigns/ \
-H "Authorization: Bearer wapl_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Friday Digest",
"template_name": "weekly_digest",
"contacts": [{ "phone": "911234567890" }],
"scheduled_at": "2026-05-15T09:30:00Z",
"scheduled_timezone": "Asia/Kolkata"
}'

Scheduled campaigns run automatically at scheduled_at — you do not need to call Send Campaign.

curl -X POST https://server.waplify.io/api/v1/campaigns/ \
-H "Authorization: Bearer wapl_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"name": "Product Showcase",
"template_name": "product_showcase",
"contacts": [{ "phone": "911234567890", "name": "John" }],
"body_data": { "1": "John" },
"carousel_data": [
{
"media_url": "https://example.com/product1.jpg",
"url_button_params": ["p1"],
"button_payloads": ["more-like-this"]
},
{
"media_url": "https://example.com/product2.jpg",
"url_button_params": ["p2"],
"button_payloads": ["more-like-this"]
}
]
}'

Success response

{
"status": "success",
"message": "Campaign created successfully",
"campaign_id": "507f1f77bcf86cd799439060",
"name": "Summer Sale Blast",
"status_value": "draft",
"total_contacts": 2,
"invalid_phones": [],
"timestamp": "2026-06-15T10:00:00Z"
}
FieldDescription
campaign_idID used for Send Campaign and Campaign Stats
status_valuedraft (needs manual send) or scheduled (auto-sends at scheduled_at)
total_contactsNumber of unique contacts resolved from your input
invalid_phonesPhone numbers that failed normalization and were skipped

Error responses

Template not approved

// 400 Bad Request
{
"error": "bad_request",
"message": "Template 'summer_sale_promo' is not approved. Status: PENDING"
}

Template not found

// 404 Not Found
{
"error": "not_found",
"message": "Template 'unknown_template' not found or does not belong to your account"
}

Missing body variables

// 400 Bad Request
{
"error": "bad_request",
"message": "Missing body variables in 'body_data': 2"
}

Media required but not provided

// 400 Bad Request
{
"error": "bad_request",
"message": "media_url is required for this template"
}

Media URL unreachable

// 400 Bad Request
{
"error": "bad_request",
"message": "Media URL is not accessible: HTTP 403"
}

All phones invalid

// 400 Bad Request
{
"error": "bad_request",
"message": "No valid contacts could be resolved from the provided phone numbers"
}

Limited-time offer template missing expiration

// 400 Bad Request
{
"error": "bad_request",
"message": "expiration_time_ms is required for limited-time offer templates"
}

Limitations

  • ORDER_DETAILS and FLOW buttons are not supported via this endpoint. Use the dashboard for those templates.
  • Per-contact variable overrides are not supported — body_data is applied to every recipient. For personalized variables per contact, use the dashboard's CSV import flow.
  • Up to 10,000 contacts per request.