概覽
當訊息由您自己的系統觸發時使用 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 條訊息 |
| Webhook 重試 | 9 次嘗試,約 22 小時內 |
發送訊息
POST/v1/messages
發送一則短訊至一個或多個號碼。請求接受時會從餘額扣除全額費用;未能送達的每則訊息將於稍後退款。回應狀態為 201,並包含每則訊息的推廣活動資料。
| 欄位 | 類型 | 描述 |
|---|---|---|
to必填 | string | string[] | 一個號碼或最多 10,000 個國際格式號碼的陣列。重複號碼只會發送一次;無法辨識的號碼會列於 skipped 中。 |
text必填 | string | 訊息內容,最多 2,000 個字符和 10 個分段。GSM-7 文字每 SMS 可容納 160 個字符,Unicode 則為 70 個。 |
send_at選填 | string | 以 ISO 8601 時間格式預約發送,可設定 5 分鐘至 60 天後。留空則立即發送。 |
opt_out選填 | boolean | true 會在訊息結尾新增一行,加入你在控制台設定的拒收提示。 |
client_ref選填 | string | 自訂參考編號,最多 64 個字母、數字及 _ . : -。會在報告和 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" }
]
}估算發送成本
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}
返回每個目的地或指定兩字母 ISO 國家代碼的每 SMS 分段價格。這兩個端點為公開,無需使用金鑰。
{ "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 則。從控制台發送的推廣活動亦可使用頁面上顯示的 ID 以相同方式讀取。
取消預定發送
POST/v1/campaigns/{id}/cancel
停止已設定 send_at 但尚未開始的推廣活動。所有訊息將被取消,全額費用退回餘額。已開始發送的推廣活動會回應 409 not_scheduled。
送達 Webhook
在控制台的 API 設定中添加 HTTPS URL,以 POST 請求接收每個最終狀態(送達或失敗)。請在 8 秒內以任何 2xx 狀態回應。其他回應和超時會於 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 = 以你的 webhook 密鑰對字串 "t.raw_body" 進行 HMAC-SHA256 計算的十六進制結果。請在解析 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 | 請重試請求;未收取任何費用。 |