Tổng quan
Sử dụng API khi tin nhắn được kích hoạt bởi hệ thống của bạn: cập nhật đơn hàng từ cửa hàng, mã từ biểu mẫu đăng nhập, nhắc nhở từ công cụ đặt chỗ. Một yêu cầu POST /v1/messages với tối đa 10,000 số sẽ tạo một chiến dịch, trừ tiền từ số dư và xếp hàng mỗi tin nhắn. Các chiến dịch tạo bởi API cũng xuất hiện trong bảng điều khiển cùng với báo cáo gửi thành công của chúng.
- Tất cả số tiền đều là đô la Mỹ dưới dạng chuỗi với năm chữ số thập phân, ví dụ "0.00534".
- Số điện thoại là quốc tế, có hoặc không có dấu + ở đầu; khoảng trắng và dấu gạch ngang sẽ bị loại bỏ.
- Thời gian theo chuẩn ISO 8601 và múi giờ UTC, ví dụ 2026-12-01T09:00:00Z.
- ID tin nhắn và chiến dịch có dạng MSG-XXXX-XXXX-XXXX và CMP-XXXX-XXXX-XXXX.
Xác thực
Tạo một khóa API trong mục API trên bảng điều khiển (khóa sẽ mở sau khi bạn nạp tiền lần đầu). Gửi khóa này trong tiêu đề Authorization của mỗi yêu cầu. Một khóa có dạng bso_api_ theo sau bởi 36 ký tự; nó chỉ hiển thị một lần, vì vậy hãy lưu trữ nó như một bí mật trên máy chủ của bạn. Tạo khóa mới sẽ thay thế khóa cũ ngay lập tức.
Authorization: Bearer bso_api_0123456789abcdef0123456789abcdef0123 Content-Type: application/json
Giới hạn
| Yêu cầu | 300 mỗi phút trên mỗi khóa; nhiều hơn sẽ nhận phản hồi 429 rate_limited |
|---|---|
| Số lượng số mỗi yêu cầu | 10,000 |
| Nội dung tin nhắn | 2,000 ký tự như đã viết, tối đa 10 phân đoạn |
| Lập lịch | send_at từ 5 phút đến 60 ngày sau |
| Tốc độ gửi | 400 tin nhắn mỗi phút trên mỗi hàng đợi |
| Thử lại webhook | 9 lần thử trong khoảng 22 giờ |
Gửi tin nhắn
POST/v1/messages
Gửi một tin nhắn đến một hoặc nhiều số. Toàn bộ chi phí được trừ từ số dư khi yêu cầu được chấp nhận; mỗi tin nhắn không thể gửi được sẽ được hoàn tiền sau. Phản hồi có trạng thái 201 và chiến dịch cùng với từng tin nhắn.
| Trường | Loại | Mô tả |
|---|---|---|
tobắt buộc | string | string[] | Một số hoặc một mảng tối đa 10,000 số theo định dạng quốc tế. Các số lặp lại chỉ được gửi một lần; các số không đọc được sẽ liệt kê trong skipped. |
textbắt buộc | string | Tin nhắn, tối đa 2,000 ký tự và 10 phân đoạn. Văn bản GSM-7 chứa 160 ký tự mỗi SMS, Unicode 70. |
send_attùy chọn | string | Thời gian ISO 8601 để lên lịch gửi, từ 5 phút đến 60 ngày sau. Bỏ trống để gửi ngay. |
opt_outtùy chọn | boolean | true thêm dòng từ chối nhận từ cài đặt console vào một dòng mới ở cuối. |
client_reftùy chọn | string | Tham chiếu riêng của bạn, tối đa 64 chữ cái, chữ số và _ . : -. Nó được trả về trong báo cáo và webhooks. |
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" }
]
}Báo giá gửi
POST/v1/quote
Lấy cùng nội dung như /v1/messages và trả về chi phí chính xác, phân đoạn và mã hóa mà không gửi hoặc tính phí. Sử dụng để hiển thị giá cho người dùng của bạn trước khi họ xác nhận.
{
"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" } ]
}Số dư
GET/v1/balance
{ "balance": "118.43120", "currency": "USD" }Biểu phí
GET/v1/rates
GET/v1/rates/{country}
Giá của một phân đoạn SMS cho mọi điểm đến, hoặc cho một mã quốc gia ISO hai chữ cái. Hai endpoint này là công khai và không cần khóa.
{ "country": "DE", "name": "Germany", "calling_code": "+49", "price": "0.01544", "currency": "USD", "per": "segment" }Trạng thái tin nhắn
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"
}Chiến dịch
GET/v1/campaigns/{id}
GET/v1/campaigns/{id}/messages?offset=0&limit=100
Đầu tiên trả về chiến dịch với số lượng theo trạng thái; thứ hai phân trang qua các tin nhắn, tối đa 1.000 mỗi trang. Chiến dịch gửi từ console có thể đọc theo cách tương tự với id hiển thị trên trang của chúng.
Hủy lịch gửi đã đặt
POST/v1/campaigns/{id}/cancel
Dừng một chiến dịch được tạo với send_at và chưa bắt đầu. Mọi tin nhắn bị hủy và toàn bộ chi phí trả về số dư. Chiến dịch đang gửi trả lời 409 not_scheduled.
Webhook giao tin
Thêm một URL HTTPS trong mục API trên console để nhận mỗi trạng thái cuối (đã gửi hoặc thất bại) dưới dạng yêu cầu POST. Trả lời với bất kỳ trạng thái 2xx nào trong vòng 8 giây. Các phản hồi khác và timeouts được thử lại sau 20 giây, 1, 5 và 15 phút, sau đó 1, 3, 6 và 12 giờ, tổng cộng 9 lần thử. Sự kiện tương tự có thể đến hai lần, vì vậy hãy sử dụng event_id để xử lý một lần.
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"
}
}Xác minh chữ ký
Header chứa thời gian Unix t và chữ ký v1 = HMAC-SHA256 của chuỗi "t.raw_body" với webhook secret của bạn, ở dạng hex. Tính toán nó trên raw body trước khi phân tích JSON, so sánh trong thời gian không đổi và từ chối các sự kiện cũ hơn năm phút.
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]);
}Trạng thái
| queued | Đã chấp nhận và đang chờ rời hàng đợi |
|---|---|
| submitted | Đã chuyển đến mạng đích |
| delivered | Đã xác nhận bởi thiết bị (cuối cùng) |
| failed | Không thể gửi; giá được hoàn lại (cuối cùng) |
| canceled | Một phần của chiến dịch đã lên lịch bị hủy trước khi gửi (cuối cùng) |
Trạng thái chiến dịch là scheduled, sending, complete hoặc canceled.
Lỗi
Lỗi trả lời với trạng thái HTTP và nội dung JSON: { "error": { "code": "insufficient_balance", "message": "..." } }. Không tính phí khi yêu cầu trả lời với lỗi.
| HTTP | Mã | Ý nghĩa |
|---|---|---|
| 400 | invalid_request | Thiếu một trường hoặc không hợp lệ; tin nhắn cho biết trường nào. |
| 400 | invalid_json | Nội dung không phải là JSON. |
| 400 | no_valid_numbers | Không thể đọc được số điện thoại nào. |
| 401 | unauthorized | Khóa bị thiếu, sai định dạng hoặc đã bị thay thế. |
| 402 | insufficient_balance | Số dư không đủ để thanh toán; nạp tiền và gửi lại. |
| 404 | not_found | ID hoặc endpoint không xác định. |
| 409 | not_scheduled | Chỉ có thể hủy một chiến dịch đã lên lịch nhưng chưa bắt đầu. |
| 413 | too_many_recipients | Nhiều hơn 10,000 số trong một yêu cầu. |
| 429 | rate_limited | Nhiều hơn 300 yêu cầu trong một phút. |
| 500 | server_error | Thử lại yêu cầu; không có gì bị tính phí. |