概述
当消息由您自己的系统触发时使用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个号码的数组,需为国际格式。重复号码仅发送一次;无法识别的号码会列在跳过列表中。 |
text必填 | string | 消息内容,最多2,000个字符和10条短信分段。GSM-7编码每条短信最多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国家代码的每条短信分段价格。这两个端点为公开接口,无需密钥。
{ "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
第一个接口返回群发活动及各状态计数;第二个接口分页查询活动消息,每页最多1000条。从控制台发送的活动也可通过页面显示的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 | 请重试请求,未产生任何费用。 |