Skip to content

Campaigns ​

Send a pre-approved template message to a large list of recipients in a single API call. Campaigns support both immediate and scheduled delivery.

Template Required

WhatsApp requires a pre-approved template for bulk campaigns. Text messages can only be sent within a 24-hour window after the recipient last contacted you — which cannot be guaranteed for bulk lists. Always use "message_type": "template" for campaigns.

Daily Tier Limit

Meta enforces a daily limit per phone number on how many business-initiated conversations it may open. The limit rises as your number builds quality:

TierDaily limit
TIER_5050 conversations/day
TIER_250250 conversations/day — where an unverified business starts
TIER_1K1,000 conversations/day
TIER_2K2,000 conversations/day
TIER_5K5,000 conversations/day
TIER_10K10,000 conversations/day
TIER_20K20,000 conversations/day
TIER_50K50,000 conversations/day
TIER_100K100,000 conversations/day
TIER_UNLIMITEDNo limit

The window is a rolling 24 hours, not a calendar day — capacity frees up continuously rather than resetting at midnight.

If your recipient count exceeds the number's remaining capacity you have two options:

  • Let us split it. Send "split_across_days": true and the campaign is divided into parts sized to the limit, one per day. See Splitting a large campaign.
  • Handle it yourself. Without that flag the API returns MESSAGING_TIER_LIMIT_EXCEEDED (HTTP 429) with a details object showing daily_limit, used_today, remaining and requested, and you split the list in your own code.

The default is unchanged, so existing integrations that already split on the 429 keep working exactly as before.

Endpoints ​

MethodPathDescription
POST/api/v1/campaignsCreate a bulk campaign
GET/api/v1/campaigns/{id}Get campaign status and statistics
GET/api/v1/campaigns/{id}/recipientsList recipients with delivery status
POST/api/v1/campaigns/{id}/cancelCancel a scheduled campaign

Create Campaign ​

POST /api/v1/campaigns

Headers ​

HeaderRequiredDescription
AuthorizationYesBearer YOUR_API_KEY
Content-TypeYesapplication/json

Body Parameters ​

ParameterTypeRequiredDescription
whatsapp_account_idstringYesSender account ID. Find it in Dashboard → WhatsApp Numbers → copy icon next to API ID:
message_typestringYesMust be template
template_namestringYesTemplate name (e.g., order_confirmation) — same name used in sendTemplate
template_languagestringNoTemplate language code (e.g., ar, en_US). Defaults to the first matching template.
template_paramsarrayNoArray of string parameters for template placeholders
recipientsarrayYesList of recipients (max 50,000)
recipients[].phonestringYesRecipient phone number with country code
recipients[].namestringNoRecipient display name
recipients[].variablesobjectNoPer-recipient template variables
campaign_namestringNoHuman-readable campaign name (max 120 characters)
scheduled_atstringNoISO 8601 datetime for scheduled delivery (e.g., 2026-05-01T09:00:00). Must be in the future.
timezonestringNoIANA timezone for scheduled_at (e.g., Asia/Riyadh). Defaults to UTC.
split_across_daysbooleanNoDivide a list larger than the number's remaining daily allowance into parts instead of returning MESSAGING_TIER_LIMIT_EXCEEDED. Defaults to false.

Example Request ​

bash
curl -X POST https://cubeconnect.io/api/v1/campaigns \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "whatsapp_account_id": "01JX...",
    "message_type": "template",
    "template_name": "order_confirmation",
    "template_language": "ar",
    "recipients": [
      { "phone": "+966501234567", "name": "Ahmed", "variables": { "1": "Ahmed", "2": "ORD-1234", "3": "CUBE20" } },
      { "phone": "+966509876543", "name": "Sara",  "variables": { "1": "Sara",  "2": "ORD-5678", "3": "CUBE15" } }
    ],
    "campaign_name": "Offer Reminder",
    "scheduled_at": "2026-05-01T09:00:00",
    "timezone": "Asia/Riyadh"
  }'
php
$response = Http::withToken('YOUR_API_KEY')
    ->post('https://cubeconnect.io/api/v1/campaigns', [
        'whatsapp_account_id' => '01JX...',
        'message_type'        => 'template',
        'template_name'       => 'order_confirmation',
        'template_language'   => 'ar',
        'recipients'          => [
            ['phone' => '+966501234567', 'name' => 'Ahmed', 'variables' => ['1' => 'Ahmed', '2' => 'ORD-1234', '3' => 'CUBE20']],
            ['phone' => '+966509876543', 'name' => 'Sara',  'variables' => ['1' => 'Sara',  '2' => 'ORD-5678', '3' => 'CUBE15']],
        ],
        'campaign_name' => 'Offer Reminder',
        'scheduled_at'  => '2026-05-01T09:00:00',
        'timezone'      => 'Asia/Riyadh',
    ]);
