Schnellstart
- 1. Schlüssel erstellen. Im Arbeitsbereich API und Webhooks öffnen. Schlüssel werden nach Ihrer ersten Guthabenaufladung freigeschaltet; jeder Schlüssel wird einmal angezeigt, kopieren Sie ihn also oder speichern Sie ihn als Textdatei.
- 2. Kosten prüfen. Senden Sie denselben Body als POST an
/costs, um Empfänger, Segmente und den Gesamtbetrag ohne Versand zu sehen. - 3. Senden. Senden Sie es als POST an
/sends. Die Antwort enthält die Sendungs-ID; Nachrichten werden sofort oder zum Zeitpunktsend_atversendet.
curl https://sendsmsonline.cc/api/v1/sends \
-H "Authorization: Bearer $SSO_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "recipients": [ { "to": "+447700900461", "fields": { "first_name": "Tom" } }, { "to": "+12015550142", "fields": { "first_name": "Ava" } } ], "text": "Hi {{first_name}}, your table is ready at 7:30.", "name": "Friday bookings", "client_ref": "bookings-1107" }'const res = await fetch("https://sendsmsonline.cc/api/v1/sends", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SSO_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
recipients: [{ to: "+447700900461", fields: { first_name: "Tom" } }],
text: "Hi {{first_name}}, your table is ready at 7:30.",
}),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error.message);
console.log(data.send.id, data.send.cost_usd);import os, requests
r = requests.post(
"https://sendsmsonline.cc/api/v1/sends",
headers={"Authorization": f"Bearer {os.environ['SSO_API_KEY']}"},
json={
"recipients": [{"to": "+447700900461", "fields": {"first_name": "Tom"}}],
"text": "Hi {{first_name}}, your table is ready at 7:30.",
},
timeout=30,
)
data = r.json()
if r.status_code >= 400:
raise RuntimeError(data["error"]["message"])
print(data["send"]["id"], data["send"]["cost_usd"])<?php
$ch = curl_init("https://sendsmsonline.cc/api/v1/sends");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer " . getenv("SSO_API_KEY"),
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"recipients" => [["to" => "+447700900461", "fields" => ["first_name" => "Tom"]]],
"text" => "Hi {{first_name}}, your table is ready at 7:30.",
]),
]);
$data = json_decode(curl_exec($ch), true);
echo $data["send"]["id"] ?? $data["error"]["message"];Authentifizierung
Senden Sie Ihren Schlüssel im Authorization-Header: Authorization: Bearer sso_live_.... Schlüssel sind 49 Zeichen lang und beginnen mit sso_live_. Bis zu 5 Schlüssel pro Konto; widerrufen Sie einen im Arbeitsbereich und er funktioniert sofort nicht mehr. Die Preisliste ist öffentlich und benötigt keinen Schlüssel.
Fehler
Fehler verwenden HTTP-Statuscodes und einen JSON-Body mit einem stabilen Code und einem Satz, den Sie einer Person zeigen können. Es wird nichts berechnet, wenn eine Anfrage fehlschlägt.
HTTP/1.1 402 Payment Required
{ "error": { "code": "insufficient_credit", "message": "Short by $3.20. Add funds to send.", "total": "8.62480", "credit": "5.42000" } }| Feld | Typ | Bedeutung |
|---|---|---|
| 400 | invalid_json, text_missing, recipients_missing, too_many_recipients, too_many_segments, schedule_range, bad_send_at | Die Anfrage muss geändert werden. |
| 401 | unauthorized | Fehlender, falscher oder widerrufener Schlüssel. |
| 402 | insufficient_credit | Nicht genug Guthaben für den Gesamtbetrag. |
| 404 | not_found | Keine solche Sendung, Nachricht, Land oder Endpunkt. |
| 409 | not_cancelable | Der Versand wurde bereits gestartet. |
| 429 | rate_limited | Mehr als 240 Anfragen pro Minute. |
Limits
- 240 Anfragen pro Minute pro Schlüssel, angezeigt in X-RateLimit-Limit, X-RateLimit-Remaining und X-RateLimit-Reset (Unix-Sekunden).
- 5,000 Nummern pro Anfrage, 2,000 Zeichen pro Text, 10 Segmente pro Nachricht.
- Nachrichten werden mit bis zu 300 pro Minute pro Konto versendet.
- send_at von 10 Minuten bis 30 Tage im Voraus.
Preise
GET/api/v1/prices
GET/api/v1/prices/{country}
Der Preis pro Nachricht (ein Segment) für jedes Land, in USD mit 5 Dezimalstellen. Kein Schlüssel erforderlich.
{ "price": { "country": "GB", "name": "United Kingdom", "dial_code": "+44", "price_usd": "0.00868" }, "updated": "2026-11-07" }Kontoguthaben
GET/api/v1/account
{ "account": "SO8K2M4Q7X9TBA", "credit_usd": "41.27660", "currency": "USD" }Kosten schätzen
POST/api/v1/costs
Nimmt denselben Körper wie ein Versand und gibt die Kosten zurück, ohne zu senden oder zu berechnen. Der zusammengeführte Text jedes Empfängers bestimmt seine Segmente, sodass die Summe exakt ist.
{
"recipients": 2, "segments_min": 1, "segments_max": 1, "encoding": "gsm7", "messages_billed": 2,
"total_usd": "0.01426", "credit_usd": "41.27660", "enough_credit": true,
"countries": [ { "country": "GB", "recipients": 1, "price_usd": "0.00868", "subtotal_usd": "0.00868" },
{ "country": "US", "recipients": 1, "price_usd": "0.00558", "subtotal_usd": "0.00558" } ],
"dropped": [], "too_long": null
}Sendung erstellen
POST/api/v1/sends
Ein Text an eine oder mehrere Nummern. Das Guthaben für den gesamten Versand wird abgebucht, wenn er akzeptiert wird; fehlgeschlagene Nachrichten werden einzeln erstattet.
| Feld | Typ | Bedeutung |
|---|---|---|
| to | string or array | Nummern im internationalen Format (+447700900461). Verwenden Sie dies oder Empfänger. |
| recipients | array | Objekte mit to und fields, für Zusammenführungsfelder: { "to": "+1...", "fields": { "first_name": "Ava" } }. |
| text | string | Die Nachricht, bis zu 2,000 Zeichen. {{field}} wird pro Empfänger ersetzt; fehlende Werte werden leer. |
| send_at | string | Optionale ISO 8601 Zeit mit Offset oder Z. Ohne sie beginnt der Versand sofort. |
| name | string | Optionales Label, das im Arbeitsbereich angezeigt wird (80 Zeichen). |
| client_ref | string | Optionaler Verweis, der in Webhooks zurückgegeben wird (64 Zeichen). |
| opt_out | boolean | Fügt die in den Einstellungen gespeicherte Opt-Out-Zeile am Ende des Textes hinzu. |
| default_country | string | ISO-Code für Nummern, die ohne Landescode geschrieben sind. |
HTTP/1.1 201 Created
{
"send": { "id": "CA7Q29XMAB4DFK", "status": "queued", "source": "api", "recipients": 2, "segments_max": 1,
"encoding": "gsm7", "cost_usd": "0.01426", "refunded_usd": "0.00000", "client_ref": "bookings-1107",
"send_at": null, "created_at": "2026-11-07T10:12:03.120Z", "started_at": null, "finished_at": null },
"dropped": [ { "value": "+4412", "code": "too_short", "message": "Too short" } ]
}Nummern, die nicht gesendet werden können, sind in dropped aufgeführt und werden nie berechnet. Codes: no_country_code, too_short, too_long, not_a_number, wrong_length, not_covered, duplicate und suppressed (auf Ihrer Nicht-Senden-Liste).
Sendungen abrufen oder auflisten
GET/api/v1/sends/{id}
GET/api/v1/sends?limit=25&before={id}
Ein einzelner Versand enthält counts pro Nachrichtenstatus. Listen sind nach Neuheit sortiert; übergeben Sie next_before als before für die nächste Seite. Versand aus dem Arbeitsbereich erscheint hier ebenfalls, mit source: "web".
Nachrichten
GET/api/v1/sends/{id}/messages?status=failed&limit=100
GET/api/v1/messages/{id}
{ "message": { "id": "MS3D8RW0K2QJ7N", "send_id": "CA7Q29XMAB4DFK", "to": "+447700900461", "country": "GB",
"segments": 1, "price_usd": "0.00868", "status": "delivered", "reason": null, "reason_text": null,
"created_at": "2026-11-07T10:12:03.120Z", "sent_at": "2026-11-07T10:12:04.010Z", "done_at": "2026-11-07T10:12:09.400Z" } }Sendung stornieren
POST/api/v1/sends/{id}/cancel
Nur geplante Versand, die noch nicht gestartet sind, können storniert werden. Die vollen Kosten werden Ihrem Guthaben gutgeschrieben und die Antwort enthält refunded_usd.
Webhooks
Setzen Sie eine HTTPS-URL im Arbeitsbereich (API und Webhooks). Wir POSTen ein Ereignis, wenn eine Nachricht zugestellt wird oder fehlschlägt, für Versand sowohl über die API als auch den Arbeitsbereich. Antworten Sie mit einem beliebigen 2xx-Status innerhalb von 10 Sekunden. Fehlgeschlagene Zustellungen werden insgesamt bis zu 7 Mal erneut versucht: sofort, dann nach 1 Minute, 5 Minuten, 30 Minuten, 1 Stunde, 2 Stunden und 3 Stunden. Der Arbeitsbereich zeigt jeden Versuch mit seinem Status und der Antwort an.
POST https://your-server.example/sms-events
X-SSO-Event: message.delivered
X-SSO-Delivery: evt_5a0c9e31d2b7f4a68c
X-SSO-Timestamp: 1794132729
X-SSO-Signature: sha256=8f1c...e42a
{ "id": "evt_5a0c9e31d2b7f4a68c", "event": "message.delivered", "created": "2026-11-07T10:12:09.000Z",
"data": { "message": { "id": "MS3D8RW0K2QJ7N", "send_id": "CA7Q29XMAB4DFK", "to": "+447700900461", "country": "GB",
"segments": 1, "price_usd": "0.00868", "status": "delivered", "reason": null, "reason_text": null,
"client_ref": "bookings-1107", "created_at": "...", "sent_at": "...", "done_at": "..." } } }Die Signatur ist HMAC-SHA256 von timestamp + "." + raw body mit Ihrem Signaturgeheimnis (es beginnt mit sso_whk_). Überprüfen Sie es am Rohkörper vor dem Parsen, lehnen Sie Zeitstempel ab, die älter als 5 Minuten sind, und verwenden Sie die Liefer-ID, um Wiederholungen zu ignorieren.
import crypto from "node:crypto";
// rawBody: the request body exactly as received (a string), before JSON.parse
export function verify(rawBody, headers, secret) {
const ts = headers["x-sso-timestamp"];
const sig = headers["x-sso-signature"] || "";
const want = "sha256=" + crypto.createHmac("sha256", secret).update(`${ts}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
return fresh && sig.length === want.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(want));
}import hashlib, hmac, time
def verify(raw_body: bytes, headers, secret: str) -> bool:
ts = headers.get("X-SSO-Timestamp", "")
sig = headers.get("X-SSO-Signature", "")
mac = hmac.new(secret.encode(), f"{ts}.".encode() + raw_body, hashlib.sha256)
want = "sha256=" + mac.hexdigest()
fresh = ts.isdigit() and abs(time.time() - int(ts)) < 300
return fresh and hmac.compare_digest(sig, want)<?php
function verify(string $rawBody, string $ts, string $sig, string $secret): bool {
$want = "sha256=" . hash_hmac("sha256", $ts . "." . $rawBody, $secret);
return ctype_digit($ts) && abs(time() - (int)$ts) < 300 && hash_equals($want, $sig);
}Ereignisse: message.delivered, message.failed und test von der Schaltfläche Testereignis senden.
Status und Gründe
| Feld | Typ | Bedeutung |
|---|---|---|
| queued | status | Warten darauf, unsere Warteschlange zu verlassen. |
| sent | status | An das Netzwerk übergeben, warte auf den Zustellbericht. Die meisten Berichte kommen innerhalb von Minuten an; einige Netzwerke benötigen bis zu 24 Stunden. |
| delivered | status | Das Netzwerk hat bestätigt, dass das Telefon die Nachricht erhalten hat. |
| failed | status | Nicht zugestellt. Der Betrag wurde auf Ihr Guthaben zurückerstattet. |
| canceled | status | Nicht gesendet, da Sie den Vorgang abgebrochen haben. Rückerstattet. |
Fehlergründe (das Feld reason):
| Feld | Typ | Bedeutung |
|---|---|---|
| network_error | reason | Netzwerkfehler |
| absent | reason | Telefon ausgeschaltet oder außerhalb des Empfangsbereichs |
| not_in_service | reason | Nummer nicht in Betrieb |
| blocked | reason | Vom Netzwerk oder Empfänger blockiert |
| landline | reason | Festnetz oder Nummer, die keine SMS empfangen kann |
| content_filter | reason | Vom Inhaltsfilter des Netzwerks abgelehnt |
| expired | reason | Abgelaufen: Kein Bericht vor der Zeitbegrenzung des Netzwerks |
| canceled | reason | Vor dem Senden abgebrochen |
| unknown | reason | Unbekannte Netzwerkantwort |
Serviceprüfungen finden Sie auf der Statusseite und API-Änderungen im Changelog.