Démarrage rapide
- 1. Créez une clé. Dans l'espace de travail, ouvrez l'API et les webhooks. Les clés sont disponibles après votre premier rechargement crédité ; chaque clé n'est affichée qu'une seule fois, alors copiez-la ou enregistrez-la dans un fichier texte.
- 2. Vérifiez le coût. POSTez le même corps à
/costspour voir les destinataires, les segments et le total sans envoyer. - 3. Envoyez. POSTez-le à
/sends. La réponse contient l'identifiant d'envoi ; les messages partent immédiatement ou àsend_at.
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"];Authentification
Envoyez votre clé dans l'en-tête Authorization : Authorization: Bearer sso_live_.... Les clés font 49 caractères et commencent par sso_live_. Jusqu'à 5 clés par compte ; révoquez-en une dans l'espace de travail et elle cesse de fonctionner immédiatement. La liste des prix est publique et ne nécessite pas de clé.
Erreurs
Les erreurs utilisent des codes de statut HTTP et un corps JSON avec un code stable et une phrase que vous pouvez montrer à une personne. Rien n'est facturé lorsqu'une requête échoue.
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" } }| Champ | Type | Signification |
|---|---|---|
| 400 | invalid_json, text_missing, recipients_missing, too_many_recipients, too_many_segments, schedule_range, bad_send_at | La requête nécessite une modification. |
| 401 | unauthorized | Clé manquante, incorrecte ou révoquée. |
| 402 | insufficient_credit | Crédit insuffisant pour le total. |
| 404 | not_found | Aucun envoi, message, pays ou point de terminaison correspondant. |
| 409 | not_cancelable | L'envoi a déjà commencé. |
| 429 | rate_limited | Plus de 240 requêtes par minute. |
Limites
- 240 requêtes par minute par clé, affichées dans X-RateLimit-Limit, X-RateLimit-Remaining et X-RateLimit-Reset (secondes unix).
- 5,000 numéros par requête, 2,000 caractères par texte, 10 segments par message.
- Les messages partent à un maximum de 300 par minute par compte.
- send_at de 10 minutes à 30 jours à l'avance.
Prix
GET/api/v1/prices
GET/api/v1/prices/{country}
Le prix par message (un segment) pour chaque pays, en USD avec 5 décimales. Aucune clé nécessaire.
{ "price": { "country": "GB", "name": "United Kingdom", "dial_code": "+44", "price_usd": "0.00868" }, "updated": "2026-11-07" }Crédit du compte
GET/api/v1/account
{ "account": "SO8K2M4Q7X9TBA", "credit_usd": "41.27660", "currency": "USD" }Estimer un coût
POST/api/v1/costs
Prend le même corps qu'un envoi et retourne ce que cela coûterait, sans envoyer ni facturer. Le texte fusionné de chaque destinataire détermine ses segments, donc le total est exact.
{
"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
}Créer un envoi
POST/api/v1/sends
Un texte à un ou plusieurs numéros. Le crédit pour l'ensemble de l'envoi est débité lorsqu'il est accepté ; les messages échoués sont remboursés un par un.
| Champ | Type | Signification |
|---|---|---|
| to | string or array | Numéros au format international (+447700900461). Utilisez ceci ou destinataires. |
| recipients | array | Objets avec to et fields, pour les champs de fusion : { "to": "+1...", "fields": { "first_name": "Ava" } }. |
| text | string | Le message, jusqu'à 2,000 caractères. {{field}} est remplacé par destinataire ; les valeurs manquantes deviennent vides. |
| send_at | string | Heure ISO 8601 facultative avec décalage ou Z. Sans cela, l'envoi commence immédiatement. |
| name | string | Étiquette facultative affichée dans l'espace de travail (80 caractères). |
| client_ref | string | Référence facultative retournée dans les webhooks (64 caractères). |
| opt_out | boolean | Ajoute la ligne de désabonnement enregistrée dans les Paramètres à la fin du texte. |
| default_country | string | Code ISO utilisé pour les numéros écrits sans indicatif pays. |
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" } ]
}Les numéros qui ne peuvent pas être envoyés sont listés dans dropped et jamais facturés. Codes : no_country_code, too_short, too_long, not_a_number, wrong_length, not_covered, duplicate et suppressed (sur votre liste de non-envoi).
Obtenir ou lister les envois
GET/api/v1/sends/{id}
GET/api/v1/sends?limit=25&before={id}
Un seul envoi inclut counts par statut de message. Les listes sont les plus récentes en premier ; passez next_before comme before pour la page suivante. Les envois depuis l'espace de travail apparaissent ici aussi, avec source: "web".
Messages
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" } }Annuler un envoi
POST/api/v1/sends/{id}/cancel
Seuls les envois programmés qui n'ont pas commencé peuvent être annulés. Le coût total retourne à votre crédit et la réponse inclut refunded_usd.
Webhooks
Définissez une URL https dans l'espace de travail (API et webhooks). Nous POSTons un événement lorsqu'un message est livré ou échoue, pour les envois depuis l'API et l'espace de travail. Répondez avec un statut 2xx dans les 10 secondes. Les livraisons échouées sont réessayées jusqu'à 7 fois au total : immédiatement, puis après 1 minute, 5 minutes, 30 minutes, 1 heure, 2 heures et 3 heures. L'espace de travail montre chaque tentative avec son statut et sa réponse.
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": "..." } } }La signature est HMAC-SHA256 de timestamp + "." + raw body avec votre secret de signature (il commence par sso_whk_). Vérifiez-la sur le corps brut avant l'analyse, rejetez les timestamps de plus de 5 minutes, et utilisez l'id de livraison pour ignorer les répétitions.
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);
}Événements : message.delivered, message.failed, et test depuis le bouton Envoyer un événement test.
Statuts et raisons
| Champ | Type | Signification |
|---|---|---|
| queued | status | En attente de quitter notre file. |
| sent | status | Transmis au réseau, en attente du rapport de livraison. La plupart des rapports arrivent en quelques minutes ; certains réseaux peuvent prendre jusqu'à 24 heures. |
| delivered | status | Le réseau a confirmé que le téléphone l'a reçu. |
| failed | status | Non livré. Le prix a été remboursé sur votre crédit. |
| canceled | status | Non envoyé car vous avez annulé. Remboursé. |
Raisons d'échec (le champ reason) :
| Champ | Type | Signification |
|---|---|---|
| network_error | reason | Erreur réseau |
| absent | reason | Téléphone éteint ou hors couverture |
| not_in_service | reason | Numéro hors service |
| blocked | reason | Bloqué par le réseau ou le destinataire |
| landline | reason | Ligne fixe ou numéro ne pouvant pas recevoir de SMS |
| content_filter | reason | Rejeté par le filtre de contenu du réseau |
| expired | reason | Expiré : aucun rapport avant la limite de temps du réseau |
| canceled | reason | Annulé avant l'envoi |
| unknown | reason | Réponse réseau inconnue |
Les vérifications de service sont sur la page d'état et les changements d'API dans le changelog.