javascript
const response = await fetch('https://cubeconnect.io/api/v1/campaigns', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    whatsapp_account_id: '01JX...',
    message_type: 'template',
    template_name: 'order_confirmation',
    template_language: 'ar',
    recipients: [
      { phone: '+966501234567', name: 'Ahmed', variables: { '1': 'Ahmed', '2': 'ORD-1234', '3': 'CUBE20' } },
      { phone: '+966509876543', name: 'Sara',  variables: { '1': 'Sara',  '2': 'ORD-5678', '3': 'CUBE15' } },
    ],
    campaign_name: 'Offer Reminder',
    scheduled_at: '2026-05-01T09:00:00',
    timezone: 'Asia/Riyadh',
  }),
})

Response 202 Accepted ​

json
{
  "success": true,
  "data": {
    "campaign_id": "01JX...",
    "name": "Offer Reminder",
    "status": "preparing",
    "message_type": "template",
    "requested_count": 2,
    "total_count": 0,
    "scheduled_at": "2026-05-01T06:00:00Z",
    "created_at": "2026-04-19T10:00:00Z"
  }
}
FieldTypeDescription
data.campaign_idstringUnique campaign ULID
data.namestring|nullCampaign name
data.statusstringpreparing — the recipient list is still being expanded
data.requested_countintegerRecipients you submitted. Final from this first response.
data.total_countintegerDeliverable recipients after opt-out filtering. 0 until preparation finishes.
data.scheduled_atstring|nullUTC scheduled datetime, or null for immediate
data.created_atstringUTC creation datetime

Why total_count is 0

The endpoint accepts your list and returns straight away, so the response time is the same for 100 recipients or 50,000. Recipient rows are written in the background, and contacts who opted out are removed then — which is why the deliverable total is not known yet.

Poll GET /api/v1/campaigns/{id} or subscribe to the campaign.created webhook for the final count.


Splitting a large campaign ​

Send "split_across_days": true and a list larger than the number's remaining daily allowance is divided into parts instead of rejected. Each part is a real campaign — it can be polled, cancelled and retried on its own — named Your name (1/4), (2/4) and so on.

bash
curl -X POST https://cubeconnect.io/api/v1/campaigns \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: national-day-2026" \
  -d '{
    "whatsapp_account_id": "01JX...",
    "message_type": "template",
    "template_name": "national_day_offer",
    "recipients": [ /* 2,500 recipients */ ],
    "campaign_name": "National Day",
    "split_across_days": true
  }'
json
{
  "success": true,
  "data": {
    "campaign_id": "01JXA...",
    "name": "National Day (1/3)",
    "status": "preparing",
    "requested_count": 1000,
    "total_count": 0,
    "campaign_group_id": "01JXG...",
    "parts_total": 3,
    "parts": [
      { "campaign_id": "01JXA...", "name": "National Day (1/3)", "part_number": 1, "status": "preparing", "requested_count": 1000, "scheduled_at": null },
      { "campaign_id": "01JXB...", "name": "National Day (2/3)", "part_number": 2, "status": "preparing", "requested_count": 1000, "scheduled_at": "2026-09-22T18:00:00Z" },
      { "campaign_id": "01JXC...", "name": "National Day (3/3)", "part_number": 3, "status": "preparing", "requested_count": 500,  "scheduled_at": "2026-09-23T18:00:00Z" }
    ]
  }
}

The first part goes out immediately (or at your scheduled_at); the rest follow a day apart. Top-level fields describe that first part, so a client written before this flag existed still reads a usable campaign_id.

The schedule is a plan, not a promise

Those dates are only an opening estimate. Every part rechecks the number's real remaining capacity when it runs, and a part is pulled forward as soon as there is room — so a campaign planned over twelve days finishes in two if your tier is raised on day one. A part that finds no capacity waits and resumes on its own, reported as throttled.

Do not split twice

If your own code already splits on MESSAGING_TIER_LIMIT_EXCEEDED, either keep doing that or set split_across_days — not both, or each of your batches is divided again. Removing your splitting logic and setting the flag is the simpler of the two.

Idempotency with a split. Each part stores your Idempotency-Key with a -p1, -p2 … suffix, and replaying the original key returns the whole group rather than building it again — so a timed-out create is safe to retry exactly as with a single campaign.


Get Campaign ​

GET /api/v1/campaigns/{id}

Path Parameters ​

ParameterDescription
idCampaign ULID returned from create

Example Request ​

bash
curl https://cubeconnect.io/api/v1/campaigns/01JX... \
  -H "Authorization: Bearer YOUR_API_KEY"
