一括SMS APIリファレンス

BulkSMSOnline REST APIを使うと、自社のソフトウェアから227か国へSMSを送信できます。料金、セグメントのルール、予約送信、返金はコンソールと同じです。リクエストとレスポンスはHTTPS上のJSONで、最終的な配信ステータスはすべて署名付きWebhookとして自社サーバーに通知できます。

ベースURLhttps://bulksmsonline.cc/api/v1

概要

メッセージを自社のシステムで発生させる場合に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必須国際形式の番号1件、または最大10,000件の番号の配列。重複した番号は1回だけ送信され、読み取れない番号はskippedに一覧表示されます。
text必須メッセージ本文で、最大2,000文字、10セグメントまで。GSM-7のテキストは1通のSMSで160文字、Unicodeは70文字です。
send_at任意送信を予約するISO 8601形式の時刻で、5分後から60日先まで。省略するとすぐに送信されます。
opt_out任意trueにすると、コンソールの設定にある配信停止の案内文が、末尾の新しい行に追加されます。
client_ref任意独自の参照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"}'
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]));
}

ステータス

queued受理済みで、キューから送信される順番を待っている状態
submitted宛先ネットワークに引き渡し済み
delivered端末が受信を確認済み(最終)
failed配信できず、料金は返金済み(最終)
canceled送信前にキャンセルされた予約キャンペーンの一部(最終)

キャンペーンのステータスはscheduled、sending、complete、canceledのいずれかです。

エラー

エラー時は、HTTPステータスとJSON本文が返されます:{ "error": { "code": "insufficient_balance", "message": "..." } }。エラーが返されたリクエストには、課金されません。

HTTPコード意味
400invalid_requestフィールドが不足しているか無効です。メッセージにどのフィールドか記載されます。
400invalid_json本文がJSONではありません。
400no_valid_numbers読み取れた番号がありません。
401unauthorizedキーがない、形式が正しくない、または置き換えられています。
402insufficient_balance残高が料金に足りません。チャージして、もう一度送信してください。
404not_foundIDまたはエンドポイントが不明です。
409not_scheduledキャンセルできるのは、まだ開始していない予約キャンペーンだけです。
413too_many_recipients1リクエストに10,000件を超える番号が含まれています。
429rate_limited1分間に300回を超えるリクエストがありました。
500server_errorリクエストを再試行してください。課金はされていません。

コンソールでAPIキーを取得

アカウントを作成し、初回チャージを追加して、APIでキーを生成します。最初のリクエストは、その1分後に送信できます。

アカウント作成
一括SMS APIリファレンス:RESTエンドポイントとWebhook | BulkSMSOnline