Ringkasan
Gunakan API saat pesan dipicu oleh sistem Anda sendiri: info pesanan dari toko, kode dari formulir login, pengingat dari alat pemesanan. Permintaan ke POST /v1/messages dengan hingga 10,000 nomor membuat satu kampanye, memotong saldo, dan mengantrekan setiap pesan. Kampanye yang dibuat lewat API juga muncul di konsol beserta laporan pengirimannya.
- Semua jumlah berupa dolar AS dalam bentuk string dengan lima desimal, misalnya "0.00534".
- Nomor telepon berformat internasional, dengan atau tanpa + di depan; spasi dan tanda hubung dihapus.
- Waktu berformat ISO 8601 dalam UTC, misalnya 2026-12-01T09:00:00Z.
- ID pesan dan kampanye berbentuk seperti MSG-XXXX-XXXX-XXXX dan CMP-XXXX-XXXX-XXXX.
Autentikasi
Buat kunci API di bagian API pada konsol (kunci aktif setelah isi saldo pertama Anda masuk). Kirim di header Authorization pada setiap permintaan. Kunci berbentuk bso_api_ diikuti 36 karakter; kunci hanya ditampilkan sekali, jadi simpan sebagai rahasia di server Anda. Membuat kunci baru langsung menggantikan kunci sebelumnya.
Authorization: Bearer bso_api_0123456789abcdef0123456789abcdef0123 Content-Type: application/json
Batas
| Permintaan | 300 per menit per kunci; selebihnya dijawab 429 rate_limited |
|---|---|
| Nomor per permintaan | 10,000 |
| Teks pesan | 2,000 karakter sesuai tulisan, hingga 10 segmen |
| Penjadwalan | send_at dari 5 menit hingga 60 hari ke depan |
| Kecepatan pengiriman | 400 pesan per menit per antrean |
| Percobaan ulang webhook | 9 percobaan selama sekitar 22 jam |
Kirim pesan
POST/v1/messages
Mengirim satu teks ke satu nomor atau lebih. Seluruh biaya diambil dari saldo saat permintaan diterima; setiap pesan yang tidak dapat dikirim dikembalikan kemudian. Responsnya berstatus 201 dan berisi kampanye beserta setiap pesan.
| Field | Tipe | Deskripsi |
|---|---|---|
towajib | string | string[] | Satu nomor atau array hingga 10,000 nomor dalam format internasional. Nomor ganda dikirim sekali; yang tidak terbaca dicantumkan di skipped. |
textwajib | string | Pesannya, hingga 2,000 karakter dan 10 segmen. Teks GSM-7 memuat 160 karakter per SMS, Unicode 70. |
send_atopsional | string | Waktu ISO 8601 untuk menjadwalkan pengiriman, 5 menit hingga 60 hari ke depan. Kosongkan untuk mengirim sekarang. |
opt_outopsional | boolean | true menambahkan baris berhenti berlangganan dari pengaturan konsol Anda di baris baru pada bagian akhir. |
client_refopsional | string | Referensi Anda sendiri, hingga 64 huruf, angka, dan _ . : -. Dikembalikan dalam laporan dan 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" }
]
}Estimasi pengiriman
POST/v1/quote
Menerima isi yang sama dengan /v1/messages dan mengembalikan biaya pasti, segmen, dan encoding tanpa mengirim atau memotong apa pun. Gunakan untuk menampilkan harga kepada pengguna Anda sebelum mereka mengonfirmasi.
{
"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" } ]
}Saldo
GET/v1/balance
{ "balance": "118.43120", "currency": "USD" }Tarif
GET/v1/rates
GET/v1/rates/{country}
Harga satu segmen SMS untuk setiap tujuan, atau untuk satu kode negara ISO dua huruf. Kedua endpoint ini publik dan tidak memerlukan kunci.
{ "country": "DE", "name": "Germany", "calling_code": "+49", "price": "0.01544", "currency": "USD", "per": "segment" }Status pesan
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"
}Kampanye
GET/v1/campaigns/{id}
GET/v1/campaigns/{id}/messages?offset=0&limit=100
Yang pertama mengembalikan kampanye beserta jumlah per status; yang kedua menelusuri pesannya per halaman, hingga 1,000 per halaman. Kampanye yang dikirim dari konsol dapat dibaca dengan cara yang sama memakai id yang ditampilkan di halamannya.
Batalkan pengiriman terjadwal
POST/v1/campaigns/{id}/cancel
Menghentikan kampanye yang dibuat dengan send_at dan belum dimulai. Setiap pesan dibatalkan dan seluruh biaya kembali ke saldo. Kampanye yang sudah berjalan dijawab 409 not_scheduled.
Webhook pengiriman
Tambahkan URL HTTPS di bagian API pada konsol untuk menerima setiap status akhir (delivered atau failed) sebagai permintaan POST. Jawab dengan status 2xx apa pun dalam 8 detik. Jawaban lain dan timeout dicoba ulang setelah 20 detik, 1, 5, dan 15 menit, lalu 1, 3, 6, dan 12 jam, total 9 percobaan. Kejadian yang sama bisa datang dua kali, jadi gunakan event_id agar diproses sekali.
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"
}
}Verifikasi tanda tangan
Header berisi waktu Unix t dan tanda tangan v1 = HMAC-SHA256 dari string "t.raw_body" dengan rahasia webhook Anda, dalam hex. Hitung atas isi mentah sebelum mengurai JSON, bandingkan dalam waktu konstan, dan tolak kejadian yang lebih lama dari lima menit.
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]);
}Status
| queued | Diterima dan menunggu keluar dari antrean |
|---|---|
| submitted | Diserahkan ke jaringan tujuan |
| delivered | Dikonfirmasi oleh ponsel (final) |
| failed | Tidak dapat dikirim; harga dikembalikan (final) |
| canceled | Bagian dari kampanye terjadwal yang dibatalkan sebelum dikirim (final) |
Status kampanye adalah scheduled, sending, complete, atau canceled.
Error
Error dijawab dengan status HTTP dan isi JSON: { "error": { "code": "insufficient_balance", "message": "..." } }. Tidak ada biaya yang dipotong saat permintaan dijawab dengan error.
| HTTP | Kode | Arti |
|---|---|---|
| 400 | invalid_request | Ada field yang hilang atau tidak valid; pesannya menyebutkan yang mana. |
| 400 | invalid_json | Isi permintaan bukan JSON. |
| 400 | no_valid_numbers | Tidak ada nomor yang dapat dibaca. |
| 401 | unauthorized | Kunci hilang, salah format, atau sudah diganti. |
| 402 | insufficient_balance | Saldo tidak mencukupi biaya; isi saldo lalu kirim lagi. |
| 404 | not_found | ID atau endpoint tidak dikenal. |
| 409 | not_scheduled | Hanya kampanye terjadwal yang belum dimulai yang dapat dibatalkan. |
| 413 | too_many_recipients | Lebih dari 10,000 nomor dalam satu permintaan. |
| 429 | rate_limited | Lebih dari 300 permintaan dalam semenit. |
| 500 | server_error | Ulangi permintaan; tidak ada biaya yang dipotong. |