API Documentation
Send SMS at scale with a battle-tested REST API. 99.9% uptime, 60+ endpoints, 7 language SDKs, and real-time delivery reports.
#Quick Start
Create Account
Sign up at bulksmsrates.com and verify your email address. Free sandbox included.
Get API Keys
Dashboard → Settings → API Keys → Create key pair. You get a Key and Secret.
Send SMS
Use the code below. You'll have a message delivered in under 60 seconds.
curl -X POST https://app.bulksmsrates.com/v1/sms/send \
-H "Content-Type: application/json" \
-H "X-API-Key: $BULKSMS_API_KEY" \
-H "X-API-Secret: $BULKSMS_API_SECRET" \
-d '{
"to": "+447700900123",
"from": "MyApp",
"body": "Hello from BulkSMSRates!"
}'{
"id": "550e8400-e29b-41d4-a716-446655440000",
"messageId": "550e8400-e29b-41d4-a716-446655440000",
"status": "queued",
"segments": 1,
"cost_micros": 35000,
"cost": 0.035
}200+ countries
Global reach via tier-1 carriers
< 1s delivery
Average time-to-handset
Real-time DLRs
Webhook & WebSocket delivery receipts
#Authentication
All API requests require authentication via one of two methods:
🔑 API Key + Secret
Best for server-to-server. Pass both headers on every request.
X-API-Key: bsms_live_abc123...
X-API-Secret: sk_live_xyz789...- • Prefix
bsms_live_= production - • Prefix
bsms_test_= sandbox - • Secrets hashed Argon2id at rest
- • Rotate anytime in Settings → API Keys
🎫 Bearer JWT
For dashboard sessions and OAuth flows. Tokens issued after login.
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...- • Access token TTL: 15 minutes
- • Refresh token TTL: 7 days (rotated on use)
- • RS256 signed
- • Use
POST /auth/refreshto renew
| Method | Endpoint | Description |
|---|---|---|
| POST | /v1/auth/register | Create account + get tokens |
| POST | /v1/auth/login | Email + password login |
| POST | /v1/auth/refresh | Rotate refresh → new access token |
| GET | /v1/auth/me | Get current user profile |
| POST | /v1/auth/logout | Revoke refresh token (blacklist) |
| GET | /v1/auth/google/url | Get Google OAuth redirect URL |
| POST | /v1/auth/google | Exchange Google code for tokens |
| POST | /v1/auth/forgot-password | Send password reset email |
| POST | /v1/auth/reset-password | Reset password with token |
| POST | /v1/auth/change-password | Change password (authenticated) |
bsms_test_ keys) never sends real SMS.#Send SMS
Send single messages with rich options: template variables, scheduled delivery, custom sender IDs, DLR callbacks, and Unicode support.
| Method | Endpoint | Description |
|---|---|---|
| POST | /v1/sms/send | Send a single SMS message |
| GET | /v1/sms/{message_id} | Get message status + DLR events |
| GET | /v1/sms | List messages (paginated, filtered) |
Request Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| to | string | ✅ | E.164 destination (+447700900123) |
| from | string | ✅ | Sender ID (up to 11 alphanum chars, or number) |
| body | string | ✅ | Message text. Supports {{variables}} |
| variables | object | — | Substitution values for {{placeholders}} |
| client_ref | string | — | Your reference ID (echoed in DLR) |
| scheduled_at | string | — | ISO 8601 UTC send time (max 30 days ahead) |
| validity_minutes | integer | — | Message TTL (default 2880 = 48h) |
| unicode | boolean | — | Force Unicode encoding |
| template_id | uuid | — | Use a saved template |
curl -X POST https://app.bulksmsrates.com/v1/sms/send \
-H "Content-Type: application/json" \
-H "X-API-Key: $BULKSMS_API_KEY" \
-H "X-API-Secret: $BULKSMS_API_SECRET" \
-d '{
"to": "+447700900123",
"from": "MyApp",
"body": "Hello from BulkSMSRates!"
}'#Bulk SMSUp to 10,000 recipients
Send personalised messages to up to 10,000 recipients in a single API call. Each recipient can have unique variable substitutions. Batches are processed asynchronously — use webhooks or poll the batch status.
| Method | Endpoint | Description |
|---|---|---|
| POST | /v1/sms/bulk | Submit a bulk SMS batch |
| GET | /v1/campaigns/{id} | Track batch / campaign progress |
curl -X POST https://app.bulksmsrates.com/v1/sms/bulk \
-H "Content-Type: application/json" \
-H "X-API-Key: $BULKSMS_API_KEY" \
-H "X-API-Secret: $BULKSMS_API_SECRET" \
-d '{
"recipients": [
{ "to": "+447700900001", "variables": { "name": "Alice" } },
{ "to": "+447700900002", "variables": { "name": "Bob" } }
],
"from": "MyApp",
"body": "Hi {{name}}, your order is ready!",
"client_ref": "batch-001"
}'{
"data": {
"batch_id": "7f3e2a1c-...",
"accepted": 2,
"rejected": 0,
"cost_estimate": 700,
"currency": "GBP"
}
}💡 Template Variables
Per-recipient personalisation using {{variable}}:
{
"body": "Hi {{name}}, code: {{code}}",
"recipients": [
{ "to": "+447700900001",
"variables": { "name": "Alice", "code": "1234" } }
]
}📅 Scheduled Sending
Deliver at a specific time (up to 30 days ahead):
{
"recipients": [{ "to": "+447700900001" }],
"from": "MyApp",
"body": "Your appointment is tomorrow!",
"scheduled_at": "2026-03-01T09:00:00Z"
}#Campaigns
Campaigns give you a named, managed batch send with approval workflows, real-time progress tracking, and pause/resume control.
| Method | Endpoint | Description |
|---|---|---|
| POST | /v1/campaigns | Create a campaign (draft state) |
| GET | /v1/campaigns | List campaigns |
| GET | /v1/campaigns/{id} | Get campaign details + progress |
| POST | /v1/campaigns/{id}/recipients | Upload recipient list |
| POST | /v1/campaigns/{id}/submit | Submit for send (→ pending_approval) |
| POST | /v1/campaigns/{id}/pause | Pause in-progress campaign |
| POST | /v1/campaigns/{id}/resume | Resume paused campaign |
| POST | /v1/campaigns/{id}/cancel | Cancel campaign |
Campaign State Machine
↓ (rejected)
rejected ↓
cancelled
{
"name": "Spring Sale 2026",
"from": "MyShop",
"body": "Hi {{name}}, get 30% off this weekend: myshop.com/sale",
"schedule_at": "2026-03-15T08:00:00Z"
}#Contacts & Groups
Manage your contact database, groups, tags, and opt-outs. Import via CSV or API. Opt-outs are automatically enforced on send.
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/contacts | List contacts (paginated) |
| POST | /v1/contacts | Create a contact |
| PATCH | /v1/contacts/{id} | Update a contact |
| DELETE | /v1/contacts/{id} | Delete a contact |
| POST | /v1/contacts/{id}/tags | Add tag to contact |
| DELETE | /v1/contacts/{id}/tags/{tag_id} | Remove tag from contact |
| GET | /v1/contacts/groups | List groups |
| POST | /v1/contacts/groups | Create a group |
| DELETE | /v1/contacts/groups/{id} | Delete a group |
| POST | /v1/contacts/groups/{id}/import | Import CSV into group |
| GET | /v1/contacts/tags | List all tags |
| POST | /v1/contacts/tags | Create a tag |
| DELETE | /v1/contacts/tags/{id} | Delete a tag |
CSV Import Format
phone,name,email,custom_field
+447700900001,Alice Smith,[email protected],VIP
+447700900002,Bob Jones,[email protected],Standard422 OPT_OUT.#Billing & Balance
BulkSMSRates uses a prepaid wallet model. All monetary values are in integer pence/cents (350 = £3.50). Top up via Stripe checkout.
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/billing/balance | Current wallet balance + credit limit |
| GET | /v1/billing/transactions | Transaction history (paginated) |
| GET | /v1/billing/invoices | List invoices |
| GET | /v1/billing/invoices/{id} | Get invoice details |
| GET | /v1/billing/invoices/{id}/pdf | Download invoice PDF |
| POST | /v1/billing/checkout | Create Stripe checkout session |
| GET | /v1/billing/usage | Usage summary (current period) |
| GET | /v1/billing/usage/export | Export usage CSV |
| POST | /v1/billing/auto-topup | Configure auto top-up rules |
curl https://app.bulksmsrates.com/v1/billing/balance \
-H "X-API-Key: $BULKSMS_API_KEY" \
-H "X-API-Secret: $BULKSMS_API_SECRET"{
"data": {
"balance": 1250,
"credit_limit": 0,
"available": 1250,
"currency": "GBP",
"low_balance_threshold": 500,
"auto_topup_enabled": true
}
}#Reports & Analytics
Access message logs, delivery statistics, and DLR breakdowns. Data is available for the last 90 days. Reports endpoints are cached (60s for stats, 10s for message logs).
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/reports/messages | Full message log with filters |
| GET | /v1/reports/dlr-stats | Delivery rate stats (sent/delivered/failed) |
Query Parameters — Message Log
| Parameter | Type | Description |
|---|---|---|
| status | string | Filter by: queued, sent, delivered, failed, expired |
| from_date | date-time | Start of range (ISO 8601) |
| to_date | date-time | End of range (ISO 8601) |
| destination | string | Filter by E.164 number (exact match) |
| campaign_id | uuid | Filter by campaign |
| page_size | integer | Results per page (1–500, default 50) |
| cursor | string | Pagination cursor from previous response |
#WhatsApp BusinessBeta
Send WhatsApp messages using approved templates. Requires a connected WhatsApp Business account via Meta Cloud API. Template messages only in the first 24h outside an active conversation window.
| Method | Endpoint | Description |
|---|---|---|
| POST | /v1/whatsapp/send | Send a WhatsApp message |
| GET | /v1/whatsapp/templates | List approved message templates |
| POST | /v1/whatsapp/templates | Create a new template (pending Meta approval) |
| DELETE | /v1/whatsapp/templates/{name} | Delete a template |
| GET | /v1/whatsapp/conversations | List active 24h conversations |
| GET | /v1/whatsapp/accounts | List connected WhatsApp Business accounts |
| GET | /v1/whatsapp/webhook | Meta webhook hub verification |
| POST | /v1/whatsapp/webhook | Receive inbound WhatsApp events |
Send Template Message
{
"to": "+447700900123",
"type": "template",
"template": {
"name": "order_shipped",
"language": "en",
"components": [
{
"type": "body",
"parameters": [
{ "type": "text", "text": "Alice" },
{ "type": "text", "text": "ORD-12345" }
]
}
]
}
}#Webhooks
Configure endpoint URLs to receive real-time event notifications. All webhooks are signed with HMAC-SHA256 for authenticity verification.
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/settings/webhooks | List configured webhooks |
| POST | /v1/settings/webhooks | Create a webhook endpoint |
| PATCH | /v1/settings/webhooks/{id} | Update webhook URL or events |
| DELETE | /v1/settings/webhooks/{id} | Delete a webhook |
Webhook Events
dlrDelivery report (delivered/failed/expired)opt_outRecipient replied STOPcampaign.completedCampaign finished sendingcampaign.failedCampaign encountered errorsbalance.lowBalance below thresholdwhatsapp.messageInbound WhatsApp messageDLR Payload Example
{
"id": "01952db4-e29b-7f00-a716-446655440000",
"event": "message.delivered",
"created_at": "2026-02-18T12:00:03Z",
"data": {
"message_id": "550e8400-e29b-41d4-a716-446655440000",
"destination": "+447700900123",
"carrier_status": "DELIVRD",
"delivered_at": "2026-02-18T12:00:03Z"
}
}Signature Verification
Every webhook request includes these headers. Always verify before processing:
X-Webhook-Signature: sha256=<HMAC-SHA256(secret, timestamp + "." + body)>
X-Webhook-Id: wh_evt_abc123
X-Webhook-Timestamp: 1708257603# Webhook arrives as HTTP POST to your endpoint
# Verify headers:
# X-Webhook-Signature: sha256=<hmac>
# X-Webhook-Timestamp: 1708257603
# X-Webhook-Id: wh_evt_abc123
# Python verification example belowRetry Policy
- • 3 attempts with exponential backoff
- • Delays: 5s → 30s → 300s
- • After 3 failures → webhook marked inactive
- • You receive an email alert on failure
Best Practices
- • Respond with 200 within 10 seconds
- • Process asynchronously (queue + worker)
- • Store X-Webhook-Id to deduplicate retries
- • Reject timestamps older than 5 minutes
#Settings & API Keys
| Method | Endpoint | Description |
|---|---|---|
| GET | /v1/settings/profile | Get account profile |
| PATCH | /v1/settings/profile | Update profile (name, timezone, etc.) |
| GET | /v1/settings/api-keys | List API keys |
| POST | /v1/settings/api-keys | Create a new API key pair |
| DELETE | /v1/settings/api-keys/{id} | Revoke an API key |
| GET | /v1/settings/sender-ids | List sender IDs |
| POST | /v1/settings/sender-ids | Request a new sender ID |
| GET | /v1/settings/smpp | Get SMPP credentials |
| POST | /v1/settings/smpp/regenerate | Regenerate SMPP password |
| GET | /v1/settings/notifications | Get notification preferences |
| PATCH | /v1/settings/notifications | Update notification preferences |
Create API Key
POST /v1/settings/api-keys
{
"name": "Production Server",
"permissions": ["sms.send", "billing.read", "reports.read"]
}
// Response
{
"data": {
"id": "key_abc123",
"name": "Production Server",
"key": "bsms_live_abc123...",
"secret": "sk_live_xyz789...", // ⚠️ Only shown once!
"created_at": "2026-02-18T12:00:00Z"
}
}#Official SDKs & Postman
Download our official SDKs for zero-boilerplate integration. All SDKs include auth, send/bulk SMS, campaigns, contacts, billing, sandbox mode, rate-limit parsing, and auto-retry.
Postman Collection
All endpoints organised by folder with pre-request auto-auth scripts, environment variables, and example responses. Import directly into Postman or Insomnia.
🧪 Sandbox Mode
Test your integration without sending real messages. Add X-Sandbox: true to any API request. Messages return instantly with a fake ID and status delivered. No charges, no real sends.
# Enable sandbox on any request
curl -X POST https://app.bulksmsrates.com/api/v1/sms/send \
-H "X-API-Key: bsms_live_..." \
-H "X-API-Secret: sk_live_..." \
-H "X-Sandbox: true" \
-H "Content-Type: application/json" \
-d '{"destination": "+447700900123", "message": "Test — not sent!", "sender_id": "Test"}'
# Response:
# {"id":"sandbox-msg-a1b2c3d4","status":"delivered","sandbox":true}
# Reset sandbox state
curl https://app.bulksmsrates.com/api/v1/sandbox/reset \
-H "X-API-Key: ..." -H "X-API-Secret: ..." -H "X-Sandbox: true"Or use the raw REST examples below — copy-paste into any project.
🐍 Python — Complete Example
import requests
BASE = "https://app.bulksmsrates.com/v1"
HEADERS = {
"X-API-Key": "bsms_live_...",
"X-API-Secret": "sk_live_...",
}
# Send SMS
def send_sms(to, body, sender="MyApp"):
return requests.post(f"{BASE}/sms/send", headers=HEADERS,
json={"to": to, "from": sender, "body": body}).json()
# Send bulk SMS
def send_bulk(recipients, body, sender="MyApp"):
return requests.post(f"{BASE}/sms/bulk", headers=HEADERS,
json={"recipients": recipients, "from": sender, "body": body}).json()
# Check balance
def get_balance():
return requests.get(f"{BASE}/billing/balance", headers=HEADERS).json()
# List messages
def list_messages(status=None, page_size=50):
params = {"page_size": page_size}
if status: params["status"] = status
return requests.get(f"{BASE}/reports/messages", headers=HEADERS, params=params).json()
# Usage:
print(send_sms("+447700900123", "Hello!"))
print(get_balance()["data"]["balance"])
msgs = list_messages(status="delivered")🟨 Node.js — Complete Example
const BASE = "https://app.bulksmsrates.com/v1";
const HEADERS = {
"Content-Type": "application/json",
"X-API-Key": process.env.BULKSMS_API_KEY,
"X-API-Secret": process.env.BULKSMS_API_SECRET,
};
const api = {
// Send single SMS
async sendSms(to, body, from = "MyApp") {
const res = await fetch(`${BASE}/sms/send`, {
method: "POST", headers: HEADERS,
body: JSON.stringify({ to, from, body }),
});
return res.json();
},
// Send bulk SMS
async sendBulk(recipients, body, from = "MyApp") {
const res = await fetch(`${BASE}/sms/bulk`, {
method: "POST", headers: HEADERS,
body: JSON.stringify({ recipients, from, body }),
});
return res.json();
},
// Get balance
async getBalance() {
const res = await fetch(`${BASE}/billing/balance`, { headers: HEADERS });
return res.json();
},
// List messages
async listMessages(params = {}) {
const qs = new URLSearchParams(params).toString();
const res = await fetch(`${BASE}/reports/messages?${qs}`, { headers: HEADERS });
return res.json();
},
};
// Usage:
const result = await api.sendSms("+447700900123", "Hello!");
console.log(result.data.message_id);
const { data } = await api.getBalance();
console.log(`Balance: £${(data.balance / 100).toFixed(2)}`);🐘 PHP — Complete Example
<?php
class BulkSMSRates {
private string $base = 'https://app.bulksmsrates.com/v1';
private array $headers;
public function __construct(string $apiKey, string $apiSecret) {
$this->headers = [
'Content-Type: application/json',
"X-API-Key: {$apiKey}",
"X-API-Secret: {$apiSecret}",
];
}
private function request(string $method, string $path, array $data = []): array {
$ch = curl_init("{$this->base}{$path}");
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $this->headers,
CURLOPT_RETURNTRANSFER => true,
]);
if ($data && $method !== 'GET') {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
}
return json_decode(curl_exec($ch), true);
}
public function sendSms(string $to, string $body, string $from = 'MyApp'): array {
return $this->request('POST', '/sms/send', compact('to', 'from', 'body'));
}
public function sendBulk(array $recipients, string $body, string $from = 'MyApp'): array {
return $this->request('POST', '/sms/bulk', compact('recipients', 'from', 'body'));
}
public function getBalance(): array {
return $this->request('GET', '/billing/balance');
}
public function listMessages(array $params = []): array {
$qs = http_build_query($params);
return $this->request('GET', "/reports/messages?{$qs}");
}
}
// Usage:
$sms = new BulkSMSRates($_ENV['API_KEY'], $_ENV['API_SECRET']);
$result = $sms->sendSms('+447700900123', 'Hello!');
echo $result['data']['message_id'] . PHP_EOL;
$balance = $sms->getBalance()['data'];
echo "Balance: " . ($balance['balance'] / 100) . " " . $balance['currency'];#Rate Limits
Rate limiting uses a token bucket algorithm per API key. Default: 100 requests/second. Enterprise plans get higher limits.
| Header | Description |
|---|---|
| X-RateLimit-Limit | Max requests per second for your key |
| X-RateLimit-Remaining | Tokens remaining in current window |
| X-RateLimit-Reset | Unix timestamp when window resets |
| Retry-After | Seconds to wait (only on 429 responses) |
Exponential Backoff Pattern
async function withRetry(fn, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const res = await fn();
if (res.status !== 429) return res;
const retryAfter = parseInt(res.headers.get('Retry-After') ?? '1');
const jitter = Math.random() * 1000;
const delay = (retryAfter * 1000 + jitter) * Math.pow(2, attempt);
console.log(`Rate limited. Retrying in ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
}
throw new Error('Max retries exceeded');
}POST /sms/bulk endpoint accepts up to 10,000 recipients per call and counts as one API request — far more efficient than looping single sends.#Pagination
All list endpoints use cursor-based pagination for consistent results even as data changes. Never miss a record or see duplicates.
Query Parameters
| Parameter | Default | Max | Description |
|---|---|---|---|
| page_size | 50 | 500 | Number of results per page |
| cursor | — | — | Opaque string from previous response |
Response Structure
{
"data": [ /* ... array of items ... */ ],
"pagination": {
"total": 1542,
"page_size": 50,
"has_more": true,
"cursor": "eyJpZCI6IjU1MGU4NDAwLWUyOWItNDFkNC..."
},
"meta": { "request_id": "req_xK9p" }
}Iterate All Pages
async function* paginate(url, headers) {
let cursor = null;
do {
const qs = cursor ? `?cursor=${cursor}&page_size=500` : '?page_size=500';
const res = await fetch(url + qs, { headers });
const body = await res.json();
yield* body.data;
cursor = body.pagination.has_more ? body.pagination.cursor : null;
} while (cursor);
}
// Usage:
for await (const msg of paginate(
"https://app.bulksmsrates.com/v1/reports/messages", HEADERS
)) {
console.log(msg.message_id, msg.status);
}#Error Codes
All errors return consistent JSON. Check the code field for programmatic handling:
{
"error": {
"code": "INSUFFICIENT_BALANCE",
"message": "Wallet balance too low to send 3 segments",
"details": { "required": 450, "available": 200 }
},
"meta": { "request_id": "req_abc123" }
}| HTTP | Code | Description |
|---|---|---|
| 400 | VALIDATION_ERROR | Invalid request body or query parameters |
| 400 | INVALID_PHONE_NUMBER | Destination is not a valid E.164 number |
| 400 | INVALID_SENDER_ID | Sender ID not approved or too long |
| 400 | MESSAGE_TOO_LONG | Body exceeds maximum character limit |
| 401 | UNAUTHORIZED | Missing, invalid, or expired credentials |
| 401 | ACCOUNT_LOCKED | Account locked after repeated failed logins |
| 403 | FORBIDDEN | Authenticated but lacking required permission |
| 404 | NOT_FOUND | Resource does not exist |
| 409 | CONFLICT | Duplicate resource or state conflict |
| 422 | INSUFFICIENT_BALANCE | Wallet balance too low for this request |
| 422 | OPT_OUT | Recipient has opted out of receiving messages |
| 422 | BLACKLISTED | Destination number is blacklisted |
| 429 | RATE_LIMITED | Too many requests — check Retry-After header |
| 429 | ACCOUNT_LOCKED | Account temporarily locked (5 failed logins) |
| 500 | INTERNAL_ERROR | Unexpected server error |
| 503 | SERVICE_UNAVAILABLE | Upstream gateway temporarily unavailable |
Start Building Today
Download an SDK, grab the Postman collection, or explore the full OpenAPI spec. Everything you need to go from zero to SMS in minutes.