Bulk SMS API reference

The BulkSMSOnline REST API sends text messages to 227 countries from your own software, with the same prices, segment rules, scheduling and refunds as the console. Requests and responses are JSON over HTTPS, and every final delivery status can be pushed to your server as a signed webhook.

Base URLhttps://bulksmsonline.cc/api/v1

Overview

Use the API when messages are triggered by your own systems: order updates from a shop, codes from a login form, reminders from a booking tool. A request to POST /v1/messages with up to 10,000 numbers creates one campaign, charges the balance and queues every message. Campaigns created by the API also appear in the console with their delivery reports.

  • All amounts are US dollars as strings with five decimals, for example "0.00534".
  • Phone numbers are international, with or without the leading +; spaces and dashes are removed.
  • Times are ISO 8601 in UTC, for example 2026-12-01T09:00:00Z.
  • Message and campaign ids look like MSG-XXXX-XXXX-XXXX and CMP-XXXX-XXXX-XXXX.

Authentication

Create an API key under API in the console (keys open after your first top-up is added). Send it in the Authorization header of every request. A key looks like bso_api_ followed by 36 characters; it is shown once, so store it as a secret on your server. Creating a new key replaces the previous one at once.

Authorization: Bearer bso_api_0123456789abcdef0123456789abcdef0123
Content-Type: application/json

Limits

Requests300 a minute per key; more answer 429 rate_limited
Numbers per request10,000
Message text2,000 characters as written, up to 10 segments
Schedulingsend_at from 5 minutes to 60 days ahead
Sending pace400 messages a minute per queue
Webhook retries9 tries over about 22 hours

Send messages

POST/v1/messages

Sends one text to one or more numbers. The full cost is taken from the balance when the request is accepted; each message that cannot be delivered is refunded later. The answer has status 201 and the campaign with every message.

FieldDescription
torequiredOne number or an array of up to 10,000 numbers in international format. Repeated numbers are sent once; unreadable ones are listed in skipped.
textrequiredThe message, up to 2,000 characters and 10 segments. GSM-7 text holds 160 characters per SMS, Unicode 70.
send_atoptionalISO 8601 time to schedule the send, 5 minutes to 60 days ahead. Leave it out to send now.
opt_outoptionaltrue adds your opt-out line from the console settings on a new line at the end.
client_refoptionalYour own reference, up to 64 letters, digits and _ . : -. It is returned in reports and webhooks.
curl -X POST https://bulksmsonline.cc/api/v1/messages \
  -H "Authorization: Bearer $BSO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":["+447700900412","+33612345678"],"text":"Order 4521 has shipped. Track it at shop.example/t/4521","client_ref":"order-4521"}'
HTTP/1.1 201 Created
{
  "id": "CMP-7KQ2-M9XA-4HTD",
  "status": "sending",
  "channel": "api",
  "recipients": 2,
  "segments": 1,
  "encoding": "GSM-7",
  "cost": "0.02158",
  "refunded": "0.00000",
  "client_ref": "order-4521",
  "send_at": null,
  "counts": { "queued": 2, "submitted": 0, "delivered": 0, "failed": 0, "canceled": 0 },
  "skipped": [],
  "balance": "118.43120",
  "messages": [
    { "id": "MSG-3RTW-81KD-ZQ0P", "to": "+447700900412", "country": "GB", "segments": 1, "price": "0.00834", "status": "queued" },
    { "id": "MSG-5HNA-4C2E-7YVB", "to": "+33612345678", "country": "FR", "segments": 1, "price": "0.01324", "status": "queued" }
  ]
}

Quote a send

POST/v1/quote

Takes the same body as /v1/messages and returns the exact cost, segments and encoding without sending or charging anything. Use it to show a price to your own users before they confirm.

{
  "recipients": 2,
  "skipped": [],
  "segments": 1,
  "encoding": "GSM-7",
  "cost": "0.02158",
  "balance": "118.45278",
  "countries": [ { "country": "GB", "recipients": 1, "cost": "0.00834" }, { "country": "FR", "recipients": 1, "cost": "0.01324" } ]
}

Balance

GET/v1/balance

{ "balance": "118.43120", "currency": "USD" }

Rates

GET/v1/rates

