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:
{
  "data": {
    "message_id": "550e8400-e29b-41d4-a716-446655440000",
    "to": "+447700900123",
    "from": "MyApp",
    "status": "queued",
    "segments": 1,
    "cost": 350,
    "currency": "GBP"
  },
  "meta": { "request_id": "req_xK9p" }
}
🌍

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

{
  "event": "dlr",
  "webhook_id": "wh_evt_abc123",
  "timestamp": "2026-02-18T12:00:03Z",
  "data": {
    "message_id": "550e8400-e29b-41d4-a716-446655440000",
    "to": "+447700900123",
    "from": "MyApp",
    "status": "delivered",
    "error_code": null,
    "client_ref": "order-456",
    "segments": 1,
    "cost": 350,
    "currency": "GBP",
    "sent_at": "2026-02-18T12:00:00Z",
    "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"
  }
}

#SDK Examples

No official SDK yet? No problem — the API is simple REST. Here are complete, copy-paste examples for each language covering all common operations.

🐍 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

Full OpenAPI Reference

Explore every endpoint, schema, and parameter in the machine-readable spec. Compatible with Postman, Insomnia, and Swagger UI.