Référence API · v1

Référence API

Tous les endpoints de l’API REST mysms : paramètres, exemples dans quatre langages, réponses et codes de retour. Une seule URL de base, du JSON partout.

Aperçu https://api.mysms.ma/v1 JSON · UTF-8 Numéros E.164 Montants en MAD HT

Aperçu Version v1

Aperçu

L’API mysms est une API REST : des URL orientées ressources, des corps JSON (UTF-8), les codes HTTP standards et une authentification par clé. Les numéros sont au format international E.164 (+212…) et les montants en dirhams hors taxes.

Cette référence est un aperçu : les endpoints peuvent encore évoluer avant l’ouverture publique. Les changements incompatibles passeront par une nouvelle version (/v2), jamais en silence.

Environnements

sk_live_…production
Envoie de vrais SMS et débite votre solde.
sk_test_…test
Mêmes requêtes et mêmes réponses, sans envoi ni débit.
URL de base https://api.mysms.ma/v1
En-têtes communs
Authorization: Bearer sk_live_••••••••Content-Type: application/jsonAccept: application/jsonIdempotency-Key: 4f1c2a9e-…   # facultatif, POST uniquement

Authentification

Chaque requête porte votre clé API dans l’en-tête Authorization: Bearer. Les clés se créent dans le tableau de bord (Développeurs › Clés API) et peuvent être révoquées à tout moment.

Types de clés

sk_live_secrète
Accès complet en production. À garder côté serveur uniquement.
sk_test_secrète
Accès complet en mode test : aucun envoi réel.
rk_live_lecture seule
Statuts, statistiques et solde uniquement. Idéale pour les rapports et les assistants IA.

Ne mettez jamais une clé secrète dans une application mobile ou du JavaScript côté navigateur.

terminal
curl https://api.mysms.ma/v1/balance \  -H "Authorization: Bearer sk_test_••••••••"
401
Réponse
{  "error": {    "type": "authentication_error",    "code": "invalid_api_key",    "message": "Clé API invalide ou révoquée."  }}

Idempotence

Un timeout réseau ne dit pas si le SMS est parti. Ajoutez un en-tête Idempotency-Key (un UUID ou l’identifiant de votre commande) à vos POST : si vous relancez la même requête, nous renvoyons la réponse d’origine au lieu d’envoyer un second message.

  • Les clés sont conservées 24 heures.
  • La même clé avec un corps différent renvoie 409.
  • Une réponse rejouée porte l’en-tête Idempotent-Replayed: true.
  • Les SDK génèrent une clé automatiquement à chaque nouvel envoi et la réutilisent pour leurs relances.
terminal
# Premier appel : le message est créécurl https://api.mysms.ma/v1/messages -X POST \  -H "Authorization: Bearer $MYSMS_KEY" \  -H "Idempotency-Key: 4f1c2a9e-order-20481" \  -d '{ "to": "+212612345678", "from": "MyShop", "body": "…" }'# Même clé après un timeout : aucune double facturation# → 201, même id msg_9fK2, en-tête Idempotent-Replayed: true

Pagination

Les endpoints de liste sont paginés par curseur, ce qui reste stable même si de nouveaux messages arrivent pendant le parcours.

Paramètres

limitinteger
De 1 à 100 éléments par page (20 par défaut).
starting_afterstring
Renvoie les éléments qui suivent cet identifiant.

Réponse

dataarray
Les éléments de la page.
has_moreboolean
Indique s’il reste des pages.
next_cursorstring
Valeur à passer dans starting_after pour la page suivante.
pagination.ts
// Parcourir toutes les pages (Node.js)let cursor;do {  const page = await sms.messages.list({ limit: 100, starting_after: cursor });  page.data.forEach((m) => console.log(m.id, m.status));  cursor = page.has_more ? page.next_cursor : null;} while (cursor);// Les SDK exposent aussi un itérateur automatique :for await (const m of sms.messages.list({ status: 'failed' })) { /* … */ }

Erreurs

Une erreur renvoie un code HTTP standard et un objet error homogène. Basez votre logique sur code, stable, plutôt que sur message, rédigé pour les humains.

Codes HTTP

  • 200 / 201Succès
  • 400JSON mal formé
  • 401Clé absente, invalide ou révoquée
  • 402Solde insuffisant
  • 403La clé n’a pas ce droit (lecture seule)
  • 404Ressource introuvable
  • 409Conflit (clé d’idempotence réutilisée avec un autre corps)
  • 422Paramètre invalide
  • 429Limite de débit atteinte
  • 5xxErreur de notre côté, réessayez avec un délai

