Pangkalahatang-ideya
Gamitin ang API kapag ang mga mensahe ay na-trigger ng iyong sariling sistema: mga update ng order mula sa shop, mga code mula sa login form, mga paalala mula sa booking tool. Ang request sa POST /v1/messages na may hanggang 10,000 na numero ay gumagawa ng isang kampanya, nag-charge sa balance, at nag-queue ng bawat mensahe. Ang mga kampanyang ginawa ng API ay lumalabas din sa console kasama ang kanilang mga delivery report.
- Lahat ng halaga ay sa US dollars bilang string na may limang decimal, halimbawa "0.00534".
- Ang mga numero ng telepono ay internasyonal, mayroon o walang + sa unahan; ang mga espasyo at gitling ay tinatanggal.
- Ang mga oras ay ISO 8601 sa UTC, halimbawa 2026-12-01T09:00:00Z.
- Ang mga message at campaign id ay mukhang MSG-XXXX-XXXX-XXXX at CMP-XXXX-XXXX-XXXX.
Pagpapatotoo
Gumawa ng API key sa ilalim ng API sa console (bubuksan ang mga key pagkatapos maidagdag ang iyong unang top-up). Ipadala ito sa Authorization header ng bawat request. Ang isang key ay mukhang bso_api_ na sinusundan ng 36 na karakter; ito ay ipinapakita minsan lamang, kaya itago ito bilang sekreto sa iyong server. Ang paggawa ng bagong key ay papalit agad sa nauna.
Authorization: Bearer bso_api_0123456789abcdef0123456789abcdef0123 Content-Type: application/json
Mga Limitasyon
| Mga Request | 300 kada minuto bawat key; higit pa ay magbibigay ng sagot na 429 rate_limited |
|---|---|
| Mga numero bawat request | 10,000 |
| Teksto ng mensahe | 2,000 na karakter ayon sa pagkakasulat, hanggang 10 na segment |
| Pag-iiskedyul | send_at mula 5 minuto hanggang 60 araw sa hinaharap |
| Bilis ng pagpapadala | 400 na mensahe kada minuto bawat queue |
| Mga retry ng webhook | 9 na pagsubok sa loob ng mga 22 oras |
Magpadala ng mensahe
POST/v1/messages
Nagpapadala ng isang text sa isa o higit pang numero. Ang buong halaga ay ibabawas sa balance kapag tinanggap ang request; bawat mensaheng hindi maideliver ay irerefund mamaya. Ang sagot ay may status 201 at ang campaign kasama ang bawat mensahe.
| Field | Uri | Paglalarawan |
|---|---|---|
tokailangan | string | string[] | Isang numero o array ng hanggang 10,000 numero sa international format. Ang mga inulit na numero ay ipapadala minsan; ang mga hindi mabasa ay nakalista sa skipped. |
textkailangan | string | Ang mensahe, hanggang 2,000 characters at 10 segments. Ang GSM-7 text ay may 160 characters bawat SMS, Unicode 70. |
send_atopsyonal | string | ISO 8601 time para iskedyul ang pagpapadala, 5 minuto hanggang 60 araw sa hinaharap. Iwanang blangko para ipadala ngayon. |
opt_outopsyonal | boolean | true nagdaragdag ng iyong opt-out line mula sa console settings sa bagong linya sa dulo. |
client_refopsyonal | string | Iyong sariling reference, hanggang 64 letra, digit at _ . : -. Ito ay ibabalik sa reports at 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" }
]
}Mag-quote ng pagpapadala
POST/v1/quote
Kumukuha ng parehong body tulad ng /v1/messages at nagbabalik ng eksaktong halaga, segments at encoding nang hindi nagpapadala o nagchacharge. Gamitin ito para ipakita ang presyo sa iyong mga user bago sila mag-confirm.
{
"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" } ]
}Balance
GET/v1/balance
{ "balance": "118.43120", "currency": "USD" }Mga Rate
GET/v1/rates
GET/v1/rates/{country}
Ang presyo ng isang SMS segment para sa bawat destinasyon, o para sa isang two-letter ISO country code. Ang dalawang endpoint na ito ay public at hindi kailangan ng key.
{ "country": "DE", "name": "Germany", "calling_code": "+49", "price": "0.01544", "currency": "USD", "per": "segment" }Status ng Mensahe
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"
}Mga Kampanya
GET/v1/campaigns/{id}
GET/v1/campaigns/{id}/messages?offset=0&limit=100
Ang una ay nagbabalik ng campaign kasama ang counts bawat status; ang pangalawa ay nagpa-page through sa mga mensahe nito, hanggang 1,000 bawat page. Ang mga campaign na ipinadala mula sa console ay mababasa sa parehong paraan gamit ang id na ipinakita sa kanilang page.
Kanselahin ang naka-schedule na pagpapadala
POST/v1/campaigns/{id}/cancel
Pinipigil ang isang campaign na ginawa gamit ang send_at at hindi pa nagsisimula. Bawat mensahe ay kinacancel at ang buong halaga ay bumabalik sa balance. Ang campaign na nagpapadala na ay sumasagot ng 409 not_scheduled.
Mga Webhook sa Paghahatid
Magdagdag ng HTTPS URL sa ilalim ng API sa console para tumanggap ng bawat final status (delivered o failed) bilang POST request. Sumagot ng kahit anong 2xx status sa loob ng 8 segundo. Ang ibang sagot at timeout ay iri-retry pagkatapos ng 20 segundo, 1, 5 at 15 minuto, tapos 1, 3, 6 at 12 oras, 9 tries sa kabuuan. Ang parehong event ay maaaring dumating ng dalawang beses, kaya gamitin ang event_id para iprocess ito minsan.
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"
}
}I-verify ang signature
Ang header ay naglalaman ng Unix time t at ang signature v1 = HMAC-SHA256 ng string na "t.raw_body" gamit ang iyong webhook secret, sa hex. I-compute ito sa raw body bago i-parse ang JSON, ihambing sa constant time at i-reject ang mga event na mas matanda sa limang minuto.
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]);
}Mga Status
| queued | Tinanggap at naghihintay na umalis sa queue |
|---|---|
| submitted | Naipasa sa destination network |
| delivered | Kumpirmado ng handset (final) |
| failed | Hindi naideliver; ang presyo ay irerefund (final) |
| canceled | Bahagi ng isang naka-schedule na campaign na kinancel bago ipadala (final) |
Ang status ng campaign ay scheduled, sending, complete o canceled.
Mga Error
Ang mga error ay sumasagot ng HTTP status at JSON body: { "error": { "code": "insufficient_balance", "message": "..." } }. Walang sisingilin kapag ang request ay sumagot ng error.
| HTTP | Code | Kahulugan |
|---|---|---|
| 400 | invalid_request | Nawawala o hindi valid ang isang field; sinasabi ng mensahe kung alin. |
| 400 | invalid_json | Ang body ay hindi JSON. |
| 400 | no_valid_numbers | Walang numero ang nabasa. |
| 401 | unauthorized | Nawawala, may sira, o napalitan ang key. |
| 402 | insufficient_balance | Hindi sapat ang balance para sa halaga; mag-top up at mag-send ulit. |
| 404 | not_found | Hindi kilalang id o endpoint. |
| 409 | not_scheduled | Ang scheduled campaign na hindi pa nagsisimula lang ang maaaring i-cancel. |
| 413 | too_many_recipients | Higit sa 10,000 na numero sa isang request. |
| 429 | rate_limited | Higit sa 300 na request sa isang minuto. |
| 500 | server_error | Subukan ulit ang request; walang na-charge. |