Überblick
Nutzen Sie die API, wenn Nachrichten durch Ihre eigenen Systeme ausgelöst werden: Bestellbestätigungen aus einem Shop, Codes aus einem Login-Formular, Erinnerungen von einem Buchungstool. Eine Anfrage an POST /v1/messages mit bis zu 10,000 Nummern erstellt eine Kampagne, belastet Ihr Guthaben und reiht jede Nachricht in die Warteschlange ein. Über die API erstellte Kampagnen erscheinen ebenfalls in der Konsole mit ihren Zustellberichten.
- Alle Beträge sind in US-Dollar als Zeichenketten mit fünf Dezimalstellen angegeben, zum Beispiel "0.00534".
- Telefonnummern sind international, mit oder ohne führendes +; Leerzeichen und Bindestriche werden entfernt.
- Zeiten sind im ISO-8601-Format in UTC, zum Beispiel 2026-12-01T09:00:00Z.
- Nachrichten- und Kampagnen-IDs sehen aus wie MSG-XXXX-XXXX-XXXX und CMP-XXXX-XXXX-XXXX.
Authentifizierung
Erstellen Sie einen API-Schlüssel unter API in der Konsole (Schlüssel werden nach Ihrer ersten Aufladung freigeschaltet). Senden Sie ihn im Authorization-Header jeder Anfrage. Ein Schlüssel sieht aus wie bso_api_ gefolgt von 36 Zeichen; er wird nur einmal angezeigt, speichern Sie ihn also als Geheimnis auf Ihrem Server. Das Erstellen eines neuen Schlüssels ersetzt den vorherigen sofort.
Authorization: Bearer bso_api_0123456789abcdef0123456789abcdef0123 Content-Type: application/json
Grenzwerte
| Anfragen | 300 pro Minute pro Schlüssel; mehr Anfragen erhalten die Antwort 429 rate_limited |
|---|---|
| Nummern pro Anfrage | 10,000 |
| Nachrichtentext | 2,000 Zeichen wie geschrieben, bis zu 10 Segmenten |
| Zeitplanung | send_at von 5 Minuten bis 60 Tage im Voraus |
| Sendetempo | 400 Nachrichten pro Minute pro Warteschlange |
| Webhook-Wiederholungsversuche | 9 Versuche über etwa 22 Stunden |
Nachrichten senden
POST/v1/messages
Sendet einen Text an eine oder mehrere Nummern. Die gesamten Kosten werden bei Annahme der Anfrage vom Guthaben abgebucht; jede nicht zustellbare Nachricht wird später erstattet. Die Antwort hat den Status 201 und enthält die Kampagne mit jeder Nachricht.
| Feld | Typ | Beschreibung |
|---|---|---|
toerforderlich | string | string[] | Eine Nummer oder ein Array von bis zu 10,000 Nummern im internationalen Format. Wiederholte Nummern werden einmal gesendet; unlesbare werden in skipped aufgeführt. |
texterforderlich | string | Die Nachricht, bis zu 2,000 Zeichen und 10 Segmenten. GSM-7-Text hat 160 Zeichen pro SMS, Unicode 70. |
send_atoptional | string | ISO-8601-Zeit zum Planen des Versands, 5 Minuten bis 60 Tage im Voraus. Lassen Sie es weg, um sofort zu senden. |
opt_outoptional | boolean | true fügt Ihre Abmeldezeile aus den Konsoleneinstellungen in einer neuen Zeile am Ende hinzu. |
client_refoptional | string | Ihre eigene Referenz, bis zu 64 Buchstaben, Ziffern und _ . : -. Sie wird in Berichten und Webhooks zurückgegeben. |
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" }
]
}Sendung kalkulieren
POST/v1/quote
Nimmt denselben Body wie /v1/messages und gibt die genauen Kosten, Segmente und Kodierung zurück, ohne etwas zu senden oder zu berechnen. Verwenden Sie es, um Ihren Benutzern einen Preis anzuzeigen, bevor sie bestätigen.
{
"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" } ]
}Guthaben
GET/v1/balance
{ "balance": "118.43120", "currency": "USD" }Preise
GET/v1/rates
GET/v1/rates/{country}
Der Preis eines SMS-Segments für jedes Ziel oder für einen zweistelligen ISO-Ländercode. Diese beiden Endpunkte sind öffentlich und benötigen keinen Schlüssel.
{ "country": "DE", "name": "Germany", "calling_code": "+49", "price": "0.01544", "currency": "USD", "per": "segment" }Nachrichtenstatus
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"
}Kampagnen
GET/v1/campaigns/{id}
GET/v1/campaigns/{id}/messages?offset=0&limit=100
Der erste gibt die Kampagne mit ihren Zählungen pro Status zurück; der zweite blättert durch ihre Nachrichten, bis zu 1.000 pro Seite. Von der Konsole gesendete Kampagnen können auf dieselbe Weise mit der auf ihrer Seite angezeigten ID gelesen werden.
Geplante Sendung stornieren
POST/v1/campaigns/{id}/cancel
Stoppt eine Kampagne, die mit send_at erstellt wurde und noch nicht begonnen hat. Jede Nachricht wird abgebrochen und die gesamten Kosten werden dem Guthaben gutgeschrieben. Eine bereits sendende Kampagne antwortet mit 409 not_scheduled.
Zustell-Webhooks
Fügen Sie unter API in der Konsole eine HTTPS-URL hinzu, um jeden endgültigen Status (delivered oder failed) als POST-Anfrage zu erhalten. Antworten Sie innerhalb von 8 Sekunden mit einem 2xx-Status. Andere Antworten und Timeouts werden nach 20 Sekunden, 1, 5 und 15 Minuten, dann 1, 3, 6 und 12 Stunden erneut versucht, insgesamt 9 Versuche. Dasselbe Ereignis kann zweimal eintreffen, verwenden Sie daher event_id, um es einmal zu verarbeiten.
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"
}
}Signatur überprüfen
Der Header enthält die Unix-Zeit t und die Signatur v1 = HMAC-SHA256 der Zeichenkette "t.raw_body" mit Ihrem Webhook-Geheimnis, in hex. Berechnen Sie sie über den Rohbody vor dem Parsen des JSON, vergleichen Sie in konstanter Zeit und lehnen Sie Ereignisse ab, die älter als fünf Minuten sind.
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 | Akzeptiert und wartet auf Verlassen der Warteschlange |
|---|---|
| submitted | An das Zielnetz übergeben |
| delivered | Vom Handgerät bestätigt (endgültig) |
| failed | Konnte nicht zugestellt werden; der Preis wird erstattet (endgültig) |
| canceled | Teil einer geplanten Kampagne, die vor dem Senden abgebrochen wurde (endgültig) |
Kampagnenstatus ist scheduled, sending, complete oder canceled.
Fehler
Fehler antworten mit einem HTTP-Status und einem JSON-Body: { "error": { "code": "insufficient_balance", "message": "..." } }. Nichts wird berechnet, wenn eine Anfrage mit einem Fehler antwortet.
| HTTP | Code | Bedeutung |
|---|---|---|
| 400 | invalid_request | Ein Feld fehlt oder ist ungültig; die Nachricht sagt, welches. |
| 400 | invalid_json | Der Body ist kein JSON. |
| 400 | no_valid_numbers | Keine der Nummern konnte gelesen werden. |
| 401 | unauthorized | Der Schlüssel fehlt, ist fehlerhaft oder wurde ersetzt. |
| 402 | insufficient_balance | Das Guthaben deckt die Kosten nicht; laden Sie auf und senden Sie erneut. |
| 404 | not_found | Unbekannte ID oder Endpunkt. |
| 409 | not_scheduled | Nur eine geplante Kampagne, die noch nicht gestartet wurde, kann abgebrochen werden. |
| 413 | too_many_recipients | Mehr als 10,000 Nummern in einer Anfrage. |
| 429 | rate_limited | Mehr als 300 Anfragen in einer Minute. |
| 500 | server_error | Wiederholen Sie die Anfrage; es wurde nichts belastet. |