Przegląd
Używaj API, gdy wiadomości są wyzwalane przez twoje systemy: aktualizacje zamówień ze sklepu, kody z formularza logowania, przypomnienia z narzędzia rezerwacji. Żądanie POST /v1/messages z maksymalnie 10,000 numerami tworzy jedną kampanię, obciąża saldo i umieszcza każdą wiadomość w kolejce. Kampanie utworzone przez API również pojawiają się w konsoli wraz ze swoimi raportami dostawy.
- Wszystkie kwoty są w dolarach amerykańskich jako ciągi znaków z pięcioma miejscami po przecinku, na przykład "0.00534".
- Numery telefonów są międzynarodowe, z wiodącym + lub bez; spacje i myślniki są usuwane.
- Czasy są w formacie ISO 8601 w UTC, na przykład 2026-12-01T09:00:00Z.
- Identyfikatory wiadomości i kampanii wyglądają jak MSG-XXXX-XXXX-XXXX i CMP-XXXX-XXXX-XXXX.
Uwierzytelnianie
Utwórz klucz API w sekcji API w konsoli (klucze stają się dostępne po dodaniu pierwszej wpłaty). Wysyłaj go w nagłówku Authorization każdego żądania. Klucz wygląda jak bso_api_ po którym następuje 36 znaków; jest pokazywany tylko raz, więc przechowuj go jako sekret na swoim serwerze. Utworzenie nowego klucza natychmiast zastępuje poprzedni.
Authorization: Bearer bso_api_0123456789abcdef0123456789abcdef0123 Content-Type: application/json
Limity
| Żądania | 300 na minutę na klucz; więcej zwraca odpowiedź 429 rate_limited |
|---|---|
| Numery na żądanie | 10,000 |
| Tekst wiadomości | 2,000 znaków w zapisie, do 10 segmentów |
| Planowanie | send_at od 5 minut do 60 dni do przodu |
| Tempo wysyłania | 400 wiadomości na minutę na kolejkę |
| Ponowne próby webhooka | 9 prób w ciągu około 22 godzin |
Wyślij wiadomości
POST/v1/messages
Wysyła jedną wiadomość do jednego lub wielu numerów. Pełny koszt jest pobierany z salda po zaakceptowaniu żądania; każda wiadomość, której nie można dostarczyć, jest później zwracana. Odpowiedź ma status 201 i zawiera kampanię z każdą wiadomością.
| Pole | Typ | Opis |
|---|---|---|
towymagane | string | string[] | Jeden numer lub tablica do 10,000 numerów w formacie międzynarodowym. Powtórzone numery są wysyłane raz; nieczytelne są wymienione w pominiętych. |
textwymagane | string | Wiadomość, do 2,000 znaków i 10 segmentów. Tekst GSM-7 zawiera 160 znaków na SMS, Unicode 70. |
send_atopcjonalne | string | Czas ISO 8601 do zaplanowania wysyłki, od 5 minut do 60 dni naprzód. Pomijając, wyślij teraz. |
opt_outopcjonalne | boolean | true dodaje twoją linię rezygnacji z ustawień konsoli w nowej linii na końcu. |
client_refopcjonalne | string | Twoje własne odwołanie, do 64 liter, cyfr i _ . : -. Zwracane jest w raportach i webhookach. |
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" }
]
}Wyceń wysyłkę
POST/v1/quote
Przyjmuje to samo ciało co /v1/messages i zwraca dokładny koszt, segmenty i kodowanie bez wysyłania lub pobierania opłat. Użyj, aby pokazać cenę swoim użytkownikom przed potwierdzeniem.
{
"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" } ]
}Saldo
GET/v1/balance
{ "balance": "118.43120", "currency": "USD" }Stawki
GET/v1/rates
GET/v1/rates/{country}
Cena jednego segmentu SMS dla każdego miejsca docelowego lub dla jednego dwuliterowego kodu kraju ISO. Te dwa punkty końcowe są publiczne i nie wymagają klucza.
{ "country": "DE", "name": "Germany", "calling_code": "+49", "price": "0.01544", "currency": "USD", "per": "segment" }Status wiadomości
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"
}Kampanie
GET/v1/campaigns/{id}
GET/v1/campaigns/{id}/messages?offset=0&limit=100
Pierwszy zwraca kampanię z liczbami według statusu; drugi przegląda jej wiadomości, do 1000 na stronę. Kampanie wysłane z konsoli można odczytać w ten sam sposób za pomocą id pokazanego na ich stronie.
Anuluj zaplanowaną wysyłkę
POST/v1/campaigns/{id}/cancel
Zatrzymuje kampanię utworzoną z send_at, która jeszcze się nie rozpoczęła. Każda wiadomość jest anulowana, a pełny koszt wraca do salda. Kampania, która już jest wysyłana, odpowiada 409 not_scheduled.
Webhook dostarczenia
Dodaj URL HTTPS w API w konsoli, aby otrzymywać każdy końcowy status (dostarczony lub nieudany) jako żądanie POST. Odpowiedz z dowolnym statusem 2xx w ciągu 8 sekund. Inne odpowiedzi i przekroczenia czasu są ponawiane po 20 sekundach, 1, 5 i 15 minutach, a następnie 1, 3, 6 i 12 godzin, 9 prób łącznie. To samo zdarzenie może przyjść dwukrotnie, więc użyj event_id, aby przetworzyć je raz.
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"
}
}Zweryfikuj podpis
Nagłówek zawiera czas Unix t i podpis v1 = HMAC-SHA256 ciągu "t.raw_body" z twoim sekretem webhooka, w hex. Oblicz go na surowym ciele przed parsowaniem JSON, porównaj w stałym czasie i odrzuć zdarzenia starsze niż pięć minut.
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]);
}Statusy
| queued | Zaakceptowane i oczekuje na opuszczenie kolejki |
|---|---|
| submitted | Przekazane do sieci docelowej |
| delivered | Potwierdzone przez telefon (końcowe) |
| failed | Nie można dostarczyć; cena jest zwracana (końcowe) |
| canceled | Część zaplanowanej kampanii anulowana przed wysłaniem (końcowe) |
Status kampanii to scheduled, sending, complete lub canceled.
Błędy
Błędy odpowiadają z statusem HTTP i ciałem JSON: { "error": { "code": "insufficient_balance", "message": "..." } }. Nic nie jest pobierane, gdy żądanie odpowiada błędem.
| HTTP | Kod | Znaczenie |
|---|---|---|
| 400 | invalid_request | Brakuje pola lub jest nieprawidłowe; wiadomość mówi które. |
| 400 | invalid_json | Treść nie jest w formacie JSON. |
| 400 | no_valid_numbers | Żaden z numerów nie mógł zostać odczytany. |
| 401 | unauthorized | Klucz jest brakujący, uszkodzony lub zastąpiony. |
| 402 | insufficient_balance | Saldo nie pokrywa kosztów; doładuj i wyślij ponownie. |
| 404 | not_found | Nieznany identyfikator lub endpoint. |
| 409 | not_scheduled | Tylko zaplanowana kampania, która się nie rozpoczęła, może zostać anulowana. |
| 413 | too_many_recipients | Więcej niż 10,000 numerów w jednym żądaniu. |
| 429 | rate_limited | Więcej niż 300 żądań w ciągu minuty. |
| 500 | server_error | Ponów żądanie; nic nie zostało obciążone. |