Skip to content

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-js

Configuration ​

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.

ParameterTypeRequiredDescription
phonestringYesRecipient phone number with country code (e.g., +966501234567)
namestringYesTemplate name (e.g., order_confirmation)
languageCodestringYesLanguage code matching the approved template (e.g., ar, en_US)
paramsstring[]NoParameters mapping to 1, 2, etc. in the template body
options.scheduledAtstringNoISO 8601 datetime for scheduled delivery (e.g., 2026-05-01T10:00:00)
options.timezonestringNoIANA timezone (e.g., Asia/Riyadh). Required when scheduledAt is set
options.whatsappAccountIdstringNoOverride the default WhatsApp account. Useful when managing multiple numbers
options.autoRetryOnFrequencyCapbooleanNoOpt-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() ​

ParameterTypeRequiredDescription
messageLogIdnumberYesThe 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()      // false

TIP

For real-time updates, configure a webhook to receive message.status_updated events instead of polling.

List Templates ​

getTemplates() ​

ParameterTypeRequiredDescription
options.statusstringNoFilter by approval status. Use APPROVED to fetch only sendable templates
options.whatsappAccountIdstringNoOverride 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.

ParameterTypeRequiredDescription
messageTypestringYesMust be template
templateNamestringYesTemplate name (e.g., order_confirmation)
templateLanguagestringYesLanguage code matching the approved template (e.g., ar, en_US)
recipientsarrayYesList of recipients. Max 50,000 per call
recipients[].phonestringYesRecipient phone number with country code
recipients[].namestringNoRecipient display name
recipients[].variablesobjectNoPer-recipient template variables (e.g., { '1': 'Ahmed', '2': 'ORD-1234' })
campaignNamestringNoHuman-readable campaign name
scheduledAtstringNoISO 8601 datetime for scheduled delivery
timezonestringNoIANA timezone. Required when scheduledAt is set
splitAcrossDaysbooleanNoDivide a list larger than the number's remaining daily allowance into parts instead of throwing MESSAGING_TIER_LIMIT_EXCEEDED. Defaults to false.
whatsappAccountIdstringNoOverride 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()  // true

totalCount 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:00Z

Those 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() ​

ParameterTypeRequiredDescription
campaignIdstringYesThe 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() // true

cancelCampaign() ​

ParameterTypeRequiredDescription
campaignIdstringYesThe campaignId of a pending campaign to cancel
javascript
const result = await cube.cancelCampaign(campaignId)
result.success // true

Opt-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() ​

ParameterTypeRequiredDescription
phonestringYesRecipient phone in international format
scope'marketing' | 'all'Nomarketing (default) suppresses promotional only; all is a full block
javascript
await cube.addOptOut('+966501234567')            // marketing opt-out
await cube.addOptOut('+966501234567', 'all')     // full block

removeOptOut() ​

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' | null

listOptOuts() ​

OptionTypeDescription
scope'marketing' | 'all'Filter by scope
phonestringFilter by phone substring
perPagenumberResults 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():

PropertyTypeDescription
statusstringqueued for immediate, scheduled for deferred delivery
messageLogIdnumberUnique tracking ID
conversationCategorystringMARKETING, UTILITY, or AUTHENTICATION
costnumberMessage cost
scheduledAtstring|nullUTC datetime if scheduled, otherwise null
javascript
response.queued()    // true if status is "queued"
response.scheduled() // true if status is "scheduled"
response.toObject()  // Plain object representation

MessageStatusResponse ​

Returned by getMessageStatus():

PropertyTypeDescription
messageLogIdnumberUnique message ID
statusstringCurrent delivery status
toPhonestringRecipient phone number
messageTypestringtemplate
metaMessageIdstring|nullWhatsApp message ID (after sending)
sentAtstring|nullUTC datetime when sent
scheduledAtstring|nullScheduled UTC datetime
costAmountnumberMessage cost
costCurrencystringCurrency code
errorMessagestring|nullError details if failed
createdAtstringLog 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 representation

CampaignResponse ​

Returned by createCampaign() and getCampaign():

PropertyTypeDescription
campaignIdstringUnique campaign ULID
namestring|nullCampaign name
statusstringpending, processing, completed, cancelled, failed
totalCountnumberTotal recipients
sentCountnumberSuccessfully sent
failedCountnumberFailed deliveries
scheduledAtstring|nullScheduled UTC datetime
createdAtstringCreation 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 representation

Error Reference ​

HTTPError CodeCause
401AUTHENTICATION_REQUIREDNo API key provided in the request
401INVALID_API_KEYAPI key is invalid or has been revoked
403FORBIDDENAPI key does not have permission for this action
403API_KEY_NO_TENANTAPI key is not linked to any account
404NOT_FOUNDThe requested resource does not exist
404TEMPLATE_NOT_FOUNDTemplate name not found in your account
422VALIDATION_ERRORRequest failed input validation — check error.details for field-level errors
422INVALID_PHONE_NUMBERPhone number is not in a valid international format
422NO_ACTIVE_ACCOUNTNo connected WhatsApp number found for the given whatsapp_account_id
422MISSING_ACCESS_TOKENThe selected WhatsApp number has no Meta access token configured
422TEMPLATE_LANGUAGE_MISMATCHLanguage code does not match any approved version of this template
422TEMPLATE_PARAMS_MISMATCHFewer parameters provided than the template requires
429RATE_LIMIT_EXCEEDEDToo many API requests — apply exponential backoff and retry
429PLAN_LIMIT_REACHEDMonthly message quota reached — upgrade your plan
429SUBSCRIPTION_EXPIREDSubscription has expired
500MESSAGE_SEND_FAILEDWhatsApp API rejected or failed to deliver the message
500INTERNAL_ERRORUnexpected server error — contact support if this persists
503SERVICE_DEGRADEDOne 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
    }
  }
}

Source Code ​

GitHub: github.com/CubeSoftLabs/cube-connect-sdk-js

CubeConnect WhatsApp Business Platform