Les réponses 429 portent un en-tête Retry-After (en secondes). Toutes les réponses portent X-RateLimit-Limit et X-RateLimit-Remaining. La liste complète des codes d’erreur est dans la documentation.

422
Objet error
{  "error": {    "type": "invalid_request",    "code": "invalid_phone_number",    "message": "Le numéro +21261234 n’est pas un numéro valide.",    "param": "to",    "request_id": "req_Q2w9"  }}

Messages

Envoyer des SMS et suivre leur livraison.

POST/v1/messages

Envoyer un SMS

Crée un message et le place en file d’envoi. La réponse est immédiate ; le statut évolue ensuite (queued → sent → delivered ou failed) et chaque changement est notifié à votre webhook.

Corps de la requête

tostringobligatoire
Numéro du destinataire au format E.164, par exemple +212612345678.
fromstringobligatoire
Sender ID validé sur votre compte (11 caractères alphanumériques au plus).
bodystringobligatoire
Texte du message. L’encodage (GSM-7 ou UCS-2) et le nombre de segments sont calculés automatiquement.
webhookstring
URL HTTPS qui recevra les événements de ce message. Remplace le webhook par défaut du compte.
scheduled_atdatetime
Date d’envoi différé (ISO 8601). Jusqu’à 30 jours à l’avance.
metadataobject
Jusqu’à 20 paires clé-valeur, renvoyées telles quelles dans la réponse et les webhooks.

Codes de retour

  • 201Message accepté et placé en file d’envoi.
  • 401Clé API absente ou invalide.
  • 402Solde insuffisant pour envoyer ce message.
  • 422Paramètre invalide (numéro, Sender ID non validé…).
  • 429Limite de débit atteinte, réessayez après Retry-After.
curl https://api.mysms.ma/v1/messages -X POST \  -H "Authorization: Bearer $MYSMS_KEY" \  -H "Idempotency-Key: 4f1c2a9e-order-20481" \  -H "Content-Type: application/json" \  -d '{    "to": "+212612345678",    "from": "MyShop",    "body": "Commande #20481 confirmée.",    "metadata": {      "order_id": "20481"    }  }'
import MySms from '@mysms/node';const sms = new MySms(process.env.MYSMS_KEY);const message = await sms.messages.send({  to: '+212612345678',  from: 'MyShop',  body: 'Commande #20481 confirmée.',  metadata: { order_id: '20481' },}, {  idempotencyKey: '4f1c2a9e-order-20481',});
use MySms\Client;$sms = new Client(env('MYSMS_KEY'));$message = $sms->messages->send([    'to' => '+212612345678',    'from' => 'MyShop',    'body' => 'Commande #20481 confirmée.',    'metadata' => ['order_id' => '20481'],], idempotencyKey: '4f1c2a9e-order-20481');
import osfrom mysms import Clientclient = Client(os.environ["MYSMS_KEY"])message = client.messages.send(    to="+212612345678",    sender="MyShop",    body="Commande #20481 confirmée.",    metadata={"order_id": "20481"},    idempotency_key="4f1c2a9e-order-20481",)
Réponse
{  "id": "msg_9fK2",  "object": "message",  "to": "+212612345678",  "from": "MyShop",  "body": "Commande #20481 confirmée.",  "status": "queued",  "segments": 1,  "encoding": "gsm7",  "price": 0.50,  "currency": "MAD",  "metadata": { "order_id": "20481" },  "created_at": "2026-10-21T09:41:02+01:00"}
{  "error": {    "type": "invalid_request",    "code": "invalid_phone_number",    "message": "Le numéro +21261234 n’est pas un numéro valide.",    "param": "to"  }}

GET/v1/messages/{id}

Statut d’un message

Renvoie un message et son dernier statut connu, avec l’opérateur de destination et les horodatages d’envoi et de livraison.

Paramètres de chemin

idstringobligatoire
Identifiant du message, renvoyé à la création (msg_…).

Codes de retour

  • 200Le message existe et appartient à votre compte.
  • 404Identifiant inconnu (ou message d’un autre compte).
