Authentication
Every request must carry your API key and API secret. Create them in the portal under API & Plugins → Generate API credentials; you can view them again at any time with View Key & Secret.
Option A — request headers (recommended)
X-Api-Key: cw_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx X-Api-Secret: yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy
Option B — HTTP Basic auth
Authorization: Basic base64(API_KEY:API_SECRET)
Option C — query / form fields
For quick tests you may pass api_key and api_secret as query-string or form parameters. Prefer headers in production so the secret never appears in URLs or logs.
Quickstart
- Generate credentialsOpen API & Plugins in the portal and click Generate API credentials. Copy the key and the secret.
- Check your balanceCall
GET /api/balanceto confirm the credentials work and you have funds. - Send a testPOST to
/api/sendwith a single number you control. - Check deliveryCall
GET /api/history?campaign_id=…using thecampaign_idreturned by the send.
curl -X POST 'https://sms.cloudwavebd.com/api/send' \
-H 'Content-Type: application/json' \
-H 'X-Api-Key: cw_YOUR_API_KEY' \
-H 'X-Api-Secret: YOUR_API_SECRET' \
-d '{ "message": "Hello from the API!", "to": "01712345678" }'
Endpoints
| Method | Path | Purpose |
|---|---|---|
| GET | /api/balance |
Current balance and currency. |
| GET | /api/senders |
List the Sender IDs you may send from, with per-part rates. |
| POST | /api/send |
Send one message to 1–500 Bangladeshi numbers. |
| GET | /api/history |
Your campaigns and per-message delivery status. |
All paths are relative to the base URL https://sms.cloudwavebd.com. Every response includes "ok": true|false.
GET /api/balance
Returns your current wallet balance. Use it to show credit in your own dashboard or to pre-check before a large send.
Response
{
"ok": true,
"balance": 123.45,
"currency": "BDT",
"client": { "id": 3, "email": "client@example.com" }
}
| Field | Type | Description |
|---|---|---|
balance | number | Wallet balance in BDT (4-decimal precision). |
currency | string | Always BDT. |
client.id | integer | Your SMS-portal client id. |
client.email | string | The account email. |
GET /api/senders
Lists every Sender ID the account may send from, with its provider and per-part price. Use these sender_id values when sending, or omit it to use the default.
Response
{
"ok": true,
"default_sender": "8809617611036",
"senders": [
{ "sender_id": "8809617611036", "provider": "bulksmsbd", "rate_bdt": 0.35, "unicode": true }
]
}
| Field | Type | Description |
|---|---|---|
senders[].sender_id | string | The Sender ID (mask) the message appears from. |
senders[].provider | string | Upstream gateway key from config/providers.php. |
senders[].rate_bdt | number | Price per SMS part in BDT, resolved for your account. |
senders[].unicode | boolean | true when Bangla (Unicode) messages are accepted on this Sender ID. |
POST /api/send
Sends one message to one or many Bangladeshi numbers. The wallet is debited per SMS part before the message is handed to the gateway.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
message | string | yes | The SMS text (UTF-8; Bangla supported). Max 4000 characters. |
to | string | string[] | yes | A single number or an array. Accepted forms: 01712345678, 8801712345678, +8801712345678. Invalid numbers are skipped. |
sender_id | string | no | Any Sender ID from GET /api/senders. Omit to use the account default. |
campaign | string | no | Label shown in History and in the portal. |
allow_duplicates | boolean | no | Default false. When false a repeated number is sent once; when true every entry is sent and billed. |
Example request
{
"message": "Your SMS text — Bangla is supported",
"to": ["01712345678", "8801812345678"],
"sender_id": "8809617611036", // optional
"campaign": "Campaign name", // optional
"allow_duplicates": false // optional — allow the same number more than once
}
Send it
curl -X POST 'https://sms.cloudwavebd.com/api/send' \
-H 'Content-Type: application/json' \
-H 'X-Api-Key: cw_YOUR_API_KEY' \
-H 'X-Api-Secret: YOUR_API_SECRET' \
-d '{
"message": "Hello from your API!",
"to": ["01712345678"]
}'
<?php
$ch = curl_init("https://sms.cloudwavebd.com/api/send");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-Api-Key: cw_YOUR_API_KEY",
"X-Api-Secret: YOUR_API_SECRET",
],
CURLOPT_POSTFIELDS => json_encode([
"message" => "Hello from PHP!",
"to" => ["01712345678"],
]),
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
import requests
resp = requests.post(
"https://sms.cloudwavebd.com/api/send",
headers={"X-Api-Key": "cw_YOUR_API_KEY", "X-Api-Secret": "YOUR_API_SECRET"},
json={"message": "Hello from Python!", "to": ["01712345678"]},
)
print(resp.json())
Response — 200 OK
{
"ok": true,
"campaign_id": 42,
"provider": "bulksmsbd",
"sender_id": "8809617611036",
"recipients": 2,
"parts": 1,
"encoding": "text",
"total_cost": 0.7,
"balance_after": 122.75,
"sent": 2,
"failed": 0,
"skipped_invalid": []
}
| Field | Type | Description |
|---|---|---|
campaign_id | integer | Use with GET /api/history?campaign_id=…. |
provider | string | Gateway that carried the message. |
sender_id | string | Sender ID actually used. |
recipients | integer | Recipients accepted after de-duping / validation. |
parts | integer | SMS parts per recipient (billing unit). |
encoding | string | text (GSM-7) or unicode (Bangla). |
total_cost | number | BDT debited (parts × rate × recipients). |
balance_after | number | Wallet balance after the debit. |
sent / failed | integer | Dispatch outcome for this call; leftovers are retried by the worker. |
skipped_invalid | string[] | Numbers that were not valid BD mobiles (first 20). |
Response — 202 Accepted (held for approval)
When a request targets more recipients than the account's approval threshold (default 100, set by the operator) it is held for admin approval. The cost is reserved from the wallet and nothing is sent until an operator approves it; a rejection refunds the reservation.
{
"ok": true,
"status": "pending_approval",
"campaign_id": 43,
"recipients": 250,
"parts": 1,
"total_cost": 87.5,
"balance_after": 35.25,
"message": "This campaign exceeds the approval threshold and is held for admin approval. …"
}
Errors
Errors use standard HTTP status codes and a consistent JSON shape:
{ "ok": false, "error": "unauthorized", "message": "Invalid API key or secret." }
| HTTP | error | Meaning | How to fix |
|---|---|---|---|
| 401 | unauthorized | Missing or invalid credentials. | Check the key/secret — they are case-sensitive. |
| 402 | send_failed | Insufficient wallet balance. | Top up in the portal (Add Funds). |
| 403 | suspended | API account suspended. | Contact the operator. |
| 404 | not_found | Campaign not found on your account. | Check campaign_id. |
| 422 | invalid_message | message missing/empty. | Send a non-empty message. |
| 422 | invalid_recipients | No valid BD numbers in to. | Use 017… / 8801… / +8801…. |
| 422 | invalid_sender_id | Unknown Sender ID. | Use a value from GET /api/senders. |
| 422 | too_many_recipients | Over the 500-recipient cap. | Split into batches. |
| 500 | server_error | Server-side problem. | Retry; if it persists, contact support. |
| 503 | no_sender | No Sender ID configured. | Contact the operator. |
GET /api/history
Without parameters: your recent campaigns, newest first. With campaign_id: that campaign plus every message and its delivery status.
Query parameters
| Param | Type | Description |
|---|---|---|
campaign_id | integer | Return one campaign with its message list (paginated). |
status | string | Filter by status: queued, sending, sent, delivered, partial, failed. |
page | integer | Page number, default 1. |
per_page | integer | Items per page, default 25, max 100. |
Response — campaign list
{
"ok": true,
"campaigns": [
{ "id": 42, "name": "Eid offer", "sender_id": "8809617611036", "provider": "bulksmsbd",
"recipients": 2, "parts": 1, "total_cost": 0.7, "status": "sent",
"created_at": "2026-09-28 12:30:11" }
],
"page": 1, "per_page": 25, "total": 1
}
Response — single campaign
{
"ok": true,
"campaign": { "id": 42, "name": "Eid offer", "status": "sent", "total_cost": 0.7, "recipients": 2, "parts": 1 },
"messages": [
{ "id": 501, "recipient": "8801712345678", "status": "sent", "attempts": 1,
"error": null, "parts": 1, "amount": 0.35, "sent_at": "2026-09-28 12:30:12" }
],
"page": 1, "per_page": 25, "total": 2
}
Billing & message parts
An SMS is billed per part. Long messages and Bangla (Unicode) messages split into more parts, so the cost is parts × rate_per_part × recipients. The rate per part comes from GET /api/senders (your account may have custom rates).
| Encoding | When it is used | 1 part | Each extra part |
|---|---|---|---|
GSM-7 (text) | Message is pure GSM-7 (English/Latin) | 160 chars | 153 chars |
Unicode (unicode) | Message contains Bangla or any non-GSM-7 character | 70 chars | 67 chars |
402 send_failed and nothing is sent.Delivery statuses
| Status | Meaning |
|---|---|
| queued | Accepted and waiting for dispatch (the worker retries these). |
| sending | Currently being handed to the gateway. |
| sent | Accepted by the gateway. |
| delivered | Confirmed delivered (when the provider's delivery report is wired). |
| failed | Rejected / not accepted. The error field explains why. |
| pending_approval | Held for operator approval (large campaign). |
Admin approval for large sends
Sends above the account's threshold (default 100 recipients) are queued as pending_approval. The full cost is reserved from the wallet immediately, but no message is transmitted until an operator approves it in the portal.
- You receive
202 Acceptedwithstatus: "pending_approval"and acampaign_id. - Poll
GET /api/history?campaign_id=…to watch it change toqueued → sent(approved) orrejected. - Approve dispatches the campaign; Reject refunds the entire reserved cost to the wallet.
- Split very large audiences into batches under the threshold if you need instant (non-approved) delivery.
Best practices
- Store credentials in environment variables or a secrets store — never in code or version control.
- Reuse one connection and set a sensible timeout (5–10s) on your HTTP client.
- Batch recipients up to 500 per request instead of one request per number.
- Handle
202 pending_approvalas success-with-review, not as an error. - De-duplicate your audience before sending unless duplicates are intentional.
- Poll history sparingly; the portal is the source of truth and delivery reports arrive asynchronously.
- Respect Bangladesh telemarketing / BTRC rules — send only to recipients who opted in.
Code samples
Send an SMS to two recipients. Replace the credentials and numbers.
curl -X POST 'https://sms.cloudwavebd.com/api/send' \
-H 'Content-Type: application/json' \
-H 'X-Api-Key: cw_YOUR_API_KEY' \
-H 'X-Api-Secret: YOUR_API_SECRET' \
-d '{
"message": "Hello from the API!",
"to": ["01712345678", "8801812345678"],
"campaign": "API test"
}'
<?php
$ch = curl_init("https://sms.cloudwavebd.com/api/send");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Content-Type: application/json",
"X-Api-Key: cw_YOUR_API_KEY",
"X-Api-Secret: YOUR_API_SECRET",
],
CURLOPT_POSTFIELDS => json_encode([
"message" => "Hello from PHP!",
"to" => ["01712345678"],
"campaign" => "PHP test",
]),
]);
$result = json_decode(curl_exec($ch), true);
curl_close($ch);
if (!empty($result["ok"])) {
echo "Queued campaign #" . $result["campaign_id"] . PHP_EOL;
} else {
echo "Error: " . ($result["message"] ?? "unknown") . PHP_EOL;
}
import requests
resp = requests.post(
"https://sms.cloudwavebd.com/api/send",
headers={"X-Api-Key": "cw_YOUR_API_KEY", "X-Api-Secret": "YOUR_API_SECRET"},
json={"message": "Hello from Python!", "to": ["01712345678"]},
timeout=10,
)
print(resp.json())
const res = await fetch("https://sms.cloudwavebd.com/api/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": "cw_YOUR_API_KEY",
"X-Api-Secret": "YOUR_API_SECRET",
},
body: JSON.stringify({ message: "Hello from Node!", to: ["01712345678"] }),
});
console.log(await res.json());
Plugins & integrations
Prefer a ready-made integration over writing your own client? Both plugins authenticate with the same API key and secret from this page and bill the same wallet.

WHMCS Module
Send SMS for orders, invoices and tickets, run bulk campaigns, and let clients top up their wallet by paying a WHMCS invoice — all from your WHMCS admin area.

WordPress / WooCommerce Plugin
Order receipts, order-status updates, new-user and comment alerts, and bulk SMS — straight from your WordPress or WooCommerce site.
FAQ
My message went to sent but the recipient says they didn't get it.
sent means the gateway accepted it. Actual handset delivery depends on the recipient's network; a delivery report (when available) later flips the status to delivered.
Can I send the same message to the same number twice?
Yes — set allow_duplicates: true. Otherwise duplicates are removed and billed once.
Why is my cost higher than recipients × rate?
Because the message split into multiple parts (long text, or Bangla/Unicode). See Billing & parts.
Is there a webhook for delivery reports?
Not yet — poll GET /api/history?campaign_id=…. Delivery-report sync is on the roadmap.
What is the rate limit?
Requests are limited by your wallet balance and the 500-recipient cap. Avoid more than a few concurrent sends; split large audiences into sequential batches.
CloudWave Bulk SMS · API v1 · Base URL https://sms.cloudwavebd.com · Last updated 23 Oct 2026.