API v1 — Production Ready

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

1

Create Account

Sign up at bulksmsrates.com and verify your email address. Free sandbox included.

2

Get API Keys

Dashboard → Settings → API Keys → Create key pair. You get a Key and Secret.

3

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!"
  }'
✅ 202 Accepted response:
{
  "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/refresh to renew
MethodEndpointDescription
POST/v1/auth/registerCreate account + get tokens
POST/v1/auth/loginEmail + password login
POST/v1/auth/refreshRotate refresh → new access token
GET/v1/auth/meGet current user profile
POST/v1/auth/logoutRevoke refresh token (blacklist)
GET/v1/auth/google/urlGet Google OAuth redirect URL
POST/v1/auth/googleExchange Google code for tokens
POST/v1/auth/forgot-passwordSend password reset email
POST/v1/auth/reset-passwordReset password with token
POST/v1/auth/change-passwordChange password (authenticated)
🔒 Security Note: Never expose your API Secret or JWT in client-side JavaScript. Use environment variables and always use HTTPS. The sandbox environment (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.

MethodEndpointDescription
POST/v1/sms/sendSend a single SMS message
GET/v1/sms/{message_id}Get message status + DLR events
GET/v1/smsList messages (paginated, filtered)

Request Parameters

FieldTypeRequiredDescription
tostringE.164 destination (+447700900123)
fromstringSender ID (up to 11 alphanum chars, or number)
bodystringMessage text. Supports {{variables}}
variablesobjectSubstitution values for {{placeholders}}
client_refstringYour reference ID (echoed in DLR)
scheduled_atstringISO 8601 UTC send time (max 30 days ahead)
validity_minutesintegerMessage TTL (default 2880 = 48h)
unicodebooleanForce Unicode encoding
template_iduuidUse 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.

MethodEndpointDescription
POST/v1/sms/bulkSubmit 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"
  }'
✅ Response:
{
  "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.

MethodEndpointDescription
POST/v1/campaignsCreate a campaign (draft state)
GET/v1/campaignsList campaigns
GET/v1/campaigns/{id}Get campaign details + progress
POST/v1/campaigns/{id}/recipientsUpload recipient list
POST/v1/campaigns/{id}/submitSubmit for send (→ pending_approval)
POST/v1/campaigns/{id}/pausePause in-progress campaign
POST/v1/campaigns/{id}/resumeResume paused campaign
POST/v1/campaigns/{id}/cancelCancel campaign

Campaign State Machine

draft → pending_approval → approved → sending → paused → completed
                ↓ (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.

MethodEndpointDescription
GET/v1/contactsList contacts (paginated)
POST/v1/contactsCreate a contact
PATCH/v1/contacts/{id}Update a contact
DELETE/v1/contacts/{id}Delete a contact
POST/v1/contacts/{id}/tagsAdd tag to contact
DELETE/v1/contacts/{id}/tags/{tag_id}Remove tag from contact
GET/v1/contacts/groupsList groups
POST/v1/contacts/groupsCreate a group
DELETE/v1/contacts/groups/{id}Delete a group
POST/v1/contacts/groups/{id}/importImport CSV into group
GET/v1/contacts/tagsList all tags
POST/v1/contacts/tagsCreate 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],Standard
ℹ️ Opt-out Enforcement: Numbers that have replied STOP, UNSUBSCRIBE, or OPT-OUT are automatically added to the opt-out list. Any send to an opted-out number returns422 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.

MethodEndpointDescription
GET/v1/billing/balanceCurrent wallet balance + credit limit
GET/v1/billing/transactionsTransaction history (paginated)
GET/v1/billing/invoicesList invoices
GET/v1/billing/invoices/{id}Get invoice details
GET/v1/billing/invoices/{id}/pdfDownload invoice PDF
POST/v1/billing/checkoutCreate Stripe checkout session
GET/v1/billing/usageUsage summary (current period)
GET/v1/billing/usage/exportExport usage CSV
POST/v1/billing/auto-topupConfigure 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"
✅ Balance response:
{
  "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).

MethodEndpointDescription
GET/v1/reports/messagesFull message log with filters
GET/v1/reports/dlr-statsDelivery rate stats (sent/delivered/failed)

Query Parameters — Message Log

ParameterTypeDescription
statusstringFilter by: queued, sent, delivered, failed, expired
from_datedate-timeStart of range (ISO 8601)
to_datedate-timeEnd of range (ISO 8601)
destinationstringFilter by E.164 number (exact match)
campaign_iduuidFilter by campaign
page_sizeintegerResults per page (1–500, default 50)
cursorstringPagination 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.

MethodEndpointDescription
POST/v1/whatsapp/sendSend a WhatsApp message
GET/v1/whatsapp/templatesList approved message templates
POST/v1/whatsapp/templatesCreate a new template (pending Meta approval)
DELETE/v1/whatsapp/templates/{name}Delete a template
GET/v1/whatsapp/conversationsList active 24h conversations
GET/v1/whatsapp/accountsList connected WhatsApp Business accounts
GET/v1/whatsapp/webhookMeta webhook hub verification
POST/v1/whatsapp/webhookReceive 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.

MethodEndpointDescription
GET/v1/settings/webhooksList configured webhooks
POST/v1/settings/webhooksCreate 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 STOP
campaign.completedCampaign finished sending
campaign.failedCampaign encountered errors
balance.lowBalance below threshold
whatsapp.messageInbound WhatsApp message

DLR 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 below
Retry 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

MethodEndpointDescription
GET/v1/settings/profileGet account profile
PATCH/v1/settings/profileUpdate profile (name, timezone, etc.)
GET/v1/settings/api-keysList API keys
POST/v1/settings/api-keysCreate a new API key pair
DELETE/v1/settings/api-keys/{id}Revoke an API key
GET/v1/settings/sender-idsList sender IDs
POST/v1/settings/sender-idsRequest a new sender ID
GET/v1/settings/smppGet SMPP credentials
POST/v1/settings/smpp/regenerateRegenerate SMPP password
GET/v1/settings/notificationsGet notification preferences
PATCH/v1/settings/notificationsUpdate 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.

🐍

Python SDK

Python 3.8+ · requests library

pip install requests
🟨

Node.js SDK

Node 18+ · Zero dependencies

ESM · Built-in fetch
🐘

PHP SDK

PHP 8.0+ · ext-curl

No Composer needed
🧡

Postman Collection

All endpoints organised by folder with pre-request auto-auth scripts, environment variables, and example responses. Import directly into Postman or Insomnia.

⬇ Download Postman Collection

🧪 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.

HeaderDescription
X-RateLimit-LimitMax requests per second for your key
X-RateLimit-RemainingTokens remaining in current window
X-RateLimit-ResetUnix timestamp when window resets
Retry-AfterSeconds 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');
}
💡 Bulk sends: The 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

ParameterDefaultMaxDescription
page_size50500Number of results per page
cursorOpaque 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" }
}
HTTPCodeDescription
400VALIDATION_ERRORInvalid request body or query parameters
400INVALID_PHONE_NUMBERDestination is not a valid E.164 number
400INVALID_SENDER_IDSender ID not approved or too long
400MESSAGE_TOO_LONGBody exceeds maximum character limit
401UNAUTHORIZEDMissing, invalid, or expired credentials
401ACCOUNT_LOCKEDAccount locked after repeated failed logins
403FORBIDDENAuthenticated but lacking required permission
404NOT_FOUNDResource does not exist
409CONFLICTDuplicate resource or state conflict
422INSUFFICIENT_BALANCEWallet balance too low for this request
422OPT_OUTRecipient has opted out of receiving messages
422BLACKLISTEDDestination number is blacklisted
429RATE_LIMITEDToo many requests — check Retry-After header
429ACCOUNT_LOCKEDAccount temporarily locked (5 failed logins)
500INTERNAL_ERRORUnexpected server error
503SERVICE_UNAVAILABLEUpstream 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.