curl https://api.mysms.ma/v1/messages/msg_9fK2 \  -H "Authorization: Bearer $MYSMS_KEY"
import MySms from '@mysms/node';const sms = new MySms(process.env.MYSMS_KEY);const message = await sms.messages.retrieve('msg_9fK2');
use MySms\Client;$sms = new Client(env('MYSMS_KEY'));$message = $sms->messages->retrieve('msg_9fK2');
import osfrom mysms import Clientclient = Client(os.environ["MYSMS_KEY"])message = client.messages.retrieve("msg_9fK2")
Réponse
{  "id": "msg_9fK2",  "object": "message",  "to": "+212612345678",  "status": "delivered",  "operator": "maroc_telecom",  "segments": 1,  "price": 0.50,  "sent_at": "2026-10-21T09:41:02+01:00",  "delivered_at": "2026-10-21T09:41:03+01:00"}
{  "error": {    "type": "not_found",    "code": "resource_missing",    "message": "Aucun message msg_0000 sur ce compte."  }}

GET/v1/messages

Lister les messages

Liste vos messages, du plus récent au plus ancien, avec filtres par statut et par date. Paginé par curseur.

Paramètres de requête

statusstring
queued, sent, delivered, failed ou expired.
created_afterdatetime
Ne renvoie que les messages créés après cette date.
limitinteger
Nombre d’éléments par page, de 1 à 100 (20 par défaut).
starting_afterstring
Curseur : l’identifiant du dernier élément de la page précédente.

Codes de retour

  • 200Une page de résultats (éventuellement vide).
curl "https://api.mysms.ma/v1/messages?status=failed&limit=20" \  -H "Authorization: Bearer $MYSMS_KEY"
import MySms from '@mysms/node';const sms = new MySms(process.env.MYSMS_KEY);const page = await sms.messages.list({  status: 'failed',  limit: 20,});
use MySms\Client;$sms = new Client(env('MYSMS_KEY'));$page = $sms->messages->list([    'status' => 'failed',    'limit' => 20,]);
import osfrom mysms import Clientclient = Client(os.environ["MYSMS_KEY"])page = client.messages.list(    status="failed",    limit=20,)
200
Réponse
{  "object": "list",  "data": [    { "id": "msg_8aB1", "status": "failed", "error_code": "unreachable" },    { "id": "msg_7cD4", "status": "failed", "error_code": "invalid_number" }  ],  "has_more": true,  "next_cursor": "msg_7cD4"}

Solde

Le crédit prépayé de votre compte.

GET/v1/balance

Lire le solde

Renvoie le crédit prépayé disponible, en dirhams hors taxes. Le crédit n’expire pas. Les recharges se font par virement bancaire depuis le tableau de bord (paiement par carte bientôt disponible).

Codes de retour

  • 200Solde disponible et montant réservé par les envois programmés.
curl https://api.mysms.ma/v1/balance \  -H "Authorization: Bearer $MYSMS_KEY"
import MySms from '@mysms/node';const sms = new MySms(process.env.MYSMS_KEY);const balance = await sms.balance.retrieve();
use MySms\Client;$sms = new Client(env('MYSMS_KEY'));$balance = $sms->balance->retrieve();
import osfrom mysms import Clientclient = Client(os.environ["MYSMS_KEY"])balance = client.balance.retrieve()
200
Réponse
{  "object": "balance",  "available": 1250.40,  "reserved": 107.00,  "currency": "MAD"}

Verify

Codes OTP générés, envoyés et vérifiés pour vous.

POST/v1/verify

Envoyer un code

Génère un code à usage unique, l’envoie par SMS et gère son expiration. Vous ne stockez jamais le code.

Corps de la requête

tostringobligatoire
Numéro à vérifier, au format E.164.
senderstring
Sender ID à utiliser. Par défaut, celui du compte.
langstring
Langue du message : fr, ar ou en (fr par défaut).
lengthinteger
Nombre de chiffres, de 4 à 8 (6 par défaut).
ttlinteger
Durée de validité en secondes, de 60 à 900 (300 par défaut).

Codes de retour

  • 201Code généré et envoyé.
  • 422Numéro invalide ou paramètre hors limites.
  • 429Trop de demandes pour ce numéro, réessayez plus tard.
curl https://api.mysms.ma/v1/verify -X POST \  -H "Authorization: Bearer $MYSMS_KEY" \  -H "Content-Type: application/json" \  -d '{    "to": "+212612345678",    "sender": "WalletMA",    "lang": "fr"  }'
import MySms from '@mysms/node';const sms = new MySms(process.env.MYSMS_KEY);const verification = await sms.verify.start({  to: '+212612345678',  sender: 'WalletMA',  lang: 'fr',});
use MySms\Client;$sms = new Client(env('MYSMS_KEY'));$verification = $sms->verify->start([    'to' => '+212612345678',    'sender' => 'WalletMA',    'lang' => 'fr',]);
import osfrom mysms import Clientclient = Client(os.environ["MYSMS_KEY"])verification = client.verify.start(    to="+212612345678",    sender="WalletMA",    lang="fr",)
201
Réponse
{  "id": "vrf_3Hq8",  "object": "verification",  "to": "+212612345678",  "status": "pending",  "expires_at": "2026-10-21T09:46:02+01:00"}

