Обзор
Используйте API, когда сообщения запускаются вашими системами: уведомления о заказах из магазина, коды из формы входа, напоминания из инструмента бронирования. Запрос POST /v1/messages с до 10,000 номерами создаёт одну кампанию, списывает средства с баланса и ставит каждое сообщение в очередь. Кампании, созданные через API, также отображаются в консоли с отчётами о доставке.
- Все суммы указаны в долларах США в виде строк с пятью знаками после запятой, например "0.00534".
- Номера телефонов указываются в международном формате, с начальным + или без; пробелы и дефисы удаляются.
- Время указывается в формате ISO 8601 по UTC, например 2026-12-01T09:00:00Z.
- ID сообщений и кампаний выглядят как MSG-XXXX-XXXX-XXXX и CMP-XXXX-XXXX-XXXX.
Аутентификация
Создайте ключ API в разделе API в консоли (ключи становятся доступны после первого пополнения). Передавайте его в заголовке Authorization каждого запроса. Ключ выглядит как bso_api_ и 36 символов; он показывается один раз, поэтому сохраните его как секрет на вашем сервере. Создание нового ключа немедленно заменяет предыдущий.
Authorization: Bearer bso_api_0123456789abcdef0123456789abcdef0123 Content-Type: application/json
Лимиты
| Запросы | 300 в минуту на ключ; при превышении, ответ 429 rate_limited |
|---|---|
| Номеров в запросе | 10,000 |
| Текст сообщения | 2,000 символов как написано, до 10 сегментов |
| Планирование | send_at от 5 минут до 60 дней вперёд |
| Скорость отправки | 400 сообщений в минуту на очередь |
| Повторы вебхуков | 9 попыток в течение примерно 22 часов |
Отправка сообщений
POST/v1/messages
Отправляет один текст на один или несколько номеров. Полная стоимость списывается с баланса при принятии запроса; за каждое сообщение, которое не удалось доставить, средства возвращаются позже. Ответ имеет статус 201 и содержит кампанию с каждым сообщением.
| Поле | Тип | Описание |
|---|---|---|
toобязательно | string | string[] | Один номер или массив до 10,000 номеров в международном формате. Повторяющиеся номера отправляются один раз; нечитаемые указываются в skipped. |
textобязательно | string | Сообщение длиной до 2,000 символов и 10 сегментов. Текст в GSM-7 содержит 160 символов на SMS, в Unicode, 70. |
send_atопционально | string | Время по ISO 8601 для планирования отправки, от 5 минут до 60 дней вперед. Оставьте пустым для отправки сейчас. |
opt_outопционально | boolean | true добавляет вашу строку отписки из настроек консоли на новой строке в конце. |
client_refопционально | string | Ваш собственный идентификатор, до 64 букв, цифр и _ . : -. Возвращается в отчетах и вебхуках. |
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"}'const res = await fetch("https://bulksmsonline.cc/api/v1/messages", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.BSO_API_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ to: ["+447700900412", "+33612345678"], text: "Order 4521 has shipped.", client_ref: "order-4521" }),
});
if (!res.ok) throw new Error((await res.json()).error.message);
const campaign = await res.json();import os, requests
res = requests.post("https://bulksmsonline.cc/api/v1/messages",
headers={"Authorization": f"Bearer {os.environ['BSO_API_KEY']}"},
json={"to": ["+447700900412", "+33612345678"], "text": "Order 4521 has shipped.", "client_ref": "order-4521"},
timeout=30)
res.raise_for_status()
campaign = res.json()<?php
$ch = curl_init("https://bulksmsonline.cc/api/v1/messages");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("BSO_API_KEY"), "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode(["to" => ["+447700900412", "+33612345678"], "text" => "Order 4521 has shipped.", "client_ref" => "order-4521"])]);
$campaign = json_decode(curl_exec($ch), true);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" }
]
}Рассчитать отправку
POST/v1/quote
Принимает тот же body, что и /v1/messages, и возвращает точную стоимость, сегменты и кодировку без отправки или списания средств. Используйте для показа цены вашим пользователям до подтверждения.
{
"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" } ]
}Баланс
GET/v1/balance
{ "balance": "118.43120", "currency": "USD" }Тарифы
GET/v1/rates
GET/v1/rates/{country}
Цена одного сегмента SMS для каждого направления или для одного двухбуквенного кода страны ISO. Эти эндпоинты публичны и не требуют ключа.
{ "country": "DE", "name": "Germany", "calling_code": "+49", "price": "0.01544", "currency": "USD", "per": "segment" }Статус сообщения
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"
}Кампании
GET/v1/campaigns/{id}
GET/v1/campaigns/{id}/messages?offset=0&limit=100
Первый возвращает кампанию с количеством сообщений по статусам; второй пролистывает сообщения, до 1000 на страницу. Кампании, отправленные из консоли, можно читать так же, используя id, указанный на их странице.
Отменить запланированную отправку
POST/v1/campaigns/{id}/cancel
Останавливает кампанию, созданную с send_at и еще не начатую. Все сообщения отменяются, полная стоимость возвращается на баланс. Кампания, которая уже отправляется, отвечает 409 not_scheduled.
Вебхуки доставки
Добавьте HTTPS URL в разделе API в консоли для получения каждого финального статуса (доставлено или не доставлено) как POST запроса. Отвечайте любым статусом 2xx в течение 8 секунд. Другие ответы и таймауты повторяются через 20 секунд, 1, 5 и 15 минут, затем 1, 3, 6 и 12 часов, всего 9 попыток. Одно и то же событие может прийти дважды, поэтому используйте event_id для однократной обработки.
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"
}
}Проверьте подпись
Заголовок содержит Unix время t и подпись v1 = HMAC-SHA256 строки "t.raw_body" с вашим секретом вебхука в hex. Вычисляйте ее над сырым телом до парсинга JSON, сравнивайте за постоянное время и отклоняйте события старше пяти минут.
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]));
}import hmac, hashlib, re, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
m = re.fullmatch(r"t=(\d+),v1=([0-9a-f]{64})", header or "")
if not m or abs(time.time() - int(m.group(1))) > 300:
return False
want = hmac.new(secret.encode(), m.group(1).encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(want, m.group(2))<?php
function verify(string $raw, string $header, string $secret): bool {
if (!preg_match('/^t=(\d+),v1=([0-9a-f]{64})$/', $header, $m)) return false;
if (abs(time() - (int)$m[1]) > 300) return false;
$want = hash_hmac('sha256', $m[1] . '.' . $raw, $secret);
return hash_equals($want, $m[2]);
}Статусы
| queued | Принято и ожидает выхода из очереди |
|---|---|
| submitted | Передано в сеть назначения |
| delivered | Подтверждено устройством (финально) |
| failed | Не удалось доставить; стоимость возвращена (финально) |
| canceled | Часть запланированной кампании отменена до отправки (финально) |
Статус кампании: scheduled, sending, complete или canceled.
Ошибки
Ошибки отвечают HTTP статусом и JSON телом: { "error": { "code": "insufficient_balance", "message": "..." } }. При ответе с ошибкой ничего не списывается.
| HTTP | Код | Значение |
|---|---|---|
| 400 | invalid_request | Отсутствует или неверное поле; сообщение указывает, какое. |
| 400 | invalid_json | Тело запроса не является JSON. |
| 400 | no_valid_numbers | Не удалось прочитать ни одного номера. |
| 401 | unauthorized | Ключ отсутствует, некорректен или заменён. |
| 402 | insufficient_balance | Баланс недостаточен для оплаты; пополните и отправьте снова. |
| 404 | not_found | Неизвестный id или эндпоинт. |
| 409 | not_scheduled | Отменить можно только запланированную, но ещё не начатую рассылку. |
| 413 | too_many_recipients | Более 10,000 номеров в одном запросе. |
| 429 | rate_limited | Более 300 запросов в минуту. |
| 500 | server_error | Повторите запрос; списаний не было. |