cubesoftware / cube-connect-sdk-php
Official Laravel SDK for CubeConnect WhatsApp Business Platform
Package info
github.com/CubeSoftLabs/cube-connect-sdk-php
pkg:composer/cubesoftware/cube-connect-sdk-php
Requires
- php: ^8.1
- illuminate/http: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Official Laravel SDK for the CubeConnect WhatsApp Business Platform.
Installation
composer require cubesoftware/cube-connect-sdk-php
The package auto-discovers its service provider and facade. No manual registration required.
Publish Configuration
php artisan vendor:publish --tag=cubeconnect-config
Environment Variables
CUBECONNECT_API_KEY=your_api_key_here
CUBECONNECT_WHATSAPP_ACCOUNT_ID=your_account_id_here
| Variable | Default | Description |
|---|---|---|
CUBECONNECT_API_KEY |
— | Your API key — Settings → API in the dashboard |
CUBECONNECT_WHATSAPP_ACCOUNT_ID |
— | Your WhatsApp account ID — Dashboard → WhatsApp Numbers → API ID: |
CUBECONNECT_URL |
https://cubeconnect.io |
API base URL |
CUBECONNECT_TIMEOUT |
30 |
Request timeout in seconds |
CUBECONNECT_WEBHOOK_SECRET |
null |
Webhook signing secret for signature verification |
Multiple WhatsApp Numbers
If your account has more than one connected number, set a default in your .env and override it per call using the $whatsappAccountId parameter:
// Send from the default number (CUBECONNECT_WHATSAPP_ACCOUNT_ID) CubeConnect::sendTemplate('+966501234567', 'order_confirmation', 'ar', ['ORD-1234']); // Send from a different number CubeConnect::sendTemplate( '+966501234567', 'offer_reminder', 'ar', ['50%'], null, // $scheduledAt null, // $timezone '01JX_MARKETING', // $whatsappAccountId — override ); // List templates for a specific number $templates = CubeConnect::getTemplates('APPROVED', '01JX_MARKETING'); // Create a campaign from a specific number CubeConnect::createCampaign([ 'message_type' => 'template', 'template_name' => 'offer_reminder', 'template_language' => 'ar', 'recipients' => [...], 'whatsapp_account_id' => '01JX_MARKETING', // override ]);
Find each number's ID in Dashboard → WhatsApp Numbers → API ID:.
Usage
sendText()
| Parameter | Type | Required | Description |
|---|---|---|---|
$phone |
string | Yes | Recipient phone number with country code |
$body |
string | Yes | Message text (max 4096 characters) |
$scheduledAt |
string|null | No | ISO 8601 datetime for scheduled delivery |
$timezone |
string|null | No | IANA timezone. Required when $scheduledAt is set |
$whatsappAccountId |
string|null | No | Override the default WhatsApp account (useful with multiple numbers) |
use CubeConnect\Facades\CubeConnect; $response = CubeConnect::sendText('+966501234567', 'مرحباً بك في متجرنا!'); $response->status; // "queued" ->messageLogId; // "01JXXX..."
Scheduled delivery:
$response = CubeConnect::sendText( '+966501234567', 'تذكير: موعدك غداً الساعة 10 صباحاً.', '2026-05-01T09:00:00', // $scheduledAt (ISO 8601) 'Asia/Riyadh', // $timezone (IANA) ); $response->status; // "scheduled" $response->scheduledAt; // "2026-05-01T06:00:00Z" (UTC)
sendTemplate()
| Parameter | Type | Required | Description |
|---|---|---|---|
$phone |
string | Yes | Recipient phone number with country code |
$name |
string | Yes | Template name (e.g., order_confirmation) |
$languageCode |
string | Yes | Language code matching the approved template (e.g., ar, en_US) |
$params |
array | No | Parameters mapping to {{1}}, {{2}}, etc. |
$scheduledAt |
string|null | No | ISO 8601 datetime for scheduled delivery |
$timezone |
string|null | No | IANA timezone. Required when $scheduledAt is set |
$whatsappAccountId |
string|null | No | Override the default WhatsApp account (useful with multiple numbers) |
use CubeConnect\Facades\CubeConnect; $response = CubeConnect::sendTemplate( '+966501234567', // $phone 'order_confirmation', // $name 'ar', // $languageCode ['ORD-1234', '500 SAR'], // $params → {{1}}, {{2}} ); $response->status; // "queued" $response->messageLogId; // 4521 $response->conversationCategory; // "UTILITY" $response->queued(); // true
Without parameters:
$response = CubeConnect::sendTemplate('+966501234567', 'welcome_message', 'ar');
Scheduled delivery:
$response = CubeConnect::sendTemplate( '+966501234567', 'appointment_reminder', 'ar', // $languageCode ['Dr. Ahmed', '10:00 AM'], // $params '2026-05-01T09:00:00', // $scheduledAt (ISO 8601) 'Asia/Riyadh', // $timezone (IANA) ); $response->status; // "scheduled" $response->scheduledAt; // "2026-05-01T06:00:00Z" (UTC)
createCampaign()
Send a pre-approved template to a large list in a single API call.
| Parameter | Type | Required | Description |
|---|---|---|---|
message_type |
string | Yes | Must be template |
template_name |
string | Yes | Template name (same as $name in sendTemplate()) |
template_language |
string | Yes | Language code (same as $languageCode in sendTemplate()) |
recipients |
array | Yes | List of recipients. Max 50,000 |
recipients[].phone |
string | Yes | Recipient phone number |
recipients[].name |
string | No | Recipient display name |
recipients[].variables |
array | No | Per-recipient variables (e.g., ['1' => 'Ahmed', '2' => 'ORD-1234']) |
campaign_name |
string | No | Human-readable campaign name |
scheduled_at |
string | No | ISO 8601 datetime for scheduled delivery |
timezone |
string | No | IANA timezone. Required when scheduled_at is set |
whatsapp_account_id |
string | No | Override the default WhatsApp account |
$campaign = CubeConnect::createCampaign([ 'message_type' => 'template', 'template_name' => 'order_confirmation', 'template_language' => 'ar', 'recipients' => [ ['phone' => '+966501234567', 'name' => 'Ahmed', 'variables' => ['1' => 'Ahmed', '2' => 'ORD-1234']], ['phone' => '+966509876543', 'name' => 'Sara', 'variables' => ['1' => 'Sara', '2' => 'ORD-5678']], ], 'campaign_name' => 'Order Notifications', ]); $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
The API accepts the campaign and returns straight away, so the response time is
the same for 100 recipients or 50,000. Recipient rows are written in the
background, which is why totalCount is 0 at first — poll getCampaign() or
subscribe to the campaign.created webhook for the final, opt-out-filtered
count.
$campaign->isPreparing(); // true while the list is expanding $campaign->isFailed(); // preparation gave up — see $campaign->failureReason
Splitting a large campaign
A list larger than the number's remaining daily allowance normally raises
MESSAGING_TIER_LIMIT_EXCEEDED. Set split_across_days and it is divided into
parts instead — each a campaign in its own right, named Your name (1/3) and so on.
$campaign = CubeConnect::createCampaign([ 'message_type' => 'template', 'template_name' => 'national_day_offer', 'recipients' => $allContacts, // 2,500 on a 1,000/day number 'campaign_name' => 'National Day', 'split_across_days' => true, ]); $campaign->isSplit(); // true $campaign->partsTotal; // 3 $campaign->campaignId; // the first part — the one going out now foreach ($campaign->parts as $part) { echo "{$part->name}: {$part->requestedCount} recipients, " .($part->scheduledAt ?? 'sending now').PHP_EOL; }
The 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.
Do not split twice. If your own code already splits on
MESSAGING_TIER_LIMIT_EXCEEDED, either keep doing that or setsplit_across_days— not both, or each of your batches gets divided again.
Idempotency — retrying safely
Every create carries an Idempotency-Key. Leave it unset and the SDK derives one
from the payload, so re-issuing an identical call returns the original campaign
instead of creating a second one:
// A timeout does not mean the campaign was not created. Retry the same call — // the second attempt returns the first campaign rather than double-sending. try { $campaign = CubeConnect::createCampaign($payload); } catch (\CubeConnect\Exceptions\TimeoutException $e) { $campaign = CubeConnect::createCampaign($payload); // safe: same derived key }
Pass your own key to control the grouping — including when you deliberately want to send the very same campaign twice:
CubeConnect::createCampaign($payload, idempotencyKey: 'national-day-batch-1');
TimeoutException (REQUEST_TIMEOUT) is raised separately from
CubeConnectException (CONNECTION_FAILED) precisely because the two call for
different responses: a timeout may have succeeded on the server and should be
retried with the same key; a connection failure never reached it.
Scheduled delivery:
$campaign = CubeConnect::createCampaign([ 'message_type' => 'template', 'template_name' => 'offer_reminder', 'template_language' => 'ar', 'recipients' => [...], 'campaign_name' => 'Flash Sale', 'scheduled_at' => '2026-05-01T09:00:00', // ISO 8601 'timezone' => 'Asia/Riyadh', // IANA timezone ]); $campaign->status; // "pending" $campaign->isScheduled(); // true
Get Campaign Status
$campaign = CubeConnect::getCampaign($campaignId); $campaign->status; // "processing", "completed", "cancelled", "failed" $campaign->totalCount; // 500 $campaign->sentCount; // 320 $campaign->failedCount; // 12 $campaign->isCompleted(); // true
Cancel a Scheduled Campaign
$ok = CubeConnect::cancelCampaign($campaignId); // true on success
List Templates
$templates = CubeConnect::getTemplates('APPROVED'); foreach ($templates as $t) { $t->name; // "order_confirmation" $t->paramsCount; // 3 $t->body; // "Hello {{1}}, your order {{2}} has been shipped." $t->header; // null $t->isApproved(); // true }
Get Message Status
Retrieve the current delivery status of a previously sent message using the messageLogId returned by sendTemplate().
$msg = CubeConnect::getMessageStatus(4521); $msg->messageLogId; // 4521 $msg->status; // "delivered" $msg->toPhone; // "966501234567" $msg->messageType; // "template" $msg->metaMessageId; // "wamid.HBgN..." $msg->sentAt; // "2026-05-01T07:05:00Z" $msg->scheduledAt; // null $msg->costAmount; // 0.05 $msg->costCurrency; // "SAR" $msg->errorMessage; // null (set if status is "failed") $msg->isSent(); // true if status is "sent" $msg->isDelivered(); // true if status is "delivered" $msg->isRead(); // true if status is "read" $msg->isFailed(); // true if status is "failed" $msg->isScheduled(); // true if status is "scheduled"
Opt-out List
Manage numbers that unsubscribed or were blocked. Opt-outs are enforced on every send — messaging an opted-out recipient throws a ValidationException with code RECIPIENT_OPTED_OUT. Scope 'marketing' (default) suppresses promotional only; 'all' is a full block.
use CubeConnect\Facades\CubeConnect; // Add / remove CubeConnect::addOptOut('+966501234567'); // marketing opt-out CubeConnect::addOptOut('+966501234567', 'all'); // full block CubeConnect::removeOptOut('+966501234567'); // re-subscribe (returns bool) // Check one number $status = CubeConnect::getOptOut('+966501234567'); $status->optedOut; // true | false $status->scope; // 'marketing' | 'all' | null // List (filter by scope / phone) $page = CubeConnect::listOptOuts(scope: 'marketing', perPage: 50); foreach ($page->optOuts as $o) { $o->phone; // '966501234567' $o->scope; // 'marketing' }
Health Check
$health = CubeConnect::health(); // ['status' => 'healthy', 'checks' => [...], 'timestamp' => '...']
Webhooks
Receive real-time notifications from CubeConnect for messages, campaigns, templates, chatbot flows, and quality events.
Setup
CUBECONNECT_WEBHOOK_SECRET=your_webhook_secret_here
Signature Verification Middleware
// routes/api.php use CubeConnect\Webhooks\WebhookHandler; Route::post('/cubeconnect/webhook', [WebhookController::class, 'handle']) ->middleware(WebhookHandler::class);
Handling Webhook Events
use CubeConnect\DTOs\WebhookEvent; class WebhookController extends Controller { public function handle(Request $request) { $event = WebhookEvent::fromRequest($request); match (true) { $event->isMessageReceived() => $this->handleMessage($event), $event->isMessageStatusUpdated() => $this->handleStatus($event), $event->isCampaignCompleted() => $this->handleCampaign($event), $event->isTemplateStatusChanged() => $this->handleTemplate($event), $event->isFlowSessionCompleted() => $this->handleFlow($event), $event->isQualityEvent() => $this->handleQuality($event), default => null, }; return response('OK', 200); } }
Supported Events
| Event | Method | Description |
|---|---|---|
message.status_updated |
isMessageStatusUpdated() |
Message status change (sent, delivered, read, failed) |
message.received |
isMessageReceived() |
Incoming message from a customer |
campaign.created |
isCampaignCreated() |
New campaign created |
campaign.started |
isCampaignStarted() |
Campaign execution started |
campaign.completed |
isCampaignCompleted() |
Campaign finished |
template.submitted |
isTemplateSubmitted() |
Template submitted to Meta |
template.status_changed |
isTemplateStatusChanged() |
Template approved, rejected, or paused |
flow.session_started |
isFlowSessionStarted() |
Chatbot flow session started |
flow.session_completed |
isFlowSessionCompleted() |
Chatbot flow session completed |
flow.session_cancelled |
isFlowSessionCancelled() |
Session cancelled by customer |
account.quality_event |
isQualityEvent() |
Quality event (block or report) |
webhook.test |
isTest() |
Connection test ping |
Dependency Injection
use CubeConnect\Contracts\Messaging; class OrderController extends Controller { public function shipped(Order $order, Messaging $messaging) { $messaging->sendTemplate( $order->customer_phone, 'order_shipped', 'ar', [$order->id, $order->tracking_number], ); } }
Response Objects
MessageResponse
Returned by sendText() and sendTemplate():
| Property | Type | Description |
|---|---|---|
status |
string |
queued for immediate delivery, scheduled for future delivery |
messageLogId |
string |
Unique tracking ID |
conversationCategory |
string |
MARKETING, UTILITY, or AUTHENTICATION |
cost |
float |
Message cost |
scheduledAt |
string|null |
UTC datetime if scheduled, otherwise null |
$response->queued(); // true if status is "queued" $response->scheduled(); // true if status is "scheduled" $response->toArray(); // Array representation
CampaignResponse
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 |
int |
Total recipients |
sentCount |
int |
Successfully sent |
failedCount |
int |
Failed deliveries |
scheduledAt |
string|null |
Scheduled UTC datetime |
createdAt |
string |
Creation timestamp |
$campaign->isScheduled(); // true if pending with a scheduledAt $campaign->isCompleted(); // true if status is "completed" $campaign->isCancelled(); // true if status is "cancelled" $campaign->toArray(); // Array representation
MessageStatusResponse
Returned by getMessageStatus():
| Property | Type | Description |
|---|---|---|
messageLogId |
string |
Unique message log ID |
status |
string |
queued, scheduled, sent, delivered, read, or failed |
toPhone |
string |
Recipient phone number |
messageType |
string |
template or text |
metaMessageId |
string|null |
WhatsApp message ID (set after delivery) |
sentAt |
string|null |
UTC datetime when sent to WhatsApp |
scheduledAt |
string|null |
UTC datetime of scheduled delivery |
costAmount |
float |
Message cost |
costCurrency |
string |
Currency code (e.g., SAR) |
errorMessage |
string|null |
Error details if status is failed |
createdAt |
string |
UTC creation datetime |
$msg->isSent(); // true if status is "sent" $msg->isDelivered(); // true if status is "delivered" $msg->isRead(); // true if status is "read" $msg->isFailed(); // true if status is "failed" $msg->isScheduled(); // true if status is "scheduled" $msg->toArray(); // Array representation
Error 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
use CubeConnect\Facades\CubeConnect; use CubeConnect\Exceptions\AuthenticationException; use CubeConnect\Exceptions\ValidationException; use CubeConnect\Exceptions\RateLimitException; use CubeConnect\Exceptions\NotFoundException; use CubeConnect\Exceptions\CubeConnectException; try { CubeConnect::sendTemplate('+966501234567', 'order_confirmation', 'ar', ['ORD-1234']); } catch (AuthenticationException $e) { // 401/403 — Invalid API key or permissions $e->errorCode; // "INVALID_API_KEY", "FORBIDDEN", ... $e->statusCode; // 401 or 403 } catch (ValidationException $e) { // 422 — Invalid request data $e->errorCode; // "VALIDATION_ERROR", "INVALID_PHONE_NUMBER", ... $e->errors; // ['phone' => ['The phone field is required.']] } catch (NotFoundException $e) { // 404 — Resource not found $e->errorCode; // "NOT_FOUND", "TEMPLATE_NOT_FOUND" } catch (RateLimitException $e) { // 429 — Rate or plan limit exceeded $e->errorCode; // "RATE_LIMIT_EXCEEDED", "PLAN_LIMIT_REACHED", ... } catch (CubeConnectException $e) { // 5xx or network errors $e->errorCode; // "INTERNAL_ERROR", "MESSAGE_SEND_FAILED", ... $e->statusCode; }
Documentation
Full API documentation is available at docs.cubeconnect.io.
License
CubeConnect for Laravel is open-sourced software licensed under the MIT license.
Copyright © 2026 Cube Software (CubeSoftLabs). All rights reserved.