POST/v1/verify/check

Vérifier un code

Compare le code saisi par l’utilisateur. Après cinq essais incorrects, la vérification est bloquée.

Corps de la requête

idstringobligatoire
Identifiant renvoyé par POST /v1/verify (vrf_…).
codestringobligatoire
Code saisi par l’utilisateur.

Codes de retour

  • 200Résultat de la vérification (valid true ou false).
  • 410Vérification expirée ou bloquée après trop d’essais.
curl https://api.mysms.ma/v1/verify/check -X POST \  -H "Authorization: Bearer $MYSMS_KEY" \  -H "Content-Type: application/json" \  -d '{    "id": "vrf_3Hq8",    "code": "731044"  }'
import MySms from '@mysms/node';const sms = new MySms(process.env.MYSMS_KEY);const result = await sms.verify.check({  id: 'vrf_3Hq8',  code: '731044',});
use MySms\Client;$sms = new Client(env('MYSMS_KEY'));$result = $sms->verify->check([    'id' => 'vrf_3Hq8',    'code' => '731044',]);
import osfrom mysms import Clientclient = Client(os.environ["MYSMS_KEY"])result = client.verify.check(    id="vrf_3Hq8",    code="731044",)
Réponse
{  "id": "vrf_3Hq8",  "status": "approved",  "valid": true}
{  "error": {    "type": "invalid_request",    "code": "verification_expired",    "message": "Ce code a expiré. Demandez-en un nouveau."  }}

Webhooks

Recevez chaque changement de statut sur votre URL, signé.

POST/v1/webhooks

Créer un webhook

Enregistre une URL qui recevra les événements de votre compte. Le secret de signature n’est renvoyé qu’une fois, à la création.

Corps de la requête

urlstringobligatoire
URL HTTPS publique de votre application.
eventsstring[]obligatoire
Événements à recevoir, par exemple message.delivered.
descriptionstring
Libellé libre, affiché dans le tableau de bord.

Codes de retour

  • 201Webhook créé. Conservez le secret, il ne sera plus affiché.
  • 422URL non HTTPS ou événement inconnu.
curl https://api.mysms.ma/v1/webhooks -X POST \  -H "Authorization: Bearer $MYSMS_KEY" \  -H "Content-Type: application/json" \  -d '{    "url": "https://myshop.ma/hooks/sms",    "events": [      "message.delivered",      "message.failed"    ]  }'
import MySms from '@mysms/node';const sms = new MySms(process.env.MYSMS_KEY);const webhook = await sms.webhooks.create({  url: 'https://myshop.ma/hooks/sms',  events: ['message.delivered', 'message.failed'],});
use MySms\Client;$sms = new Client(env('MYSMS_KEY'));$webhook = $sms->webhooks->create([    'url' => 'https://myshop.ma/hooks/sms',    'events' => ['message.delivered', 'message.failed'],]);
import osfrom mysms import Clientclient = Client(os.environ["MYSMS_KEY"])webhook = client.webhooks.create(    url="https://myshop.ma/hooks/sms",    events=["message.delivered", "message.failed"],)
201
Réponse
{  "id": "wh_5Lm2",  "object": "webhook",  "url": "https://myshop.ma/hooks/sms",  "events": ["message.delivered", "message.failed"],  "secret": "whsec_••••••••••••"}

Événements

Nous envoyons un POST JSON à votre URL. Répondez par un code 2xx en moins de 10 secondes ; sinon nous relançons avec un délai croissant (1 min, 5 min, 30 min, 2 h…) pendant 24 heures.

Types d’événements

message.queued
Le message est accepté et en file d’envoi.
message.sent
Le message a été remis au réseau de l’opérateur.
message.delivered
L’opérateur confirme la livraison sur le téléphone.
message.failed
Échec définitif, avec un error_code.
message.inbound
Une réponse SMS est arrivée sur votre numéro.
verify.approved
Un code Verify a été validé.
sender.updated
Le statut d’un Sender ID a changé.
balance.low
Votre solde passe sous le seuil d’alerte.
message.delivered
POST → votre URL
{  "id": "evt_2Rt6",  "type": "message.delivered",  "created_at": "2026-10-21T09:41:03+01:00",  "data": {    "id": "msg_9fK2",    "status": "delivered",    "operator": "maroc_telecom",    "metadata": { "order_id": "20481" }  }}

