Skip to content
smsbulk

Bulk SMS API v1

A small REST API for sending bulk SMS from your own software: JSON in and out, Bearer keys, the same prices and checks as the console, and signed webhooks for delivery reports. Base URL https://smsbulk.cc/rest/v1

Overview

Send to up to 8,000 numbers per request, each with its own merge values, and the API prices every recipient with their own merged text, exactly like the console. Ask for a quote first with the same body to know the cost; nothing is sent or charged by a quote. Times are ISO 8601 in UTC, money is a string in US dollars with five decimals.

curl https://smsbulk.cc/rest/v1/send \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "recipients": [
      { "to": "+14155550142", "fields": { "name": "Maria" } },
      { "to": "+447400123456", "fields": { "name": "Tom" } }
    ],
    "text": "Hi {{name}}, your order ships today.",
    "client_ref": "order-batch-17"
  }'

Authentication

Create a key in the console under API and webhooks; keys open after your first credited top-up. Send it on every request as a Bearer token. A key is shown once; you can keep up to 5 keys and revoke any of them at once.

Authorization: Bearer smsb_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Rates

Public, no key needed. The price per SMS segment for every destination, or for one ISO code.

GEThttps://smsbulk.cc/rest/v1/rates

GEThttps://smsbulk.cc/rest/v1/rates/{ISO}

{
  "country": "US",
  "name": "United States",
  "dial_code": "+1",
  "rate": "0.00485",
  "currency": "USD",
  "per": "message segment"
}

Credit

GEThttps://smsbulk.cc/rest/v1/credit

{ "credit": "125.40000", "currency": "USD", "account": "A418820037715" }

Quote

Prices a send without sending it. Takes the same body as Send.

POSThttps://smsbulk.cc/rest/v1/quote

FieldMeaning
toOne number or a list. International form with + is safest; local numbers need default_country.
recipientsInstead of to: a list of { to, fields } where fields fill merge fields such as {{name}}.
textThe message, up to 2,000 characters and 10 segments after merging.
default_countryISO code used for numbers written without +, such as GB. Falls back to the default saved in Settings.
opt_outAdds your saved opt-out wording as a last line (save it in Settings first).
{
  "recipients": 2,
  "messages": 2,
  "segments": { "min": 1, "max": 1 },
  "encoding": "GSM-7",
  "total": "0.01240",
  "currency": "USD",
  "credit": "125.40000",
  "credit_after": "125.38760",
  "countries": [
    { "country": "US", "recipients": 1, "rate": "0.00485", "segments": 1, "subtotal": "0.00485" },
    { "country": "GB", "recipients": 1, "rate": "0.00755", "segments": 1, "subtotal": "0.00755" }
  ],
  "left_out": []
}

Send

Creates a campaign, takes its total from your credit and queues the messages. Answers 201 with the campaign. Numbers that cannot be sent are listed in left_out and never charged.

POSThttps://smsbulk.cc/rest/v1/send

FieldMeaning
to / recipients / text / default_country / opt_outAs in Quote.
send_atISO 8601 time to start, 5 minutes to 90 days ahead. Leave out to send now.
nameYour label for the campaign, up to 80 characters.
client_refYour own reference, up to 64 characters, returned on every message and webhook.
HTTP/1.1 201 Created
{
  "campaign": {
    "id": "C507318264409",
    "name": null,
    "status": "sending",
    "source": "api",
    "test": false,
    "recipients": 2,
    "cost": "0.01240",
    "refunded": "0.00000",
    "net_cost": "0.01240",
    "currency": "USD",
    "send_at": null,
    "created_at": "2026-11-07T14:02:11.000Z",
    "started_at": "2026-11-07T14:02:11.000Z",
    "finished_at": null,
    "client_ref": "order-batch-17"
  },
  "messages": 2,
  "left_out": []
}

Campaigns

GEThttps://smsbulk.cc/rest/v1/campaigns?limit=25&before={id}

GEThttps://smsbulk.cc/rest/v1/campaigns/{id}

GEThttps://smsbulk.cc/rest/v1/campaigns/{id}/messages?status=failed&limit=100&after={message id}

A single campaign includes counts per status: queued, sent, delivered, failed and canceled. Message lists page with after and has_more; up to 1,000 per page.

Messages

GEThttps://smsbulk.cc/rest/v1/messages/{id}

GEThttps://smsbulk.cc/rest/v1/messages?to=%2B14155550142&status=delivered&client_ref=order-batch-17&limit=50

