Appearance
JavaScript SDK
Official JavaScript/TypeScript package for integrating with the CubeConnect WhatsApp Business API.
Requirements
- Node.js 18+ or modern browser
- ES modules support
Installation
bash
npm install @cubesoftware/cube-connect-sdk-jsConfiguration
javascript
import { CubeConnect } from '@cubesoftware/cube-connect-sdk-js'
const cube = new CubeConnect({
apiKey: process.env.CUBECONNECT_API_KEY, // Settings → API
whatsappAccountId: process.env.CUBECONNECT_WHATSAPP_ACCOUNT_ID, // Dashboard → WhatsApp Numbers → API ID:
})Optional settings:
javascript
const cube = new CubeConnect({
apiKey: process.env.CUBECONNECT_API_KEY,
whatsappAccountId: process.env.CUBECONNECT_WHATSAPP_ACCOUNT_ID,
baseUrl: 'https://cubeconnect.io', // default
timeout: 30000, // 30 seconds (default)
})Multiple WhatsApp Numbers
If your account has more than one connected number, set the default in the constructor and override it per call using options.whatsappAccountId:
javascript
const cube = new CubeConnect({
apiKey: process.env.CUBECONNECT_API_KEY,
whatsappAccountId: '01JX_DEFAULT', // used when no override is passed
})
// Send from a different number
await cube.sendTemplate('+966501234567', 'offer_reminder', 'ar', ['50%'], {
whatsappAccountId: '01JX_MARKETING',
})
// List templates for a specific number
const templates = await cube.getTemplates({ whatsappAccountId: '01JX_MARKETING' })
// Create a campaign from a specific number
await cube.createCampaign({
messageType: 'template',
templateName: 'offer_reminder',
templateLanguage: 'ar',
recipients: [...],
whatsappAccountId: '01JX_MARKETING',
})Find each number's ID in Dashboard → WhatsApp Numbers → API ID:.
Sending Messages
sendTemplate()
Send a pre-approved template message to any number at any time.
| Parameter | Type | Required | Description |
|---|---|---|---|
phone | string | Yes | Recipient phone number with country code (e.g., +966501234567) |
name | string | Yes | Template name (e.g., order_confirmation) |
languageCode | string | Yes | Language code matching the approved template (e.g., ar, en_US) |
params | string[] | No | Parameters mapping to 1, 2, etc. in the template body |
options.scheduledAt | string | No | ISO 8601 datetime for scheduled delivery (e.g., 2026-05-01T10:00:00) |
options.timezone | string | No | IANA timezone (e.g., Asia/Riyadh). Required when scheduledAt is set |
options.whatsappAccountId | string | No | Override the default WhatsApp account. Useful when managing multiple numbers |
options.autoRetryOnFrequencyCap | boolean | No | Opt-in 26-hour auto-retry when Meta rejects with 131049 (per-recipient marketing cap). One retry per chain — the retry itself never auto-retries. Recommended true for marketing templates. |
javascript
const response = await cube.sendTemplate(
'+966501234567', // phone
'order_confirmation', // name
'ar', // languageCode
['ORD-1234', '500 SAR'], // params → {{1}}, {{2}}
)
response.status // "queued"
response.messageLogId // 4521
response.conversationCategory // "UTILITY"Without parameters:
javascript
const response = await cube.sendTemplate('+966501234567', 'welcome_message', 'ar')With 26h auto-retry on 131049 (per-recipient marketing cap):
javascript
const response = await cube.sendTemplate(
'+966501234567',
'eid_promo',
'ar',
[],
{ autoRetryOnFrequencyCap: true }, // landed today or 26h later — never lost
)TIP
The autoRetryOnFrequencyCap option is only useful for MARKETING templates — UTILITY and AUTHENTICATION templates aren't subject to the per-recipient marketing cap.
Scheduled delivery:
javascript
const response = await cube.sendTemplate(
'+966501234567',
'appointment_reminder',
'ar', // languageCode
['Dr. Ahmed', '10:00 AM'], // params
{
scheduledAt: '2026-05-01T09:00:00', // ISO 8601
timezone: 'Asia/Riyadh', // IANA timezone
},
)
response.status // "scheduled"
response.scheduledAt // "2026-05-01T06:00:00Z" (UTC)Override the sending number (when you manage multiple WhatsApp numbers):
javascript
const response = await cube.sendTemplate(
'+966501234567',
'order_confirmation',
'ar',
['ORD-1234'],
{ whatsappAccountId: 'account_id_2' }, // overrides the default
)Get Message Status
getMessageStatus()
| Parameter | Type | Required | Description |
|---|---|---|---|
messageLogId | number | Yes | The messageLogId returned when the message was sent |
javascript
const sent = await cube.sendTemplate('+966501234567', 'order_confirmation', 'ar', ['ORD-1234'])
const status = await cube.getMessageStatus(sent.messageLogId)
status.status // "delivered"
status.toPhone // "966501234567"
status.metaMessageId // "wamid.HBgN..."
status.sentAt // "2026-05-01T07:05:00Z"
status.isDelivered() // true
status.isRead() // falseTIP
For real-time updates, configure a webhook to receive message.status_updated events instead of polling.
List Templates
getTemplates()
| Parameter | Type | Required | Description |
|---|---|---|---|
options.status | string | No | Filter by approval status. Use APPROVED to fetch only sendable templates |
options.whatsappAccountId | string | No | Override the default WhatsApp account |
javascript
const templates = await cube.getTemplates({ status: 'APPROVED' })
templates.forEach(t => {
t.name // "order_confirmation"
t.language // "ar"
t.category // "UTILITY"
t.status // "APPROVED"
t.paramsCount // 3
t.body // "Hello {{1}}, your order {{2}} has been shipped."
t.header // null
})All templates (no filter):
javascript
const templates = await cube.getTemplates()Templates for a specific number:
javascript
const templates = await cube.getTemplates({ status: 'APPROVED', whatsappAccountId: 'account_id_2' })Bulk Campaigns
createCampaign()
Send a pre-approved template to a large list in a single API call. The whatsappAccountId set in the constructor is used automatically. Pass whatsappAccountId in the payload to override it.
| Parameter | Type | Required | Description |
|---|---|---|---|
messageType | string | Yes | Must be template |
templateName | string | Yes | Template name (e.g., order_confirmation) |
templateLanguage | string | Yes | Language code matching the approved template (e.g., ar, en_US) |
recipients | array | Yes | List of recipients. Max 50,000 per call |
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 (e.g., { '1': 'Ahmed', '2': 'ORD-1234' }) |
campaignName | string | No | Human-readable campaign name |
scheduledAt | string | No | ISO 8601 datetime for scheduled delivery |
timezone | string | No | IANA timezone. Required when scheduledAt is set |
splitAcrossDays | boolean | No | Divide a list larger than the number's remaining daily allowance into parts instead of throwing MESSAGING_TIER_LIMIT_EXCEEDED. Defaults to false. |
whatsappAccountId | string | No | Override the default WhatsApp account |
javascript
const campaign = await cube.createCampaign({
messageType: 'template',
templateName: 'order_confirmation', // same name used in sendTemplate()
templateLanguage: 'ar', // same languageCode used in sendTemplate()
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' } },
],
campaignName: 'Offer Reminder',
scheduledAt: '2026-05-01T09:00:00', // optional
timezone: 'Asia/Riyadh', // required when scheduledAt is set
})
campaign.campaignId // "01JX..."
campaign.status // "preparing" — the list is still being expanded
campaign.requestedCount // 2 — what you submitted, final immediately
campaign.totalCount // 0 until preparation finishes
campaign.isScheduled() // truetotalCount starts at 0
The API accepts the campaign and returns straight away, so the call costs the same for 100 recipients or 50,000. Recipient rows are written in the background and opted-out contacts are removed then — poll getCampaign() or subscribe to the campaign.created webhook for the final, filtered count.
Splitting a large campaign
A list larger than the number's remaining daily allowance normally throws MESSAGING_TIER_LIMIT_EXCEEDED. Set splitAcrossDays and it is divided into parts instead — each a campaign in its own right, named Your name (1/3), (2/3), and so on.
typescript
const campaign = await cube.createCampaign({
messageType: 'template',
templateName: 'national_day_offer',
templateLanguage: 'ar',
recipients: allContacts, // 2,500 on a 1,000/day number
campaignName: 'National Day',
splitAcrossDays: true,
})
campaign.isSplit() // true
campaign.partsTotal // 3
campaign.campaignId // the first part — the one going out now
for (const part of campaign.parts) {
console.log(`${part.name}: ${part.requestedCount} recipients, ${part.scheduledAt ?? 'sending now'}`)
}
// National Day (1/3): 1000 recipients, sending now
// National Day (2/3): 1000 recipients, 2026-09-22T18:00:00Z
// National Day (3/3): 500 recipients, 2026-09-23T18:00:00ZThose dates are an opening plan, not a promise: each part rechecks the real remaining capacity when it runs and is pulled forward as soon as there is room, so a campaign planned over twelve days finishes in two if your tier is raised.
Do not split twice
If your own code already splits on MESSAGING_TIER_LIMIT_EXCEEDED, either keep doing that or set splitAcrossDays — not both, or each of your batches gets divided again.
getCampaign()
| Parameter | Type | Required | Description |
|---|---|---|---|
campaignId | string | Yes | The campaignId returned from createCampaign() |
javascript
const campaign = await cube.getCampaign(campaignId)
campaign.status // "processing", "completed", "cancelled"
campaign.totalCount // 500
campaign.sentCount // 320
campaign.failedCount // 12
campaign.isCompleted() // truecancelCampaign()
| Parameter | Type | Required | Description |
|---|---|---|---|
campaignId | string | Yes | The campaignId of a pending campaign to cancel |
javascript
const result = await cube.cancelCampaign(campaignId)
result.success // trueOpt-out List
Manage the numbers that have unsubscribed or were blocked. Opt-outs are enforced automatically on every send — an opted-out recipient throws a ValidationError with code RECIPIENT_OPTED_OUT. See the Opt-out List API for full details.
addOptOut()
| Parameter | Type | Required | Description |
|---|---|---|---|
phone | string | Yes | Recipient phone in international format |
scope | 'marketing' | 'all' | No | marketing (default) suppresses promotional only; all is a full block |
javascript
await cube.addOptOut('+966501234567') // marketing opt-out
await cube.addOptOut('+966501234567', 'all') // full blockremoveOptOut()
Re-subscribe a number (remove it from the list).
javascript
const { removed } = await cube.removeOptOut('+966501234567')getOptOut()
Check whether a single number is opted out.
javascript
const status = await cube.getOptOut('+966501234567')
status.optedOut // true | false
status.scope // 'marketing' | 'all' | nulllistOptOuts()
| Option | Type | Description |
|---|---|---|
scope | 'marketing' | 'all' | Filter by scope |
phone | string | Filter by phone substring |
perPage | number | Results per page (max 100) |
javascript
const page = await cube.listOptOuts({ scope: 'marketing', perPage: 50 })
page.optOuts // OptOutRecord[]
page.pagination // { currentPage, perPage, total, lastPage }Health Check
javascript
const health = await cube.health()
if (health.status === 'healthy') {
// Platform is operational
}Response Objects
MessageResponse
Returned by sendTemplate():
| Property | Type | Description |
|---|---|---|
status | string | queued for immediate, scheduled for deferred delivery |
messageLogId | number | Unique tracking ID |
conversationCategory | string | MARKETING, UTILITY, or AUTHENTICATION |
cost | number | Message cost |
scheduledAt | string|null | UTC datetime if scheduled, otherwise null |
javascript
response.queued() // true if status is "queued"
response.scheduled() // true if status is "scheduled"
response.toObject() // Plain object representationMessageStatusResponse
Returned by getMessageStatus():
| Property | Type | Description |
|---|---|---|
messageLogId | number | Unique message ID |
status | string | Current delivery status |
toPhone | string | Recipient phone number |
messageType | string | template |
metaMessageId | string|null | WhatsApp message ID (after sending) |
sentAt | string|null | UTC datetime when sent |
scheduledAt | string|null | Scheduled UTC datetime |
costAmount | number | Message cost |
costCurrency | string | Currency code |
errorMessage | string|null | Error details if failed |
createdAt | string | Log creation timestamp |
javascript
status.isSent() // true if status is "sent"
status.isDelivered() // true if status is "delivered"
status.isRead() // true if status is "read"
status.isFailed() // true if status is "failed"
status.isScheduled() // true if status is "scheduled"
status.toObject() // Plain object representationCampaignResponse
Returned by createCampaign() and getCampaign():
| Property | Type | Description |
|---|---|---|
campaignId | string | Unique campaign ULID |
name | string|null | Campaign name |
status | string | pending, processing, completed, cancelled, failed |
totalCount | number | Total recipients |
sentCount | number | Successfully sent |
failedCount | number | Failed deliveries |
scheduledAt | string|null | Scheduled UTC datetime |
createdAt | string | Creation timestamp |
javascript
campaign.isScheduled() // true if pending with a scheduledAt
campaign.isCompleted() // true if status is "completed"
campaign.isCancelled() // true if status is "cancelled"
campaign.toObject() // Plain object representationError Reference
| HTTP | Error Code | Cause |
|---|---|---|
| 401 | AUTHENTICATION_REQUIRED | No API key provided in the request |
| 401 | INVALID_API_KEY | API key is invalid or has been revoked |
| 403 | FORBIDDEN | API key does not have permission for this action |
| 403 | API_KEY_NO_TENANT | API key is not linked to any account |
| 404 | NOT_FOUND | The requested resource does not exist |
| 404 | TEMPLATE_NOT_FOUND | Template name not found in your account |
| 422 | VALIDATION_ERROR | Request failed input validation — check error.details for field-level errors |
| 422 | INVALID_PHONE_NUMBER | Phone number is not in a valid international format |
| 422 | NO_ACTIVE_ACCOUNT | No connected WhatsApp number found for the given whatsapp_account_id |
| 422 | MISSING_ACCESS_TOKEN | The selected WhatsApp number has no Meta access token configured |
| 422 | TEMPLATE_LANGUAGE_MISMATCH | Language code does not match any approved version of this template |
| 422 | TEMPLATE_PARAMS_MISMATCH | Fewer parameters provided than the template requires |
| 429 | RATE_LIMIT_EXCEEDED | Too many API requests — apply exponential backoff and retry |
| 429 | PLAN_LIMIT_REACHED | Monthly message quota reached — upgrade your plan |
| 429 | SUBSCRIPTION_EXPIRED | Subscription has expired |
| 500 | MESSAGE_SEND_FAILED | WhatsApp API rejected or failed to deliver the message |
| 500 | INTERNAL_ERROR | Unexpected server error — contact support if this persists |
| 503 | SERVICE_DEGRADED | One or more platform services are temporarily unavailable |
Error Handling
All errors include an errorCode property matching the API error codes:
javascript
import {
CubeConnect,
AuthenticationError,
ValidationError,
RateLimitError,
NotFoundError,
CubeConnectError,
} from '@cubesoftware/cube-connect-sdk-js'
try {
await cube.sendTemplate('+966501234567', 'order_confirmation', 'ar', ['ORD-1234'])
} catch (e) {
if (e instanceof AuthenticationError) {
// 401/403 — Invalid API key or permissions
e.errorCode // "INVALID_API_KEY", "FORBIDDEN", ...
e.statusCode // 401 or 403
} else if (e instanceof ValidationError) {
// 422 — Invalid request data
e.errorCode // "VALIDATION_ERROR", "TEMPLATE_LANGUAGE_MISMATCH", ...
e.errors // { available_languages: ['ar'] }
} else if (e instanceof NotFoundError) {
// 404 — Resource not found
e.errorCode // "NOT_FOUND", "TEMPLATE_NOT_FOUND"
} else if (e instanceof RateLimitError) {
// 429 — Rate or plan limit exceeded
e.errorCode // "RATE_LIMIT_EXCEEDED", "PLAN_LIMIT_REACHED", ...
} else if (e instanceof CubeConnectError) {
// 5xx or network errors
e.errorCode // "INTERNAL_ERROR", "MESSAGE_SEND_FAILED", ...
e.statusCode
}
}TypeScript
Full type definitions are included:
typescript
import type {
CubeConnectOptions,
HealthResponse,
MessageResponseData,
MessageStatusResponseData,
SendOptions,
CreateCampaignPayload,
CampaignRecipient,
CampaignResponseData,
TemplateData,
} from '@cubesoftware/cube-connect-sdk-js'
import { CampaignResponse, MessageStatusResponse } from '@cubesoftware/cube-connect-sdk-js'Webhooks
Receive real-time notifications from CubeConnect for messages, campaigns, templates, chatbot flows, and quality events.
Signature Verification
javascript
import { verifyWebhookSignature } from '@cubesoftware/cube-connect-sdk-js'
app.post('/cubeconnect/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const isValid = verifyWebhookSignature({
payload: req.body.toString(),
signature: req.headers['x-webhook-signature'],
timestamp: req.headers['x-webhook-timestamp'],
secret: process.env.CUBECONNECT_WEBHOOK_SECRET,
})
if (!isValid) return res.status(401).send('Invalid signature')
// Process webhook...
res.sendStatus(200)
})Handling Events
javascript
import { WebhookEvent } from '@cubesoftware/cube-connect-sdk-js'
const event = new WebhookEvent(JSON.parse(body))
if (event.isMessageReceived()) {
console.log(`New message from ${event.get('from')}: ${event.get('content')}`)
}
if (event.isCampaignCompleted()) {
console.log(`Campaign "${event.get('name')}": ${event.get('sent_count')} sent`)
}
if (event.isTemplateStatusChanged()) {
console.log(`Template "${event.get('template_name')}" is now ${event.get('status')}`)
}See Webhook Events for full payload examples and Signature Verification for security details.
Real-World Examples
E-Commerce Order Confirmation
javascript
import { CubeConnect } from '@cubesoftware/cube-connect-sdk-js'
const cube = new CubeConnect({ apiKey: process.env.CUBECONNECT_API_KEY })
async function confirmOrder(order) {
await cube.sendTemplate(
order.customerPhone, // phone
'order_confirmation', // name
'ar', // languageCode
[order.reference, `${order.total} SAR`], // params
)
}OTP Verification
javascript
const code = String(Math.floor(Math.random() * 1000000)).padStart(6, '0')
await cube.sendTemplate(phone, 'verification_code', 'ar', [code])With Retry on Rate Limit
javascript
import { CubeConnect, RateLimitError } from '@cubesoftware/cube-connect-sdk-js'
const cube = new CubeConnect({ apiKey: process.env.CUBECONNECT_API_KEY })
async function sendWithRetry(phone, templateName, languageCode, params, retries = 3) {
for (let i = 0; i < retries; i++) {
try {
return await cube.sendTemplate(phone, templateName, languageCode, params)
} catch (e) {
if (e instanceof RateLimitError && i < retries - 1) {
await new Promise(r => setTimeout(r, 2000 * (i + 1)))
continue
}
throw e
}
}
}