ภาพรวม
ใช้ 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 บรรจุ 160 อักขระต่อ SMS ส่วน 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}
ราคาของหนึ่งเซกเมนต์ SMS สำหรับทุกปลายทาง หรือสำหรับรหัสประเทศ 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
รายการแรกส่งคืนแคมเปญพร้อมจำนวนตามสถานะ รายการที่สองแบ่งหน้าข้อความของแคมเปญ สูงสุด 1,000 ต่อหน้า แคมเปญที่ส่งจากคอนโซลอ่านได้ด้วยวิธีเดียวกันโดยใช้ ID ที่แสดงในหน้าของแคมเปญ
ยกเลิกการส่งที่ตั้งเวลาไว้
POST/v1/campaigns/{id}/cancel
หยุดแคมเปญที่สร้างด้วย send_at และยังไม่เริ่ม ทุกข้อความถูกยกเลิกและค่าใช้จ่ายเต็มจำนวนคืนเข้ายอดเงิน แคมเปญที่กำลังส่งอยู่ตอบ 409 not_scheduled
Webhook รายงานการส่ง
เพิ่ม URL แบบ HTTPS ใต้ API ในคอนโซลเพื่อรับสถานะสุดท้ายแต่ละรายการ (delivered หรือ failed) เป็นคำขอ POST ตอบกลับด้วยสถานะ 2xx ใดก็ได้ภายใน 8 วินาที คำตอบอื่นและการหมดเวลาจะลองใหม่หลัง 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 = HMAC-SHA256 ของสตริง "t.raw_body" ด้วยความลับ webhook ของคุณ ในรูปเลขฐานสิบหก คำนวณจากเนื้อหาดิบก่อนแยกวิเคราะห์ 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 | ลองส่งคำขอใหม่ จะไม่มีการหักเงิน |