GET/v1/rates/{country}

The price of one SMS segment for every destination, or for one two-letter ISO country code. These two endpoints are public and need no key.

{ "country": "DE", "name": "Germany", "calling_code": "+49", "price": "0.01544", "currency": "USD", "per": "segment" }

Message status

GET/v1/messages/{id}

{
  "id": "MSG-3RTW-81KD-ZQ0P",
  "campaign_id": "CMP-7KQ2-M9XA-4HTD",
  "to": "+447700900412",
  "country": "GB",
  "segments": 1,
  "price": "0.00834",
  "status": "delivered",
  "error": null,
  "created_at": "2026-11-07T09:00:02Z",
  "submitted_at": "2026-11-07T09:00:03Z",
  "final_at": "2026-11-07T09:00:08Z"
}

Campaigns

GET/v1/campaigns/{id}

GET/v1/campaigns/{id}/messages?offset=0&limit=100

The first returns the campaign with its counts per status; the second pages through its messages, up to 1,000 per page. Campaigns sent from the console can be read the same way with the id shown on their page.

Cancel a scheduled send

POST/v1/campaigns/{id}/cancel

Stops a campaign that was created with send_at and has not started yet. Every message is canceled and the full cost returns to the balance. A campaign that is already sending answers 409 not_scheduled.

Delivery webhooks

Add an HTTPS URL under API in the console to receive each final status (delivered or failed) as a POST request. Answer with any 2xx status within 8 seconds. Other answers and timeouts are retried after 20 seconds, 1, 5 and 15 minutes, then 1, 3, 6 and 12 hours, 9 tries in total. The same event can arrive twice, so use event_id to process it once.

POST https://your-server.example/sms-status
Content-Type: application/json
X-BulkSMSOnline-Signature: t=1794042008,v1=5f0c2a...e91b

{
  "type": "message.status",
  "event_id": "ev_8f1c22d0a4b97e63c1f0",
  "sent_at": "2026-11-07T09:00:09Z",
  "data": {
    "message_id": "MSG-3RTW-81KD-ZQ0P",
    "campaign_id": "CMP-7KQ2-M9XA-4HTD",
    "to": "+447700900412",
    "country": "GB",
    "status": "delivered",
    "error": null,
    "segments": 1,
    "price": "0.00834",
    "currency": "USD",
    "client_ref": "order-4521",
    "final_at": "2026-11-07T09:00:08Z"
  }
}

Verify the signature

The header holds the Unix time t and the signature v1 = HMAC-SHA256 of the string "t.raw_body" with your webhook secret, in hex. Compute it over the raw body before parsing the JSON, compare in constant time and reject events older than five minutes.

import crypto from "node:crypto";

export function verify(rawBody, header, secret) {
  const m = /^t=(\d+),v1=([0-9a-f]{64})$/.exec(header || "");
  if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
  const want = crypto.createHmac("sha256", secret).update(m[1] + "." + rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(want), Buffer.from(m[2]));
}

Statuses

queuedAccepted and waiting to leave the queue
submittedHanded to the destination network
deliveredConfirmed by the handset (final)
failedCould not be delivered; the price is refunded (final)
canceledPart of a scheduled campaign canceled before sending (final)

Campaign status is scheduled, sending, complete or canceled.

Errors

Errors answer with an HTTP status and a JSON body: { "error": { "code": "insufficient_balance", "message": "..." } }. Nothing is charged when a request answers with an error.

HTTPCodeMeaning
400invalid_requestA field is missing or not valid; the message says which.
400invalid_jsonThe body is not JSON.
400no_valid_numbersNone of the numbers could be read.
401unauthorizedThe key is missing, malformed or replaced.
402insufficient_balanceThe balance does not cover the cost; top up and send again.
404not_foundUnknown id or endpoint.
409not_scheduledOnly a scheduled campaign that has not started can be canceled.
413too_many_recipientsMore than 10,000 numbers in one request.
429rate_limitedMore than 300 requests in a minute.
500server_errorRetry the request; nothing was charged.

Get an API key in the console

Create an account, add your first top-up and generate a key under API. Your first request can go out a minute later.

Create account
Bulk SMS API Reference: REST Endpoints and Webhooks | BulkSMSOnline