Tout ce que fait le dashboard passe par une API REST, et cette même API est ouverte à vos propres logiciels : un CRM qui envoie des rappels de rendez-vous, un site web qui envoie un code de confirmation, un script qui lit les réponses. L'API tourne sur votre passerelle, sur votre serveur. Vos appels ne passent jamais par nous.
Cette page s'adresse au développeur qui connecte une application à la passerelle. Elle couvre ce qu'il faut pour un premier appel qui fonctionne et les règles que suivent tous les endpoints. La liste complète des endpoints, avec tous leurs champs, se trouve dans la référence interactive intégrée à votre passerelle.
URL de base
L'API répond sous /api/v1 à l'adresse de votre passerelle :
| Installation de la passerelle | URL de base |
|---|---|
| Avec un nom de domaine (recommandé) | https://sms.exemple.com/api/v1 |
| Sur un réseau local, sans domaine | http://<machine>.local:8080/api/v1 |
| Depuis le serveur lui-même | http://127.0.0.1:8080/api/v1 |
Les requêtes et les réponses sont en JSON (Content-Type: application/json). Les dates sont au
format ISO 8601, en UTC (2026-09-21T09:14:02Z). Les numéros de téléphone sont toujours au format
international E.164 : un +, l'indicatif du pays, puis le numéro, sans espace (+261341234567).
Le corps d'une requête est plafonné à 2 Mio, bien au-delà de ce qu'exige un envoi. Au-delà, la
réponse est 413 PAYLOAD_TOO_LARGE.
Authentification : les clés API
Une application s'authentifie avec une clé API, envoyée dans l'en-tête X-API-KEY de chaque
appel :
curl -H "X-API-KEY: sk_..." https://sms.exemple.com/api/v1/messages?limit=1
La clé voyage toujours dans cet en-tête, jamais dans l'URL : une URL finit dans les journaux des serveurs, dans l'historique du navigateur et dans les liens partagés, et une clé écrite là doit être considérée comme divulguée. Une clé placée dans les paramètres de l'URL n'est tout simplement pas lue.
Les clés se créent sur la page Clés API du dashboard, par un superadministrateur. Chaque clé a :
| Réglage | Ce qu'il fait |
|---|---|
| Une portée | read, send ou admin : ce que la clé a le droit de faire (voir plus bas) |
| Adresses IP autorisées | Facultatif. Une fois renseignées, les appels venant de toute autre adresse sont refusés |
| Appels par minute | Facultatif. Protège contre la rafale d'un script qui s'emballe |
| Appels par jour | Facultatif. Plafonne le volume total, compté par jour UTC |
Le secret s'affiche une seule fois, à la création de la clé : la passerelle n'en garde qu'une empreinte. Créer, renouveler et révoquer des clés est expliqué dans Clés API.
Les trois portées
Les portées sont ordonnées : send peut tout ce que peut read, et admin peut tout.
| Portée | Ce qu'elle ouvre |
|---|---|
read |
Toutes les lectures : messages, conversations, contacts, groupes, appareils, campagnes, statistiques, journal d'activité |
send |
Tout ce que permet read, plus l'envoi d'un SMS (POST /messages) et la relance d'un message en échec (POST /messages/{id}/retry) |
admin |
Toute l'API : configuration, contacts, modèles, règles, campagnes, webhooks, exports en masse, sauvegardes, journal technique et les clés API elles-mêmes |
Quelques lectures exigent aussi admin, parce qu'elles livrent beaucoup de données d'un coup ou
décrivent l'intérieur de l'installation : les exports en masse (/export/..., /contacts/export),
les sauvegardes (/backups/...), la liste des comptes du dashboard (/users) et le journal technique
(/logs/technical). Créer, modifier ou supprimer un compte du dashboard est refusé à toute clé, même
admin : ces actions demandent une personne connectée au dashboard.
Donnez à chaque application la portée la plus restreinte qui suffit. Un site web qui envoie des codes
de confirmation a besoin de send, pas d'admin.
Le dashboard n'utilise pas de clé
Le dashboard se connecte avec un cookie de session, pas avec une clé. C'est pourquoi certains endpoints visibles dans la référence (connexion, mot de passe, premier compte) ne servent à rien à une intégration : ils appartiennent au dashboard.
Un premier appel : envoyer un SMS et le suivre
1. Envoyer
curl -X POST https://sms.exemple.com/api/v1/messages \
-H "X-API-KEY: sk_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: commande-10482-confirmation" \
-d '{"to": "+261341234567", "body": "Votre commande 10482 est prête."}'
La passerelle répond 202 Accepted dès que le message est mis en file, avant qu'il ne quitte le
téléphone :
{
"accepted": [
{
"id": "0192f0c1-7a3e-7c55-9d1e-2b8f4a6c1d20",
"conversationId": "0192f0c1-7a3e-7c55-9d1e-2b8f4a6c1d21",
"direction": "out",
"phone": "+261341234567",
"body": "Votre commande 10482 est prête.",
"encoding": "ucs2",
"segments": 1,
"status": "queued",
"attempts": 0,
"createdAt": "2026-09-21T09:14:02Z"
}
]
}
Options d'un envoi
Tous les champs de la requête :
| Champ | Par défaut | Valeurs admises | Effet |
|---|---|---|---|
to |
obligatoire | Un numéro E.164, ou une liste de 1 à 500 | Un message est mis en file par numéro. Au-delà de 500, utilisez une campagne, qui étale l'envoi |
body |
obligatoire | Tout texte non vide | Aucune limite de longueur : un long texte est découpé en plusieurs SMS (segments). Un accent ou un emoji hors de l'alphabet GSM fait passer tout le message en ucs2, qui tient moins de caractères par SMS |
deviceId |
aucun : la passerelle choisit | L'identifiant d'un téléphone appairé | Impose ce téléphone et court-circuite le choix automatique et les règles de routage par préfixe. Si ce téléphone ne peut pas envoyer, le message passe par les nouvelles tentatives habituelles, puis finit failed |
simSlot |
aucun : la passerelle choisit | 0 ou 1 |
Impose un emplacement de SIM sur le téléphone choisi |
scheduledAt |
aucun : envoi immédiat | Une date ISO 8601 future | Le message reste en file jusqu'à cette heure. Une date passée est refusée avec SCHEDULED_AT_IN_PAST |
shortenLinks |
false |
true ou false |
true remplace chaque adresse http:// ou https:// de body par un lien court de votre passerelle qui compte les clics, avant la mise en file. La même adresse écrite deux fois donne un seul lien, partagé par tous les destinataires de la requête ; une adresse qui est déjà l'un de vos liens courts reste telle quelle. Le body renvoyé, enregistré et envoyé porte les liens courts : sa longueur et segments changent. GET /links renvoie shortUrlLength pour calculer cette longueur à l'avance. Voir Liens courts |
Sans deviceId ni simSlot, le téléphone et la SIM sont choisis selon vos réglages de routage (mode
de routage, règles par préfixe, quotas des SIM) : voir Routage. Le quota
journalier de chaque SIM et les nouvelles tentatives (Paramètres > Envoi) s'appliquent aux
messages envoyés par l'API exactement comme à ceux envoyés depuis le dashboard. La fenêtre d'envoi et
la pause de nuit, elles, ne s'appliquent pas : elles ne retiennent que les campagnes, donc un envoi
par l'API part à toute heure.
Les options de texte de Paramètres > Général, appareils (Retirer les diacritiques,
Supprimer les espaces superflus, Tout en majuscules) s'appliquent aussi aux envois par l'API.
Elles sont appliquées au moment où le message part vers un téléphone, avec les réglages de ce
téléphone : la réponse à cette requête montre body, encoding et segments tels que vous les avez
envoyés, et GET /messages/{id} les montre tels qu'ils sont partis une fois le message parti. Voir
Paramètres.
La référence liste aussi templateId et variables sur cette requête. La version actuelle de la
passerelle ne les applique pas à un envoi par l'API : envoyez le texte final dans body. Les
modèles sont disponibles depuis le dashboard et dans les campagnes.
Avec shortenLinks, rejouer une requête avec la même Idempotency-Key et le même body d'origine
renvoie le message déjà mis en file, bien que son body enregistré porte désormais les liens
courts : ni second lien ni second message n'est créé.
Si un destinataire est invalide, ou s'est désinscrit, toute la requête est refusée et rien n'est mis en file : vous n'avez jamais à deviner quelle partie d'une requête est passée.
2. Suivre son statut
curl -H "X-API-KEY: sk_..." \
https://sms.exemple.com/api/v1/messages/0192f0c1-7a3e-7c55-9d1e-2b8f4a6c1d20
Le champ status suit la vie du message :
| Statut | Signification |
|---|---|
queued |
Accepté, en attente d'un téléphone |
dispatched |
Confié à un téléphone, en attente de sa confirmation |
sent |
Le téléphone a confirmé l'envoi du SMS |
delivered |
L'opérateur a confirmé la remise au destinataire |
failed |
Abandonné après les tentatives prévues. errorCode en donne la raison (liste des codes) |
pending_quota_exceeded |
Toutes les SIM utilisables ont atteint leur quota du jour : il part quand un quota se libère |
paused |
Retenu, par exemple par une campagne en pause |
cancelled |
Retiré avant de partir. errorCode en donne la raison (CAMPAIGN_CANCELLED, CONTACT_BLACKLISTED) |
Un message queued qui attend le rythme de ses SIM porte aussi holdReason (sim_cadence :
l'intervalle minimal entre deux SMS par SIM ; sim_rate : la limite par minute) et heldUntil,
l'heure de son prochain essai. Les deux valent null sinon. Il part de lui-même, sans consommer de
tentative.
delivered dépend de l'envoi d'un accusé de réception par l'opérateur : certains n'en envoient pas,
et un message peut alors rester à sent alors qu'il est bien arrivé. Le fonctionnement de la file,
des nouvelles tentatives et des fenêtres d'envoi est expliqué dans
Envoyer des messages.
Plutôt que de redemander sans cesse, vous pouvez laisser la passerelle vous appeler quand le
statut change : voir Webhooks et les événements message.sent,
message.delivered et message.failed.
Idempotence : relancer sans envoyer deux fois
Le réseau coupe au mauvais moment, votre script n'a pas reçu la réponse, et il réessaie. Sans précaution, le destinataire reçoit le SMS deux fois.
Pour l'éviter, ajoutez un en-tête Idempotency-Key à POST /messages, avec une valeur de votre
choix qui identifie l'opération (entre 8 et 128 caractères, par exemple
commande-10482-confirmation) :
- la même clé renvoyée dans les 24 heures avec le même corps renvoie la réponse d'origine,
sans rien envoyer. Cette réponse porte l'en-tête
Idempotency-Replayed: true; - la même clé envoyée avec un corps différent est refusée avec
409 IDEMPOTENCY_CONFLICT: une clé désigne une opération, pas un emplacement à réutiliser ; - une requête qui a échoué (réponse d'erreur) ne consomme pas la clé : vous pouvez la corriger et la renvoyer avec la même clé.
Les clés sont liées à l'appelant : une autre application qui choisirait la même valeur ne reçoit jamais votre réponse. Choisissez tout de même des valeurs propres à votre application (un UUID, ou votre numéro de commande préfixé du nom de l'application), car une valeur déjà utilisée pour un autre message est refusée. L'en-tête est facultatif, mais toute intégration qui réessaie d'elle-même devrait l'envoyer.
Pagination
Toutes les listes sont paginées par curseur, jamais par numéro de page :
| Paramètre | Rôle |
|---|---|
limit |
Éléments par page, de 1 à 200. 50 par défaut |
cursor |
Le nextCursor de la page précédente. À omettre pour la première page |
La réponse contient la liste dans data, et le curseur de la page suivante dans nextCursor, qui
vaut null sur la dernière page :
curl -H "X-API-KEY: sk_..." "https://sms.exemple.com/api/v1/messages?limit=100&direction=in"
{ "data": [ ... ], "nextCursor": "0192f0c1-6b2d-7a10-8c3e-5d4f3a2b1c09" }
Renvoyez le curseur tel quel, sans chercher à l'interpréter. L'historique des messages arrive du plus récent au plus ancien. Comme le curseur désigne le dernier élément reçu et non un numéro de page, les messages qui arrivent pendant que vous parcourez les pages ne les décalent pas : rien n'est sauté ni lu deux fois.
Les plafonds et leurs en-têtes
Quand la clé a un plafond d'appels par minute, chaque réponse porte :
| En-tête | Contenu |
|---|---|
X-RateLimit-Limit |
Le plafond de la clé par minute |
X-RateLimit-Remaining |
Les appels restants dans la minute en cours |
X-RateLimit-Reset |
Quand la fenêtre se rouvre, en secondes Unix |
Au-delà du plafond par minute ou du quota journalier, la réponse est 429 RATE_LIMITED avec un
en-tête Retry-After, en secondes. Attendez ce délai avant de réessayer : un appel refusé compte
quand même, donc réessayer plus vite n'aide pas. Le quota journalier est compté par jour UTC.
Les erreurs
Toutes les erreurs ont la même forme, quel que soit l'endpoint :
{
"error": {
"code": "INVALID_PHONE",
"message": "recipient is not a valid international number",
"details": { "recipient": "0341234567" },
"requestId": "req_01J8XK2M9F"
}
}
| Champ | Usage |
|---|---|
code |
Un identifiant stable. C'est lui que votre code doit tester, jamais le message |
message |
Une courte explication en anglais, pour un humain qui lit un journal |
details |
Contexte facultatif : le champ en cause, le plafond, la valeur attendue |
requestId |
À transmettre à qui consulte les journaux de la passerelle : il retrouve la requête aussitôt |
Les codes que vous rencontrerez le plus souvent :
| HTTP | Code | Que faire |
|---|---|---|
| 401 | MISSING_API_KEY |
Ajoutez l'en-tête X-API-KEY |
| 403 | INVALID_API_KEY |
La clé est inconnue ou révoquée. Vérifiez-la, ou créez-en une nouvelle |
| 403 | IP_NOT_ALLOWED |
L'appel vient d'une adresse hors de la liste autorisée de la clé |
| 403 | FORBIDDEN_SCOPE |
La portée de la clé ne couvre pas cette opération. Utilisez une clé de portée plus large |
| 400 | VALIDATION_ERROR |
Un champ manque ou sort des bornes. details indique lequel |
| 400 | INVALID_PHONE |
Un numéro n'est pas au format E.164 (+ et indicatif du pays) |
| 400 | EMPTY_BODY |
Le message n'a pas de texte |
| 403 | CONTACT_BLACKLISTED |
Un destinataire s'est désinscrit. Rien n'a été mis en file |
| 422 | SCHEDULED_AT_IN_PAST |
scheduledAt doit être dans le futur |
| 404 | NOT_FOUND |
L'identifiant n'existe pas sur cette passerelle |
| 409 | IDEMPOTENCY_CONFLICT |
Cette Idempotency-Key a déjà servi pour une autre requête |
| 409 | VALIDATION_ERROR |
Un nom qui doit être unique est déjà pris |
| 413 | PAYLOAD_TOO_LARGE |
Le corps dépasse 2 Mio |
| 429 | RATE_LIMITED |
Plafond atteint. Attendez Retry-After |
| 403 | LICENSE_BLOCKED |
La licence a été retirée. Les lectures fonctionnent toujours, les écritures sont refusées. Voir Licence |
| 500 | INTERNAL_ERROR |
Un problème inattendu sur la passerelle. Réessayez plus tard et transmettez le requestId |
Un message d'erreur ne contient jamais le texte d'un SMS, un numéro de téléphone complet ni une clé.
La liste complète des codes figure dans la référence, sous le schéma ErrorCode.
Ce qui ne produit pas d'erreur. Un téléphone hors ligne ou une SIM qui a atteint son quota du
jour ne font pas refuser la requête : le message est accepté (202) et le problème apparaît plus
tard, dans son statut. Quand toutes les SIM utilisables ont épuisé leur quota, le message attend en
pending_quota_exceeded que le quota se libère. Quand aucun téléphone ne peut le prendre, la
passerelle réessaie selon Paramètres > Envoi (4 tentatives par défaut, avec des délais
croissants entre elles) et le marque failed après la dernière. Surveillez son statut, ou abonnez-vous
à message.failed.
La référence interactive
Chaque passerelle embarque la référence complète de sa propre API, générée à partir du même contrat que le code qui répond. Elle ne peut pas décrire un endpoint que votre version n'a pas.
| Où | Ce que vous y trouvez |
|---|---|
| Référence de l'API dans le menu du dashboard | Toutes les opérations regroupées par thème, avec leurs champs, réponses et codes d'erreur |
https://<votre passerelle>/api/v1/ |
La même référence en page autonome, avec « Try it out » : collez une clé et lancez un appel |
https://<votre passerelle>/api-docs/openapi.json |
Le contrat OpenAPI 3.1 brut, pour générer un client dans votre langage |
Ces pages se lisent sans connexion : le contrat ne contient aucun secret et aucune de vos données. Lancer un appel depuis elles demande tout de même une clé valide, avec les mêmes contrôles que tout autre appel. Rien n'est chargé depuis internet pour les afficher.
Pour générer un client, pointez votre outil habituel (OpenAPI Generator, openapi-typescript,
oapi-codegen, etc.) sur openapi.json.
Avant d'installer. La même référence se trouve sur notre site, sous Référence de l'API en haut
de cette documentation (/docs/api-reference), avec son contrat sur /docs/openapi.json. Elle décrit
la dernière version publiée, et ses exemples utilisent https://your-gateway.example.com/api/v1 comme
URL de base : remplacez-la par l'adresse de votre passerelle. Rien de ce que vous faites sur cette page
n'atteint une passerelle, et elle n'a pas de « Try it out ». Une fois votre passerelle en service, fiez-vous
à sa propre référence : elle correspond exactement à la version installée.