Gambaran keseluruhan
Gunakan API apabila mesej dicetuskan oleh sistem anda sendiri: kemas kini pesanan dari kedai, kod dari borang log masuk, peringatan dari alat tempahan. Permintaan ke POST /v1/messages dengan sehingga 10,000 nombor mencipta satu kempen, mengecas baki dan mengatur setiap mesej dalam barisan. Kempen yang dicipta oleh API juga muncul di konsol dengan laporan penghantarannya.
- Semua jumlah adalah dalam dolar AS sebagai rentetan dengan lima perpuluhan, contohnya "0.00534".
- Nombor telefon adalah antarabangsa, dengan atau tanpa tanda + di hadapan; ruang dan sengkang akan dialihkan.
- Masa adalah dalam format ISO 8601 dan UTC, contohnya 2026-12-01T09:00:00Z.
- ID mesej dan kempen kelihatan seperti MSG-XXXX-XXXX-XXXX dan CMP-XXXX-XXXX-XXXX.
Pengesahan
Buat kunci API di bawah API dalam konsol (kunci dibuka selepas top-up pertama anda ditambah). Hantarkannya dalam pengepala Pemberian Kuasa setiap permintaan. Kunci kelihatan seperti bso_api_ diikuti 36 aksara; ia dipaparkan sekali sahaja, jadi simpan sebagai rahsia di pelayan anda. Mencipta kunci baharu akan menggantikan kunci sebelumnya serta-merta.
Authorization: Bearer bso_api_0123456789abcdef0123456789abcdef0123 Content-Type: application/json
Had
| Permintaan | 300 seminit setiap kunci; lebih daripada itu jawapan 429 rate_limited |
|---|---|
| Nombor setiap permintaan | 10,000 |
| Teks mesej | 2,000 aksara seperti ditulis, sehingga 10 segmen |
| Penjadualan | send_at dari 5 minit hingga 60 hari ke hadapan |
| Kadar penghantaran | 400 mesej seminit setiap barisan |
| Percubaan semula webhook | 9 percubaan dalam kira-kira 22 jam |
Hantar mesej
POST/v1/messages
Menghantar satu teks kepada satu atau lebih nombor. Jumlah kos akan ditolak daripada baki apabila permintaan diterima; setiap mesej yang tidak dapat dihantar akan dipulangkan kemudian. Jawapan mempunyai status 201 dan kempen dengan setiap mesej.
| Medan | Jenis | Penerangan |
|---|---|---|
todiperlukan | string | string[] | Satu nombor atau tatasusunan sehingga 10,000 nombor dalam format antarabangsa. Nombor berulang dihantar sekali; yang tidak boleh dibaca disenaraikan dalam skipped. |
textdiperlukan | string | Mesej, sehingga 2,000 aksara dan 10 segmen. Teks GSM-7 memegang 160 aksara setiap SMS, Unicode 70. |
send_atpilihan | string | Masa ISO 8601 untuk menjadualkan penghantaran, 5 minit hingga 60 hari ke hadapan. Biarkan kosong untuk hantar sekarang. |
opt_outpilihan | boolean | true menambah baris opt-out daripada tetapan konsol pada baris baharu di hujung. |
client_refpilihan | string | Rujukan sendiri, sehingga 64 huruf, digit dan _ . : -. Ia dikembalikan dalam laporan dan webhook. |
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" }
]
}Dapatkan sebut harga
POST/v1/quote
Mengambil badan yang sama seperti /v1/messages dan mengembalikan kos tepat, segmen dan pengekodan tanpa menghantar atau mengecaj apa-apa. Gunakannya untuk menunjukkan harga kepada pengguna sendiri sebelum mereka mengesahkan.
{
"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" } ]
}Baki
GET/v1/balance
{ "balance": "118.43120", "currency": "USD" }Kadar
GET/v1/rates
GET/v1/rates/{country}
Harga satu segmen SMS untuk setiap destinasi, atau untuk satu kod negara ISO dua huruf. Kedua-dua endpoint ini adalah awam dan tidak memerlukan kunci.
{ "country": "DE", "name": "Germany", "calling_code": "+49", "price": "0.01544", "currency": "USD", "per": "segment" }Status mesej
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"
}Kempen
GET/v1/campaigns/{id}
GET/v1/campaigns/{id}/messages?offset=0&limit=100
Yang pertama mengembalikan kempen dengan kiraan setiap status; yang kedua muka surat melalui mesejnya, sehingga 1,000 setiap halaman. Kempen yang dihantar dari konsol boleh dibaca dengan cara yang sama dengan id yang ditunjukkan pada halaman mereka.
Batal penghantaran berjadual
POST/v1/campaigns/{id}/cancel
Menghentikan kempen yang dibuat dengan send_at dan belum bermula. Setiap mesej dibatalkan dan jumlah kos dikembalikan ke baki. Kempen yang sudah menghantar menjawab 409 not_scheduled.
Webhook penghantaran
Tambah URL HTTPS di bawah API dalam konsol untuk menerima setiap status akhir (dihantar atau gagal) sebagai permintaan POST. Jawab dengan sebarang status 2xx dalam 8 saat. Jawapan lain dan masa tamat dicuba semula selepas 20 saat, 1, 5 dan 15 minit, kemudian 1, 3, 6 dan 12 jam, 9 percubaan secara keseluruhan. Peristiwa yang sama boleh tiba dua kali, jadi gunakan event_id untuk memprosesnya sekali.
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"
}
}Sahkan tandatangan
Header memegang masa Unix t dan tandatangan v1 = HMAC-SHA256 bagi rentetan "t.raw_body" dengan rahsia webhook anda, dalam hex. Kirakannya ke atas badan mentah sebelum menghuraikan JSON, bandingkan dalam masa malar dan tolak peristiwa yang lebih lama daripada lima minit.
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]);
}Status
| queued | Diterima dan menunggu untuk meninggalkan barisan |
|---|---|
| submitted | Diserahkan kepada rangkaian destinasi |
| delivered | Disahkan oleh telefon (muktamad) |
| failed | Tidak dapat dihantar; harga dipulangkan (muktamad) |
| canceled | Sebahagian daripada kempen berjadual dibatalkan sebelum penghantaran (muktamad) |
Status kempen adalah scheduled, sending, complete atau canceled.
Ralat
Ralat menjawab dengan status HTTP dan badan JSON: { "error": { "code": "insufficient_balance", "message": "..." } }. Tiada apa yang dicaj apabila permintaan menjawab dengan ralat.
| HTTP | Kod | Maksud |
|---|---|---|
| 400 | invalid_request | Medan tiada atau tidak sah; mesej menyatakan yang mana. |
| 400 | invalid_json | Badan bukan JSON. |
| 400 | no_valid_numbers | Tiada nombor dapat dibaca. |
| 401 | unauthorized | Kunci tiada, rosak atau diganti. |
| 402 | insufficient_balance | Baki tidak mencukupi untuk kos; tambah nilai dan hantar semula. |
| 404 | not_found | ID atau endpoint tidak dikenali. |
| 409 | not_scheduled | Hanya kempen yang dijadualkan dan belum bermula boleh dibatalkan. |
| 413 | too_many_recipients | Lebih daripada 10,000 nombor dalam satu permintaan. |
| 429 | rate_limited | Lebih daripada 300 permintaan dalam satu minit. |
| 500 | server_error | Cuba permintaan semula; tiada caj dikenakan. |