API SMS en masse v1
Une petite API REST pour envoyer des SMS en masse depuis votre logiciel : JSON en entrée et sortie, clés Bearer, les mêmes prix et vérifications que la console, et des webhooks signés pour les rapports de remise. URL de base https://smsbulk.cc/rest/v1
Aperçu
Envoyez jusqu'à 8,000 numéros par requête, chacun avec ses propres valeurs de fusion, et l'API tarifie chaque destinataire avec son propre texte fusionné, exactement comme la console. Demandez d'abord un devis avec le même corps pour connaître le coût ; rien n'est envoyé ou facturé par un devis. Les heures sont au format ISO 8601 en UTC, l'argent est une chaîne en dollars américains avec cinq décimales.
curl https://smsbulk.cc/rest/v1/send \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipients": [
{ "to": "+14155550142", "fields": { "name": "Maria" } },
{ "to": "+447400123456", "fields": { "name": "Tom" } }
],
"text": "Hi {{name}}, your order ships today.",
"client_ref": "order-batch-17"
}'const res = await fetch("https://smsbulk.cc/rest/v1/send", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify({
recipients: [
{ to: "+14155550142", fields: { name: "Maria" } },
{ to: "+447400123456", fields: { name: "Tom" } },
],
text: "Hi {{name}}, your order ships today.",
client_ref: "order-batch-17",
}),
});
const data = await res.json();
console.log(res.status, data.campaign?.id, data.campaign?.cost);import requests
res = requests.post(
"https://smsbulk.cc/rest/v1/send",
headers={"Authorization": "Bearer YOUR_API_KEY"},
json={
"recipients": [
{"to": "+14155550142", "fields": {"name": "Maria"}},
{"to": "+447400123456", "fields": {"name": "Tom"}},
],
"text": "Hi {{name}}, your order ships today.",
"client_ref": "order-batch-17",
},
timeout=30,
)
data = res.json()
print(res.status_code, data.get("campaign", {}).get("id"))<?php
$body = json_encode([
"recipients" => [
["to" => "+14155550142", "fields" => ["name" => "Maria"]],
["to" => "+447400123456", "fields" => ["name" => "Tom"]],
],
"text" => "Hi {{name}}, your order ships today.",
"client_ref" => "order-batch-17",
]);
$ch = curl_init("https://smsbulk.cc/rest/v1/send");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer YOUR_API_KEY", "Content-Type: application/json"],
CURLOPT_POSTFIELDS => $body,
]);
$data = json_decode(curl_exec($ch), true);
echo curl_getinfo($ch, CURLINFO_HTTP_CODE), " ", $data["campaign"]["id"] ?? "", PHP_EOL;Authentification
Créez une clé dans la console sous API et webhooks ; les clés sont activées après votre premier rechargement crédité. Envoyez-la à chaque requête en tant que token Bearer. Une clé est affichée une seule fois ; vous pouvez conserver jusqu'à 5 clés et en révoquer n'importe laquelle immédiatement.
Authorization: Bearer smsb_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxTarifs
Public, aucune clé nécessaire. Le prix par segment SMS pour chaque destination, ou pour un code ISO.
GEThttps://smsbulk.cc/rest/v1/rates
GEThttps://smsbulk.cc/rest/v1/rates/{ISO}
{
"country": "US",
"name": "United States",
"dial_code": "+1",
"rate": "0.00485",
"currency": "USD",
"per": "message segment"
}Crédit
GEThttps://smsbulk.cc/rest/v1/credit
{ "credit": "125.40000", "currency": "USD", "account": "A418820037715" }Devis
Tarifie un envoi sans l'envoyer. Prend le même corps que Envoyer.
POSThttps://smsbulk.cc/rest/v1/quote
| Champ | Type | Signification |
|---|---|---|
| to | string or string[] | Un numéro ou une liste. La forme internationale avec + est la plus sûre ; les numéros locaux nécessitent default_country. |
| recipients | object[] | Au lieu de to : une liste de { to, fields } où fields remplit les champs de fusion comme {{name}}. |
| text | string | Le message, jusqu'à 2,000 caractères et 10 segments après fusion. |
| default_country | string | Code ISO utilisé pour les numéros écrits sans +, comme GB. Revient à la valeur par défaut enregistrée dans les paramètres. |
| opt_out | boolean | Ajoute votre mention de désinscription enregistrée comme dernière ligne (enregistrez-la d'abord dans les paramètres). |
{
"recipients": 2,
"messages": 2,
"segments": { "min": 1, "max": 1 },
"encoding": "GSM-7",
"total": "0.01240",
"currency": "USD",
"credit": "125.40000",
"credit_after": "125.38760",
"countries": [
{ "country": "US", "recipients": 1, "rate": "0.00485", "segments": 1, "subtotal": "0.00485" },
{ "country": "GB", "recipients": 1, "rate": "0.00755", "segments": 1, "subtotal": "0.00755" }
],
"left_out": []
}Envoyer
Crée une campagne, prend son total de votre crédit et met les messages en file d'attente. Répond avec un code 201 et la campagne. Les numéros qui ne peuvent pas être envoyés sont listés dans left_out et ne sont jamais facturés.
POSThttps://smsbulk.cc/rest/v1/send
| Champ | Type | Signification |
|---|---|---|
| to / recipients / text / default_country / opt_out | Comme dans Quote. | |
| send_at | string | Heure ISO 8601 pour démarrer, 5 minutes à 90 jours à l'avance. Laissez vide pour envoyer maintenant. |
| name | string | Votre étiquette pour la campagne, jusqu'à 80 caractères. |
| client_ref | string | Votre propre référence, jusqu'à 64 caractères, renvoyée sur chaque message et webhook. |
HTTP/1.1 201 Created
{
"campaign": {
"id": "C507318264409",
"name": null,
"status": "sending",
"source": "api",
"test": false,
"recipients": 2,
"cost": "0.01240",
"refunded": "0.00000",
"net_cost": "0.01240",
"currency": "USD",
"send_at": null,
"created_at": "2026-11-07T14:02:11.000Z",
"started_at": "2026-11-07T14:02:11.000Z",
"finished_at": null,
"client_ref": "order-batch-17"
},
"messages": 2,
"left_out": []
}Campagnes
GEThttps://smsbulk.cc/rest/v1/campaigns?limit=25&before={id}
GEThttps://smsbulk.cc/rest/v1/campaigns/{id}
GEThttps://smsbulk.cc/rest/v1/campaigns/{id}/messages?status=failed&limit=100&after={message id}
Une seule campagne inclut des comptes par statut : en file d'attente, envoyé, remis, échoué et annulé. Liste des messages avec after et has_more ; jusqu'à 1 000 par page.
Messages
GEThttps://smsbulk.cc/rest/v1/messages/{id}
GEThttps://smsbulk.cc/rest/v1/messages?to=%2B14155550142&status=delivered&client_ref=order-batch-17&limit=50
{
"id": "M220947781305",
"campaign": "C507318264409",
"to": "+14155550142",
"country": "US",
"segments": 1,
"encoding": "GSM-7",
"price": "0.00485",
"status": "delivered",
"reason": null,
"reason_text": null,
"refunded": false,
"client_ref": "order-batch-17",
"created_at": "2026-11-07T14:02:11.000Z",
"sent_at": "2026-11-07T14:02:12.000Z",
"finished_at": "2026-11-07T14:02:16.000Z"
}Annuler une campagne planifiée
POSThttps://smsbulk.cc/rest/v1/campaigns/{id}/cancel
Fonctionne jusqu'à l'heure de début. Le coût total est restitué à votre crédit en une ligne de registre et le statut de la campagne devient annulé.
Webhooks
Définissez une URL de point de terminaison dans la console. Nous envoyons un JSON POST pour message.delivered, message.failed et campaign.done, ainsi qu'un test lorsque vous appuyez sur Envoyer un événement de test. Chaque événement contient quatre en-têtes, et toute réponse 2xx dans les 10 secondes est considérée comme reçue.
| Champ | Type | Signification |
|---|---|---|
| Smsbulk-Event | en-tête | Nom de l'événement, comme message.delivered. |
| Smsbulk-Delivery | en-tête | ID de livraison, le même à chaque nouvelle tentative d'un événement. |
| Smsbulk-Timestamp | en-tête | Secondes Unix lorsque cette tentative a été signée. |
| Smsbulk-Signature | en-tête | sha256= suivi du HMAC-SHA256 base64 du timestamp, d'un saut de ligne et du corps brut, clé avec votre secret sbw_. |
La remise est tentée 8 fois : immédiatement, puis après 1 minute, 5 minutes, 15 minutes, 1 heure, 3 heures, 6 heures et 12 heures. Chaque tentative apparaît dans le journal de remise de la console avec son statut HTTP et le début de votre réponse.
{
"id": "dlv_3f9a1c07b2e44d58a6c1",
"event": "message.failed",
"created": "2026-11-07T14:02:16.000Z",
"data": {
"message": "M220947781305",
"campaign": "C507318264409",
"to": "+447400123456",
"country": "GB",
"status": "failed",
"reason": "not_in_service",
"segments": 1,
"price": "0.00755",
"client_ref": "order-batch-17"
}
}Vérifier la signature
import crypto from "node:crypto";
// raw = the request body exactly as received (string), secret = your sbw_ signing secret
export function verify(raw, headers, secret) {
const ts = headers["smsbulk-timestamp"];
const want = "sha256=" + crypto.createHmac("sha256", secret).update(ts + "\n" + raw).digest("base64");
const got = headers["smsbulk-signature"] || "";
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
return fresh && got.length === want.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want));
}import base64, hashlib, hmac, time
def verify(raw: bytes, headers, secret: str) -> bool:
ts = headers.get("Smsbulk-Timestamp", "")
mac = hmac.new(secret.encode(), ts.encode() + b"\n" + raw, hashlib.sha256).digest()
want = "sha256=" + base64.b64encode(mac).decode()
fresh = abs(time.time() - int(ts or 0)) < 300
return fresh and hmac.compare_digest(want, headers.get("Smsbulk-Signature", ""))Erreurs et limites
Les erreurs utilisent le statut HTTP et un corps JSON : {"error":{"code":"insufficient_credit","message":"..."}}. Aucun frais n'est facturé en cas d'échec de la demande.
| Champ | Type | Signification |
|---|---|---|
| 400 bad_json | Le corps n'est pas du JSON. | |
| 401 unauthorized | Clé manquante, malformée ou révoquée. | |
| 402 insufficient_credit | Le total dépasse votre crédit. Ajoutez des fonds dans la console. | |
| 404 not_found | Aucun point de terminaison, campagne, message ou code ISO correspondant sur ce compte. | |
| 409 not_scheduled | Une annulation a été demandée pour une campagne déjà commencée. | |
| 422 text_missing, text_too_long, too_many_segments | Message vide, dépassant 2,000 caractères, ou plus de 10 segments pour un destinataire. | |
| 422 no_recipients, too_many, recipients_missing | Aucun numéro envoyable, plus de 8,000 numéros, ou ni 'to' ni 'recipients'. | |
| 422 schedule_range, bad_send_at | send_at en dehors de 5 minutes à 90 jours, ou heure invalide. | |
| 422 opt_out_unset | opt_out est vrai mais aucun texte n'est enregistré dans les Paramètres. | |
| 429 rate_limited | Plus de 300 requêtes par minute pour cette clé. |
Chaque réponse contient X-RateLimit-Limit, X-RateLimit-Remaining et X-RateLimit-Reset.
Codes exclus
Les numéros supprimés avant le calcul du prix reviennent dans left_out avec leur position dans votre liste, la valeur envoyée, un code et la raison explicite.
| Champ | Type | Signification |
|---|---|---|
| no_country_code | Pas de + et pas de pays par défaut. | |
| too_short / too_long | Moins de 8 ou plus de 15 chiffres. | |
| not_a_number | Lettres ou symboles qui ne font pas partie d'un numéro. | |
| nanp_length | Un numéro +1 sans onze chiffres. | |
| not_covered | Une destination en dehors de notre liste. | |
| duplicate | Le même numéro est apparu plus tôt dans la requête. | |
| suppressed | Le numéro est sur votre liste de non-envoi. |
Statuts et raisons
| Champ | Type | Signification |
|---|---|---|
| queued | statut | En attente de sortie de notre file. |
| sent | statut | Transmis au réseau, en attente du rapport de remise. La plupart des rapports arrivent en quelques minutes ; certains réseaux prennent jusqu'à 24 heures. |
| delivered | statut | Le réseau a confirmé que le téléphone l'a reçu. |
| failed | statut | Non remis. Le prix a été remboursé sur votre crédit. |
| canceled | statut | Non envoyé car vous avez annulé. Remboursé. |
| Champ | Type | Signification |
|---|---|---|
| network_error | raison | Erreur réseau |
| switched_off | raison | Téléphone éteint ou hors couverture |
| not_in_service | raison | Numéro hors service |
| blocked | raison | Bloqué par le réseau ou le destinataire |
| landline | raison | Ligne fixe ou numéro ne pouvant pas recevoir de SMS |
| content_filter | raison | Rejeté par le filtre de contenu du réseau |
| expired | raison | Expiré : aucun rapport avant la limite de temps du réseau |
| unknown | raison | Réponse réseau inconnue |
| canceled | raison | Annulé avant l'envoi |
Des questions sur l'API ? Ouvrez un ticket dans la console ou consultez la FAQ.