Skip to main content

Campaigns

Bulk template sends: create a campaign, upload recipients with per-recipient variables, launch it, and track delivery.

A campaign sends one approved template to many recipients, each with their own variable values, throttled so WhatsApp does not rate-limit you. It is the right tool for an order-status blast, a restock notice or a renewal reminder. For a single send, use POST /messages/template instead.

All endpoints are under https://api.callmissed.com/api/v1/whatsapp.

Lifecycle

  1. 1

    Create

    returns a campaign in

  2. 2

    Add recipients

    in batches of up to 10,000

  3. 3

    Launch

    prices the list, holds the credits, and starts the worker

  4. 4

    Track

    returns live counters and a recipient sample

Campaign statuses: draft, scheduled, running, completed, cancelled, failed. Recipient statuses: pending, sent, delivered, read, failed, skipped.

Recipients can only be added while the campaign is draft. Once it is running the worker is already claiming rows.

Create a campaign

POST /api/v1/whatsapp/campaigns · scope whatsapp:write

FieldTypeRequiredNotes
phone_number_idUUIDYesThe CallMissed phone id (the id from GET /phone_numbers), not Meta's id
namestring, 1 to 255 charsYesYour label for the campaign
template_namestring, 1 to 255 charsYesAn approved template's name
template_languagestring, 2 to 16 charsNoTemplate locale. Default en
template_componentsarray of objectsNoThe component shape, with {{N}} placeholders left in. Default empty
scheduled_atdatetimeNoWhen you intend to run it. Recorded on the row; launching is still an explicit call

template_components is a shape, not a finished payload. Leave the {{1}}, {{2}} tokens in the parameter text and the worker substitutes each recipient's variables before sending. Non-text parameters, such as a header image, are passed through untouched.

curl -X POST https://api.callmissed.com/api/v1/whatsapp/campaigns \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "phone_number_id": "9c2b7e30-1d8a-4c5f-9b3d-2f4a6e8b1c2d",
    "name": "April restock notice",
    "template_name": "back_in_stock",
    "template_language": "en_US",
    "template_components": [
      {
        "type": "body",
        "parameters": [
          { "type": "text", "text": "{{1}}" },
          { "type": "text", "text": "{{2}}" }
        ]
      }
    ]
  }'

Response (201 Created)

{
  "id": "6d1e8b3a-2c4f-4a5b-8e9d-0f1a2b3c4d5e",
  "account_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "phone_number_id": "9c2b7e30-1d8a-4c5f-9b3d-2f4a6e8b1c2d",
  "name": "April restock notice",
  "template_name": "back_in_stock",
  "template_language": "en_US",
  "status": "draft",
  "scheduled_at": null,
  "started_at": null,
  "completed_at": null,
  "total": 0,
  "sent": 0,
  "delivered": 0,
  "read": 0,
  "failed": 0,
  "created_at": "2026-04-19T12:00:00Z"
}
FieldTypeNotes
idUUIDThe campaign id
account_idUUIDThe WABA it sends from
phone_number_idUUIDThe sending number
statusstringCampaign status
scheduled_at / started_at / completed_atdatetime, nullableTimestamps, ISO 8601 UTC
totalintegerRecipients added
sent / delivered / read / failedintegerLive counters, updated by the worker and by delivery webhooks

404 with "phone_number_id not found" if the number is not on your workspace.

Add recipients

POST /api/v1/whatsapp/campaigns/{campaign_id}/recipients · scope whatsapp:write

Up to 10,000 per call. Paginate for larger lists.

FieldTypeRequiredNotes
recipientsarray, max 10000YesThe batch
recipients[].to_phonestring, 8 to 32 charsYesAny format. Non-digits are stripped, and the result must be 8 to 15 digits
recipients[].variablesobject of string to stringNoValues keyed by placeholder number, so {"1": "Priya"} fills {{1}}
curl -X POST https://api.callmissed.com/api/v1/whatsapp/campaigns/6d1e8b3a-2c4f-4a5b-8e9d-0f1a2b3c4d5e/recipients \
  -H "Authorization: Bearer cm_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "recipients": [
      { "to_phone": "+91 90000 00000", "variables": { "1": "Priya", "2": "Ethiopia Guji" } },
      { "to_phone": "919000000001", "variables": { "1": "Arun", "2": "Colombia Huila" } },
      { "to_phone": "12", "variables": { "1": "Broken" } }
    ]
  }'

Response (200 OK)

{
  "inserted": 2,
  "skipped_invalid": 1,
  "skipped_duplicate": 0,
  "total_now": 2
}
FieldTypeNotes
insertedintegerRecipients added
skipped_invalidintegerNumbers that were not 8 to 15 digits after stripping
skipped_duplicateintegerNumbers already on the campaign, or repeated inside the batch
total_nowintegerThe campaign's recipient total after this call

Bad rows are counted and skipped rather than failing the batch, so a 10,000-row paste with a few broken cells still lands the good ones. Adding to a campaign that is not draft returns 409 with Cannot add recipients to a campaign in status=running.