Signature

Chaque requête porte un en-tête MySms-Signature : un horodatage t et une signature v1, HMAC-SHA256 de t.corps avec le secret du webhook.

  • Calculez la signature sur le corps brut, avant tout décodage JSON.
  • Comparez en temps constant et rejetez les horodatages de plus de 5 minutes (protection contre le rejeu).
  • Un même événement peut arriver deux fois : dédoublonnez sur son id.
Vérifier la signature
import crypto from 'node:crypto';// En-tête : MySms-Signature: t=1792569600,v1=5f2c…export function verify(rawBody, header, secret) {  const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');  const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;  return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));}
// En-tête : MySms-Signature: t=1792569600,v1=5f2c…$payload = $request->getContent();[$t, $v1] = sscanf($request->header('MySms-Signature'), 't=%d,v1=%s');$expected = hash_hmac('sha256', "{$t}.{$payload}", $webhookSecret);if (! hash_equals($expected, $v1) || abs(time() - $t) > 300) {    abort(401);}
import hmac, hashlib, time# En-tête : MySms-Signature: t=1792569600,v1=5f2c…def verify(raw_body: bytes, header: str, secret: str) -> bool:    parts = dict(p.split("=", 1) for p in header.split(","))    signed = f"{parts['t']}.".encode() + raw_body    expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()    fresh = abs(time.time() - int(parts["t"])) < 300    return fresh and hmac.compare_digest(expected, parts["v1"])

Sender ID

Le nom de votre marque comme expéditeur.

GET/v1/senders

Lister les Sender ID

Liste les noms d’expéditeur de votre compte et leur statut de validation auprès des opérateurs.

Codes de retour

  • 200Sender ID du compte : approved, pending ou rejected.
curl https://api.mysms.ma/v1/senders \  -H "Authorization: Bearer $MYSMS_KEY"
import MySms from '@mysms/node';const sms = new MySms(process.env.MYSMS_KEY);const senders = await sms.senders.list();
use MySms\Client;$sms = new Client(env('MYSMS_KEY'));$senders = $sms->senders->list();
import osfrom mysms import Clientclient = Client(os.environ["MYSMS_KEY"])senders = client.senders.list()
200
Réponse
{  "object": "list",  "data": [    { "id": "snd_1aZ9", "name": "MyShop", "status": "approved" },    { "id": "snd_2bY8", "name": "MyShopPromo", "status": "pending" }  ],  "has_more": false}

POST/v1/senders

Demander un Sender ID

Soumet un nouveau nom d’expéditeur pour validation. Le statut passe à approved une fois accepté ; un événement sender.updated est envoyé.

Corps de la requête

namestringobligatoire
3 à 11 caractères alphanumériques, sans espace, proches de votre marque.
use_casestringobligatoire
transactional, otp ou marketing.
samplestring
Exemple de message, qui accélère la validation.

Codes de retour

  • 201Demande enregistrée, en attente de validation.
  • 409Ce nom existe déjà sur votre compte.
curl https://api.mysms.ma/v1/senders -X POST \  -H "Authorization: Bearer $MYSMS_KEY" \  -H "Content-Type: application/json" \  -d '{    "name": "MyShop",    "use_case": "transactional",    "sample": "Commande #20481 confirmée."  }'
import MySms from '@mysms/node';const sms = new MySms(process.env.MYSMS_KEY);const sender = await sms.senders.create({  name: 'MyShop',  use_case: 'transactional',  sample: 'Commande #20481 confirmée.',});
use MySms\Client;$sms = new Client(env('MYSMS_KEY'));$sender = $sms->senders->create([    'name' => 'MyShop',    'use_case' => 'transactional',    'sample' => 'Commande #20481 confirmée.',]);
import osfrom mysms import Clientclient = Client(os.environ["MYSMS_KEY"])sender = client.senders.create(    name="MyShop",    use_case="transactional",    sample="Commande #20481 confirmée.",)
201
Réponse
{  "id": "snd_3cX7",  "object": "sender",  "name": "MyShop",  "status": "pending"}

Tarifs

Payez uniquement ce que vous envoyez

Crédit prépayé sans expiration, rechargé par virement bancaire (paiement par carte bientôt disponible). Les clés de test sont gratuites.

Commencez à envoyer en 5 minutes

Créez un compte gratuit, recevez 100 messages de test et constatez la rapidité de la livraison locale. Sans carte bancaire.