概要
メッセージを自社のシステムで発生させる場合にAPIを使います。たとえば、ショップからの注文通知、ログインフォームからのコード、予約ツールからのリマインドです。最大10,000件の番号を指定したPOST /v1/messagesへのリクエストで、キャンペーンが1つ作成され、残高が課金され、すべてのメッセージがキューに入ります。APIで作成したキャンペーンも、配信レポートとともにコンソールに表示されます。
- 金額はすべて米ドルで、小数第5位までの文字列です。例:"0.00534"。
- 電話番号は国際形式で、先頭の+はあってもなくても構いません。スペースとハイフンは削除されます。
- 時刻はUTCのISO 8601形式です。例: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が返ります |
|---|---|
| 1リクエストあたりの番号数 | 10,000 |
| メッセージ本文 | 入力どおりで2,000文字、最大10セグメント |
| 予約送信 | send_atは5分後から60日先まで |
| 送信ペース | キューごとに毎分400通 |
| Webhookの再送 | 約22時間で9回 |
メッセージ送信
POST/v1/messages
1つのメッセージを1件以上の番号へ送信します。リクエストが受理された時点で、全額が残高から差し引かれます。配信できなかったメッセージは、後で返金されます。レスポンスはステータス201で、キャンペーンとすべてのメッセージが含まれます。
| フィールド | 型 | 説明 |
|---|---|---|
to必須 | string | string[] | 国際形式の番号1件、または最大10,000件の番号の配列。重複した番号は1回だけ送信され、読み取れない番号はskippedに一覧表示されます。 |
text必須 | string | メッセージ本文で、最大2,000文字、10セグメントまで。GSM-7のテキストは1通のSMSで160文字、Unicodeは70文字です。 |
send_at任意 | string | 送信を予約するISO 8601形式の時刻で、5分後から60日先まで。省略するとすぐに送信されます。 |
opt_out任意 | boolean | trueにすると、コンソールの設定にある配信停止の案内文が、末尾の新しい行に追加されます。 |
client_ref任意 | string | 独自の参照ID。英字、数字、_ . : -で最大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}
すべての宛先、または2文字のISO国コードで指定した1つの宛先について、SMS 1セグメントの料金を返します。この2つのエンドポイントは公開されており、キーは不要です。
{ "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つ目はステータスごとの件数つきでキャンペーンを返し、2つ目はそのメッセージをページ単位(1ページ最大1,000件)で返します。コンソールから送信したキャンペーンも、そのページに表示される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回試行されます。同じイベントが2回届くことがあるため、event_idを使って1回だけ処理してください。
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が含まれます。v1は、文字列"t.raw_body"に対してWebhookシークレットを使って計算したHMAC-SHA256で、十六進数です。JSONを解析する前に生の本文に対して計算し、定数時間で比較して、5分より古いイベントは拒否してください。
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 | 1リクエストに10,000件を超える番号が含まれています。 |
| 429 | rate_limited | 1分間に300回を超えるリクエストがありました。 |
| 500 | server_error | リクエストを再試行してください。課金はされていません。 |