php
$response = Http::withToken('YOUR_API_KEY')
    ->get("https://cubeconnect.io/api/v1/campaigns/{$campaignId}");
javascript
const response = await fetch(`https://cubeconnect.io/api/v1/campaigns/${campaignId}`, {
  headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
})

Response 200 OK ​

json
{
  "success": true,
  "data": {
    "campaign_id": "01JX...",
    "name": "Offer Reminder",
    "status": "processing",
    "message_type": "template",
    "total_count": 500,
    "sent_count": 320,
    "failed_count": 12,
    "scheduled_at": null,
    "created_at": "2026-04-19T10:00:00Z"
  }
}

Campaign Status Values ​

StatusDescription
preparingAccepted; the recipient list is still being expanded. total_count is not final yet.
pendingRecipients ready, waiting to start
scheduledWaiting for its scheduled time
processingCurrently sending to recipients
throttledPaused on the number's daily limit. Resumes on its own when capacity frees up — no action needed.
completedAll recipients processed
cancelledCancelled, or stopped mid-send
failedCampaign encountered a fatal error; see failure_reason

Get Campaign Recipients ​

GET /api/v1/campaigns/{id}/recipients

Returns a paginated list of recipients with their individual delivery status. Use this to audit exactly which numbers were reached and which failed.

Path Parameters ​

ParameterDescription
idCampaign ULID returned from create

Query Parameters ​

ParameterTypeDefaultDescription
pageinteger1Page number
per_pageinteger50Results per page (max 100)
statusstring—Filter by status: pending, sent, or failed

Example Request ​

bash
curl "https://cubeconnect.io/api/v1/campaigns/01JX.../recipients?per_page=100&status=failed" \
  -H "Authorization: Bearer YOUR_API_KEY"
php
$page = $cube->getCampaignRecipients(
    campaignId: '01JX...',
    page: 1,
    perPage: 100,
    status: 'failed',
);

foreach ($page->recipients as $r) {
    echo "{$r->phone}: {$r->status} — {$r->errorMessage}\n";
}

if ($page->hasMorePages()) {
    $nextPage = $cube->getCampaignRecipients('01JX...', $page->currentPage + 1, 100, 'failed');
}
typescript
const page = await cube.getCampaignRecipients('01JX...', {
  page: 1,
  perPage: 100,
  status: 'failed',
})

for (const r of page.recipients) {
  console.log(`${r.phone}: ${r.status} — ${r.errorMessage}`)
}

if (page.pagination.currentPage < page.pagination.lastPage) {
  const next = await cube.getCampaignRecipients('01JX...', { page: 2, perPage: 100, status: 'failed' })
}

Response 200 OK ​

json
{
  "success": true,
  "data": {
    "campaign_id": "01JX...",
    "recipients": [
      {
        "phone": "966501234567",
        "name": "Ahmed",
        "status": "sent",
        "message_log_id": "01JY...",
        "error_message": null,
        "sent_at": "2026-04-19T10:05:32Z"
      },
      {
        "phone": "966509876543",
        "name": "Sara",
        "status": "failed",
        "message_log_id": null,
        "error_message": "Invalid phone number",
        "sent_at": null
      }
    ],
    "pagination": {
      "current_page": 1,
      "per_page": 50,
      "total": 1000,
      "last_page": 20
    }
  }
}

Recipient Status Values ​

StatusDescription
pendingNot yet processed
sentMessage dispatched successfully
failedDelivery failed — see error_message for reason

Cancel Campaign ​

POST /api/v1/campaigns/{id}/cancel

Cancels a scheduled campaign. Only campaigns in pending status can be cancelled.

Example Request ​

bash
curl -X POST https://cubeconnect.io/api/v1/campaigns/01JX.../cancel \
  -H "Authorization: Bearer YOUR_API_KEY"
php
$response = Http::withToken('YOUR_API_KEY')
    ->post("https://cubeconnect.io/api/v1/campaigns/{$campaignId}/cancel");
javascript
const response = await fetch(`https://cubeconnect.io/api/v1/campaigns/${campaignId}/cancel`, {
  method: 'POST',
  headers: { 'Authorization': 'Bearer YOUR_API_KEY' },
})

Response 200 OK ​

json
{
  "success": true,
  "data": {
    "success": true
  }
}

Finding Your Account ID ​

The whatsapp_account_id is the ULID identifier for your WhatsApp Business account on CubeConnect.

To find it: Go to Dashboard → WhatsApp Numbers and click the copy icon next to API ID: on any connected number.


Webhooks ​

Track campaign progress in real time using webhook events:

EventTrigger
campaign.createdCampaign accepted
campaign.startedExecution begins
campaign.completedAll messages processed

CubeConnect WhatsApp Business Platform