Огляд
Використовуйте API, коли повідомлення запускають ваші власні системи: статуси замовлень з магазину, коди з форми входу, нагадування із системи бронювання. Запит POST /v1/messages з до 10,000 номерів створює одну розсилку, списує кошти з балансу й ставить кожне повідомлення в чергу. Розсилки, створені через API, також з'являються в консолі зі своїми звітами про доставку.
- Усі суми подаються в доларах США як рядки з п'ятьма десятковими знаками, наприклад «0.00534».
- Телефонні номери міжнародні, з початковим + або без нього; пробіли й дефіси прибираються.
- Час подається за ISO 8601 в UTC, наприклад 2026-12-01T09:00:00Z.
- Ідентифікатори повідомлень і розсилок мають вигляд 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
Приймає те саме тіло, що й /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
Перша повертає розсилку з кількістю за статусами; друга постачає її повідомлення по сторінках, до 1,000 на сторінку. Розсилки, надіслані з консолі, можна читати так само за ідентифікатором, що показаний на їхній сторінці.
Скасування запланованого надсилання
POST/v1/campaigns/{id}/cancel
Зупиняє розсилку, створену з send_at, яка ще не почалася. Усі повідомлення скасовуються, а повна вартість повертається на баланс. Розсилка, що вже триває, відповідає 409 not_scheduled.
Вебхуки доставки
Додайте URL HTTPS в розділі API консолі, щоб отримувати кожен кінцевий статус (delivered або failed) як запит 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 | Невідомий ідентифікатор або кінцева точка. |
| 409 | not_scheduled | Скасувати можна лише заплановану розсилку, яка ще не почалася. |
| 413 | too_many_recipients | Понад 10,000 номерів в одному запиті. |
| 429 | rate_limited | Понад 300 запитів за хвилину. |
| 500 | server_error | Повторіть запит; нічого не списано. |