{
  "id": "M220947781305",
  "campaign": "C507318264409",
  "to": "+14155550142",
  "country": "US",
  "segments": 1,
  "encoding": "GSM-7",
  "price": "0.00485",
  "status": "delivered",
  "reason": null,
  "reason_text": null,
  "refunded": false,
  "client_ref": "order-batch-17",
  "created_at": "2026-11-07T14:02:11.000Z",
  "sent_at": "2026-11-07T14:02:12.000Z",
  "finished_at": "2026-11-07T14:02:16.000Z"
}

Cancel a scheduled campaign

POSThttps://smsbulk.cc/rest/v1/campaigns/{id}/cancel

Works until the start time. The whole cost returns to your credit in one ledger line and the campaign status becomes canceled.

Webhooks

Set an endpoint URL in the console. We POST JSON for message.delivered, message.failed and campaign.done, plus test when you press Send test event. Each event carries four headers, and any 2xx answer within 10 seconds counts as received.

FieldMeaning
Smsbulk-EventEvent name, such as message.delivered.
Smsbulk-DeliveryDelivery id, the same on every retry of one event.
Smsbulk-TimestampUnix seconds when this attempt was signed.
Smsbulk-Signaturesha256= followed by the base64 HMAC-SHA256 of timestamp, a newline and the raw body, keyed with your sbw_ secret.

Delivery is tried 8 times: at once, then after 1 minute, 5 minutes, 15 minutes, 1 hour, 3 hours, 6 hours and 12 hours. Each attempt shows in the console delivery log with its HTTP status and the start of your answer.

{
  "id": "dlv_3f9a1c07b2e44d58a6c1",
  "event": "message.failed",
  "created": "2026-11-07T14:02:16.000Z",
  "data": {
    "message": "M220947781305",
    "campaign": "C507318264409",
    "to": "+447400123456",
    "country": "GB",
    "status": "failed",
    "reason": "not_in_service",
    "segments": 1,
    "price": "0.00755",
    "client_ref": "order-batch-17"
  }
}

Verify the signature

import crypto from "node:crypto";

// raw = the request body exactly as received (string), secret = your sbw_ signing secret
export function verify(raw, headers, secret) {
  const ts = headers["smsbulk-timestamp"];
  const want = "sha256=" + crypto.createHmac("sha256", secret).update(ts + "\n" + raw).digest("base64");
  const got = headers["smsbulk-signature"] || "";
  const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
  return fresh && got.length === want.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want));
}

Errors and limits

Errors use the HTTP status and a JSON body: {"error":{"code":"insufficient_credit","message":"..."}}. Nothing is charged when a request fails.

FieldMeaning
400 bad_jsonThe body is not JSON.
401 unauthorizedMissing, malformed or revoked key.
402 insufficient_creditThe total is more than your credit. Add funds in the console.
404 not_foundNo such endpoint, campaign, message or ISO code on this account.
409 not_scheduledCancel was asked for a campaign that already started.
422 text_missing, text_too_long, too_many_segmentsMessage empty, over 2,000 characters, or over 10 segments for a recipient.
422 no_recipients, too_many, recipients_missingNo sendable number, more than 8,000 numbers, or neither to nor recipients.
422 schedule_range, bad_send_atsend_at outside 5 minutes to 90 days, or not a valid time.
422 opt_out_unsetopt_out is true but no wording is saved in Settings.
429 rate_limitedMore than 300 requests in a minute for this key.

Every answer carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset.

Left-out codes

Numbers removed before pricing come back in left_out with their position in your list, the value you sent, a code and the plain reason.

FieldMeaning
no_country_codeNo + and no default country.
too_short / too_longUnder 8 or over 15 digits.
not_a_numberLetters or symbols that are not part of a number.
nanp_lengthA +1 number without eleven digits.
not_coveredA destination outside our list.
duplicateThe same number appeared earlier in the request.
suppressedThe number is on your do-not-send list.

Statuses and reasons

FieldMeaning
queuedWaiting to leave our queue.
sentHanded to the network, waiting for the delivery report. Most reports arrive within minutes; some networks take up to 24 hours.
deliveredThe network confirmed the phone received it.
failedNot delivered. The price was refunded to your credit.
canceledNot sent because you canceled. Refunded.
FieldMeaning
network_errorNetwork error
switched_offPhone switched off or out of coverage
not_in_serviceNumber not in service
blockedBlocked by the network or the recipient
landlineLandline or number that cannot receive SMS
content_filterRejected by the network's content filter
expiredExpired: no report before the network's time limit
unknownUnknown network response
canceledCanceled before sending

Questions about the API? Open a ticket in the console, or read the FAQ.

Bulk SMS API v1 Documentation: Send, Quote, Webhooks | SMSBulk