Launch

POST /api/v1/whatsapp/campaigns/{campaign_id}/launch · scope whatsapp:write

No body. Flips the campaign to running and starts the send worker.

curl -X POST https://api.callmissed.com/api/v1/whatsapp/campaigns/6d1e8b3a-2c4f-4a5b-8e9d-0f1a2b3c4d5e/launch \
  -H "Authorization: Bearer cm_your_api_key"

Returns the campaign object with status: "running" and started_at set.

The credit hold

Before anything is sent, the whole pending recipient list is priced against the real rate card, per recipient, using the template's category and each number's region. Those credits are then held, so a campaign launched a second later cannot spend them.

If the balance will not cover it the launch is refused with 402 and the campaign stays draft, retryable after a top-up. Nothing was sent and nothing was charged.

{
  "detail": "Not enough credits to launch this campaign. It needs about 8631.40 credits for 1200 recipients and you are short by 431.40. Top up your balance and try again."
}

Pricing varies by more than tenfold across markets, so a mixed India and Germany list is priced per recipient rather than at a blended rate. If the campaign's template has not been synced locally, it is priced as MARKETING, the most expensive category, so a campaign can never start underfunded.

Failures

CodeMeaning
400The campaign has no pending recipients to send to
402Not enough credits for the priced recipient list. The campaign stays draft
404No such campaign on your workspace
409The campaign is not draft or scheduled, for example it is already running

Concurrent launch calls are serialised, so a double-click cannot start two workers and double-send.

Cancel

POST /api/v1/whatsapp/campaigns/{campaign_id}/cancel · scope whatsapp:write

No body. Works from draft, scheduled or running. A running worker notices within one send, so a few in-flight messages may still go out.

curl -X POST https://api.callmissed.com/api/v1/whatsapp/campaigns/6d1e8b3a-2c4f-4a5b-8e9d-0f1a2b3c4d5e/cancel \
  -H "Authorization: Bearer cm_your_api_key"

Returns the campaign object with status: "cancelled" and completed_at set. 409 from any other status, for example one already completed.

List campaigns

GET /api/v1/whatsapp/campaigns · scope whatsapp:read

ParamTypeDefaultNotes
limitinteger, 1 to 10050Max rows, newest first
curl "https://api.callmissed.com/api/v1/whatsapp/campaigns?limit=50" \
  -H "Authorization: Bearer cm_your_api_key"

Returns an array of campaign objects.

Get one campaign

GET /api/v1/whatsapp/campaigns/{campaign_id} · scope whatsapp:read

The campaign object plus a sample of up to 50 recent recipient rows, most recently updated first. This is the progress endpoint: poll it while a campaign runs.

curl https://api.callmissed.com/api/v1/whatsapp/campaigns/6d1e8b3a-2c4f-4a5b-8e9d-0f1a2b3c4d5e \
  -H "Authorization: Bearer cm_your_api_key"

Response (200 OK)

{
  "id": "6d1e8b3a-2c4f-4a5b-8e9d-0f1a2b3c4d5e",
  "account_id": "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "phone_number_id": "9c2b7e30-1d8a-4c5f-9b3d-2f4a6e8b1c2d",
  "name": "April restock notice",
  "template_name": "back_in_stock",
  "template_language": "en_US",
  "status": "running",
  "scheduled_at": null,
  "started_at": "2026-04-19T12:05:02Z",
  "completed_at": null,
  "total": 1200,
  "sent": 418,
  "delivered": 402,
  "read": 191,
  "failed": 3,
  "created_at": "2026-04-19T12:00:00Z",
  "recipients_sample": [
    {
      "id": "aa11bb22-cc33-4d44-8e55-6f7788990011",
      "to_phone": "919000000000",
      "status": "delivered",
      "wamid": "wamid.HBgMOTE5MDAwMDAwMDAwFQIAERgSN0MyRDFBOEY0RTVCOTAxMgA=",
      "error": null,
      "sent_at": "2026-04-19T12:05:44Z",
      "last_status_at": "2026-04-19T12:05:51Z"
    },
    {
      "id": "bb22cc33-dd44-4e55-9f66-7788990011aa",
      "to_phone": "919000000002",
      "status": "failed",
      "wamid": null,
      "error": "Request rejected by Meta - check the recipient and payload.",
      "sent_at": null,
      "last_status_at": "2026-04-19T12:05:47Z"
    }
  ]
}
FieldTypeNotes
recipients_sample[].idUUIDRecipient row id
recipients_sample[].to_phonestringNormalised to digits only
recipients_sample[].statusstringpending, sent, delivered, read, failed or skipped
recipients_sample[].wamidstring, nullableMeta's message id once sent
recipients_sample[].errorstring, nullableWhy this recipient failed
recipients_sample[].sent_atdatetime, nullableWhen the send left
recipients_sample[].last_status_atdatetimeLast status change

The sample is capped at 50 rows and is not paginated. Use the counters on the campaign itself for totals.