Appearance
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:
| Tier | Daily limit |
|---|---|
TIER_50 | 50 conversations/day |
TIER_250 | 250 conversations/day — where an unverified business starts |
TIER_1K | 1,000 conversations/day |
TIER_2K | 2,000 conversations/day |
TIER_5K | 5,000 conversations/day |
TIER_10K | 10,000 conversations/day |
TIER_20K | 20,000 conversations/day |
TIER_50K | 50,000 conversations/day |
TIER_100K | 100,000 conversations/day |
TIER_UNLIMITED | No 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": trueand 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 adetailsobject showingdaily_limit,used_today,remainingandrequested, 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
| Method | Path | Description |
|---|---|---|
POST | /api/v1/campaigns | Create a bulk campaign |
GET | /api/v1/campaigns/{id} | Get campaign status and statistics |
GET | /api/v1/campaigns/{id}/recipients | List recipients with delivery status |
POST | /api/v1/campaigns/{id}/cancel | Cancel a scheduled campaign |
Create Campaign
POST /api/v1/campaigns
Headers
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer YOUR_API_KEY |
Content-Type | Yes | application/json |
Body Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
whatsapp_account_id | string | Yes | Sender account ID. Find it in Dashboard → WhatsApp Numbers → copy icon next to API ID: |
message_type | string | Yes | Must be template |
template_name | string | Yes | Template name (e.g., order_confirmation) — same name used in sendTemplate |
template_language | string | No | Template language code (e.g., ar, en_US). Defaults to the first matching template. |
template_params | array | No | Array of string parameters for template placeholders |
recipients | array | Yes | List of recipients (max 50,000) |
recipients[].phone | string | Yes | Recipient phone number with country code |
recipients[].name | string | No | Recipient display name |
recipients[].variables | object | No | Per-recipient template variables |
campaign_name | string | No | Human-readable campaign name (max 120 characters) |
scheduled_at | string | No | ISO 8601 datetime for scheduled delivery (e.g., 2026-05-01T09:00:00). Must be in the future. |
timezone | string | No | IANA timezone for scheduled_at (e.g., Asia/Riyadh). Defaults to UTC. |
split_across_days | boolean | No | Divide 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"
}
}| Field | Type | Description |
|---|---|---|
data.campaign_id | string | Unique campaign ULID |
data.name | string|null | Campaign name |
data.status | string | preparing — the recipient list is still being expanded |
data.requested_count | integer | Recipients you submitted. Final from this first response. |
data.total_count | integer | Deliverable recipients after opt-out filtering. 0 until preparation finishes. |
data.scheduled_at | string|null | UTC scheduled datetime, or null for immediate |
data.created_at | string | UTC 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
| Parameter | Description |
|---|---|
id | Campaign 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
| Status | Description |
|---|---|
preparing | Accepted; the recipient list is still being expanded. total_count is not final yet. |
pending | Recipients ready, waiting to start |
scheduled | Waiting for its scheduled time |
processing | Currently sending to recipients |
throttled | Paused on the number's daily limit. Resumes on its own when capacity frees up — no action needed. |
completed | All recipients processed |
cancelled | Cancelled, or stopped mid-send |
failed | Campaign 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
| Parameter | Description |
|---|---|
id | Campaign ULID returned from create |
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
page | integer | 1 | Page number |
per_page | integer | 50 | Results per page (max 100) |
status | string | — | 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
| Status | Description |
|---|---|
pending | Not yet processed |
sent | Message dispatched successfully |
failed | Delivery 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:
| Event | Trigger |
|---|---|
campaign.created | Campaign accepted |
campaign.started | Execution begins |
campaign.completed | All messages processed |