- Créez un compte, puis une clé sandbox dans Tableau de bord → Clés API, et exportez-la : export ENVOISMS_API_KEY=env_test_…
- Collez un des exemples ci-contre : il fonctionne tel quel avec cette clé, sans rien envoyer ni débiter.
- Déclarez un webhook pour recevoir les statuts de remise et les réponses.
- Rechargez, créez une clé live (smr_…) et remplacez la clé sandbox dans votre configuration.
1. Envoyer un SMS
Un seul appel. Avec une clé sandbox (env_test_…), l'envoi est simulé : rien ne part, rien n'est débité, et la réponse indique le prix qu'aurait payé une clé live (simulated_cost).
RéponseHTTP/1.1 202 Accepted
{
"id": "msg_9f2c4e1a7b3d",
"to": "+212612345678",
"channel": "sms",
"cascade": false,
"status": "queued",
"cost": { "eur": 0, "mad": 0 },
"sandbox": true,
"simulated_cost": { "eur": 0.0704, "mad": 0.792 },
"segments": 1,
"created_at": "2026-10-22T10:30:00.000Z"
}curl https://api.envoisms.ma/v1/messages \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"to": "+212612345678",
"message": "Your order #1042 has shipped."
}'2. Envoyer un modèle WhatsApp
WhatsApp part de votre propre numéro WhatsApp Business, connecté une fois dans le tableau de bord (sinon 403 WHATSAPP_NOT_CONNECTED). Hors de la fenêtre de 24 h, seul un modèle approuvé peut ouvrir la conversation ; ses variables {{1}}, {{2}}… se passent dans metadata.variables, dans l'ordre.
RéponseHTTP/1.1 202 Accepted
{
"id": "msg_4b81d0c2e9f7",
"to": "+212612345678",
"channel": "whatsapp",
"cascade": false,
"status": "queued",
"cost": { "eur": 0.05778, "mad": 0.65 },
"segments": 1,
"created_at": "2026-10-22T10:31:00.000Z"
}curl https://api.envoisms.ma/v1/messages \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+212612345678",
"channel": "whatsapp",
"template": { "name": "order_shipped", "language": "fr" },
"metadata": { "variables": ["Karim", "#1042"] }
}'3. Vérifier un numéro (OTP)
EnvoiSMS génère le code, l'envoie, compte les essais et le fait expirer (10 min par défaut). Vous ne stockez que session_id. Avec une clé sandbox, la réponse contient sandbox_code pour tester /verify/check sans téléphone.
RéponsePOST /v1/verify/send → 202 Accepted
{
"session_id": "vrf_2c7e9a14b0d3",
"message_id": "msg_e1a07c93b5f2",
"to": "+212612345678",
"channel": "sms",
"expires_at": "2026-10-22T10:42:00.000Z",
"resend_after_seconds": 30,
"fallback_channels": [],
"status": "sent",
"sandbox": true,
"simulated": true,
"sandbox_code": "483921"
}
POST /v1/verify/check → 200 OK
{ "session_id": "vrf_2c7e9a14b0d3", "verified": true, "verified_at": "2026-10-22T10:33:12.000Z" }# 1. Send the code
curl https://api.envoisms.ma/v1/verify/send \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "to": "+212612345678", "brand": "MyApp" }'
# 2. Check what the user typed
curl https://api.envoisms.ma/v1/verify/check \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "session_id": "vrf_…", "code": "483921" }'Authentification
Authentification#
Chaque requête /v1 porte une clé API dans l'en-tête Authorization. Créez, limitez et révoquez vos clés dans Tableau de bord → Clés API ; une clé n'est affichée qu'une fois, à sa création. Ne l'utilisez que côté serveur.
smr_… envoie réellement et débite le solde ; env_test_… simule tout, gratuitement. Même URL pour les deux.
Chaque clé porte une liste de permissions (send, verify, status…). Hors de cette liste : 403 FORBIDDEN.
Optionnelle, par clé. Un appel venant d'une autre IP reçoit 401 UNAUTHORIZED.
X-RateLimit-Limit, X-RateLimit-Remaining et X-RateLimit-Reset (horodatage Unix) sur chaque réponse.
POST /v1/messages HTTP/1.1
Host: api.envoisms.ma
Authorization: Bearer smr_your_live_key
Content-Type: application/json
Idempotency-Key: 6f1d2c9e-4b7a-4f3e-9d21-0c5e8a7b3f10
HTTP/1.1 202 Accepted
X-RateLimit-Limit: 180
X-RateLimit-Remaining: 179
X-RateLimit-Reset: 1792663860Confirmation de commande (SMS)
Envoyer une confirmation de commande par SMS#
Mis à jour le
- Créez une clé sandbox et exportez-la : export ENVOISMS_API_KEY=env_test_…
- Facultatif : demandez votre nom d'expéditeur (POST /v1/sender-ids, 15 à 30 jours). En attendant, omettez sender_id : l'expéditeur par défaut s'applique.
- Sur l'événement « commande payée », envoyez le SMS avec Idempotency-Key = l'identifiant de la commande : un double déclenchement n'envoie qu'un message.
- Déclarez un webhook sur message.delivered et message.failed ; metadata.order_id vous revient dans chaque livraison.
- Testez +212600000002 pour voir la branche d'échec, puis passez à une clé live (smr_…).
curl https://api.envoisms.ma/v1/messages \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042-paid" \
-d '{
"to": "+212612345678",
"message": "Commande #1042 confirmée : 349 MAD. Livraison prévue jeudi.",
"metadata": { "order_id": "1042" }
}'Commandes COD sur WhatsApp
Faire confirmer les commandes en paiement à la livraison sur WhatsApp#
Mis à jour le
Avec WooCommerce, le plugin EnvoiSMS (v2.0.0) fait tout : chaque commande COD attend, le client reçoit un message avec les boutons Confirmer / Annuler, et son clic met la commande à jour.
- Connectez votre numéro WhatsApp dans Tableau de bord → WhatsApp (vous pouvez garder l'application WhatsApp Business sur votre téléphone).
- Installez le plugin depuis github.com/envoisms/envoisms-woocommerce (WooCommerce 7.1+, PHP 8.0+) et collez votre clé API dans WooCommerce → EnvoiSMS Maroc.
- Cochez COD « Activer » et enregistrez : le plugin déclare son webhook (message.inbound) et garde le secret de signature.
- Cliquez « Créer les modèles WhatsApp » (français et arabe) : EnvoiSMS les relit puis les soumet à Meta ; leur statut s'affiche sur la même page.
- Une fois les modèles approuvés, passez une commande de test en paiement à la livraison. Vous pouvez suivre leur statut par l'API :
curl "https://api.envoisms.ma/v1/templates?channel=whatsapp" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"Campagne marketing (API)
Lancer une campagne marketing par l'API#
Mis à jour le
- Créez une liste : POST /v1/contacts/lists {"name": "Clients fidèles"} → lst_…
- Importez les contacts : POST /v1/contacts/import {"list_id", "contacts": [{"phone", "name"}]}.
- Enregistrez la preuve du consentement de chacun : POST /v1/consents {"phone", "purpose": "marketing", "status": "granted", "source": "form", "evidence": {…}}.
- Créez la campagne : en SMS avec body (dites comment se désabonner) ; en WhatsApp avec template_id, template_variables et "consent_attested": true. scheduled_at la programme.
- Lancez-la (POST /v1/campaigns/{id}/send), suivez-la avec GET /v1/campaigns/{id}, mettez-la en pause avec /pause. Les désabonnés (STOP) sont exclus automatiquement.
curl https://api.envoisms.ma/v1/campaigns \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Soldes de novembre",
"channel": "sms",
"list_id": "lst_…",
"body": "-20% ce week-end avec le code NOV20.",
"scheduled_at": "2026-11-07T09:00:00Z"
}'Boîte de réception (webhooks)
Une boîte de support à double sens#
Mis à jour le
- Déclarez un webhook sur message.inbound (et message.read si vous affichez les accusés de lecture).
- Vérifiez la signature puis rangez le message par numéro : data.from est le client, data.channel le canal.
- Lisez l'historique d'un client avec GET /v1/conversations/{phone}.
- Répondez avec POST /v1/conversations/{phone}/messages. Sur WhatsApp, un texte libre n'est possible que 24 h après le dernier message du client ; au-delà, envoyez un modèle (template_name) ou passez en SMS.
- Marquez le fil lu avec POST /v1/conversations/{phone}/mark-read. Un STOP du client arrive en contact.optout et bloque les envois suivants.
curl https://api.envoisms.ma/v1/conversations/+212612345678/messages \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "message": "Bonjour, votre colis part aujourd'"'"'hui." }'Plateformes et agences
Connecter le WhatsApp de vos clients#
Mis à jour le
Un numéro WhatsApp ne se connecte pas par l'API : Meta exige que le propriétaire de l'entreprise se connecte lui-même (Embedded Signup). Chaque client a donc son propre compte EnvoiSMS, qui paie ses envois.
- Le client crée son compte sur envoisms.ma (inscription par e-mail).
- Il connecte son numéro dans Tableau de bord → WhatsApp, en se connectant à Meta (Embedded Signup ; coexistence avec l'application WhatsApp Business possible).
- Il vous crée une clé API limitée à ce dont vous avez besoin (par ex. send, status) et vous la transmet ; vous la gardez côté serveur, une par client.
- Vérifiez que le numéro peut envoyer : GET /v1/whatsapp/waba-connection → readiness.can_send et readiness.blockers.
- Créez ses modèles (POST /v1/templates) et envoyez avec sa clé. Ses envois, son solde et ses webhooks restent les siens.
curl https://api.envoisms.ma/v1/whatsapp/waba-connection \
-H "Authorization: Bearer $CLIENT_API_KEY"Sandbox et tests
Tester sans rien envoyer ni payer#
Mis à jour le
Créez une clé sandbox dans Tableau de bord → Clés API (case « Sandbox »). Elle commence par env_test_ ; une clé live commence par smr_. Les deux appellent la même URL, https://api.envoisms.ma/v1 : c'est la clé qui décide, pas l'adresse.
Une clé sandbox est validée exactement comme une clé live (format du numéro, Sender ID, fenêtre de 24 h, contenu), mais rien ne part et rien n'est débité. Ce qu'une clé live aurait refusé est signalé dans warnings plutôt que masqué.
- POST /v1/messages renvoie sandbox: true, cost à zéro et simulated_cost : le prix réel de ce même envoi.
- POST /v1/verify/send renvoie sandbox_code : le code qu'aurait reçu l'utilisateur.
- Chaque clé ne voit que son propre trafic : GET /v1/messages avec une clé sandbox ne liste que les envois simulés, et inversement.
- Numéros de test (POST /v1/messages, clé sandbox uniquement) : +212600000002 échoue (message.failed, SANDBOX_TEST_FAILED) ; +212600000003 est non délivrable (message.undeliverable) ; +212600000004 est désabonné (403 OPTED_OUT dès la requête) ; +212600000005 ne reçoit jamais d'accusé (reste « sent ») ; tout autre numéro, dont +212600000001, est délivré (message.delivered) environ 2 s après l'envoi.
- Une clé sandbox ne peut pas devenir live : créez une clé live le moment venu et changez la variable d'environnement.
SMS ou WhatsApp ?
Choisir le bon canal#
Mis à jour le
SMS est le canal par défaut : omettez channel. Il atteint tout téléphone, sans numéro connecté, sans modèle, sans fenêtre de 24 h. C'est le bon choix pour une notification ou une alerte à n'importe quel client.
WhatsApp ("channel": "whatsapp") demande votre propre numéro WhatsApp Business connecté. Pour écrire en premier, ou plus de 24 h après le dernier message du client, utilisez un modèle approuvé ; un texte libre n'est accepté que dans les 24 h qui suivent un message du client (sinon 400 OUT_OF_24H_WINDOW, rien débité). WhatsApp est facturé selon la catégorie du modèle.
Les deux à la fois : "cascade": true essaie WhatsApp puis bascule en SMS si WhatsApp refuse, échoue ou ne confirme pas la remise dans cascade_timeout secondes (120 par défaut). Seul ce qui est réellement parti est facturé.
Codes de vérification : préférez POST /v1/verify/send, qui gère génération, essais, expiration et renvoi, plutôt que d'envoyer vous-même un code par /v1/messages.
Idempotence et reprises
Réessayer sans envoyer deux fois#
Mis à jour le
Ajoutez un en-tête Idempotency-Key (255 caractères max., par ex. un UUID ou « commande-1042-expediee ») à POST /v1/messages et POST /v1/messages/bulk (et leurs équivalents /v1/waba/messages). Si l'appel expire ou que votre processus plante, renvoyez la même requête avec la même clé : l'envoi n'a lieu qu'une fois, et la reprise renvoie le corps JSON d'origine avec Idempotency-Replayed: true.
- La clé est conservée 24 h, par compte. Toute réponse inférieure à 500 est conservée et rejouée, y compris une erreur 4xx (par ex. 429 ou 402) : après une 4xx, corrigez la cause et utilisez une NOUVELLE clé.
- La clé est liée au corps exact (octet pour octet) : une reprise renvoie le même corps. Un corps différent avec la même clé — même un JSON simplement reformaté — renvoie 422 IDEMPOTENCY_KEY_REUSED.
- Une erreur 5xx ou un délai dépassé n'est pas conservé : réessayez avec la MÊME clé, avec un délai croissant (1 s, 2 s, 4 s…).
- 409 IDEMPOTENCY_IN_FLIGHT : la requête d'origine est encore en cours ; attendez Retry-After puis réessayez avec la même clé. 422 IDEMPOTENCY_KEY_REUSED : la clé a déjà servi pour un corps différent.
- 429 RATE_LIMITED : attendez le nombre de secondes indiqué par Retry-After. X-RateLimit-Limit, X-RateLimit-Remaining et X-RateLimit-Reset accompagnent chaque réponse authentifiée.
Pagination
Parcourir une liste#
Mis à jour le
Les listes acceptent limit et offset et renvoient { "data": [...], "limit", "offset", "total" }. Pour GET /v1/messages : 50 par défaut, 200 au maximum, du plus récent au plus ancien, filtrable par status, channel, campaign_id, from_date et to_date. Ajoutez format=csv pour obtenir tout le jeu filtré en un fichier.
curl "https://api.envoisms.ma/v1/messages?status=failed&limit=100&offset=0" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"Permissions des clés
Une clé, le minimum de droits#
Mis à jour le
Chaque clé porte des permissions. Un appel hors de ses permissions renvoie 403 FORBIDDEN. Donnez à chaque serveur la plus petite liste qui suffit : un backend d'OTP n'a besoin que de verify et status.
- send — POST /v1/messages, /v1/messages/bulk, /v1/templates, /v1/conversations, /v1/lookup
- status — GET /v1/messages, POST /v1/verify/check et le reste de /v1/verify (sessions, apps, lookup, void)
- verify — POST /v1/verify/send, /v1/verify/resend
- balance — GET /v1/billing/balance ; billing — le reste de /v1/billing
- contacts (contacts, listes, consentements), campaigns, webhooks, optouts, analytics, api_keys, sender_id
- Paramétrage WhatsApp (/v1/whatsapp/…) : les modifications — connexion d'un numéro, outils, profil, relais — exigent une session du tableau de bord (une clé API reçoit 403). Seule la lecture est ouverte aux clés, par ex. GET /v1/whatsapp/waba-connection.
Versions et changements
Ce qui peut changer, et comment vous le saurez#
Mis à jour le
La version est dans le chemin : /v1. Dans /v1, l'API n'évolue que par ajouts — nouveaux endpoints, champs optionnels, champs de réponse, codes d'erreur, événements webhook. Ignorez les champs que vous ne connaissez pas. Un changement incompatible sort en /v2, à côté de /v1, jamais à sa place.
Retirer quelque chose de /v1 est annoncé au moins 12 mois à l'avance : par e-mail au titulaire du compte, dans le journal des changements, et sur la route elle-même, dont chaque réponse porte alors Deprecation (date de l'annonce), Sunset (date de fin) et Link vers le guide de migration. Seule exception : un correctif de sécurité, annoncé dès que possible. Aucune route de /v1 n'est dépréciée aujourd'hui.
Limites par endpoint
Les limites, endpoint par endpoint#
Mis à jour le
Toutes les routes partagent la limite par minute de la clé (Starter 60, Business 180, Pro 500, Enterprise 1 200 par défaut ; X-RateLimit-* sur chaque réponse ; 429 RATE_LIMITED avec Retry-After au-delà). Certaines routes ont en plus leurs propres règles :
- POST /v1/messages/bulk : 10 000 messages par appel.
- POST /v1/messages et /bulk : quotas quotidien et mensuel optionnels du compte (PATCH /v1/billing/quota) ; au-delà, 429 QUOTA_EXCEEDED jusqu'à minuit UTC.
- POST /v1/verify/send et /resend : par numéro, un code toutes les 30 s en SMS et 60 s sur WhatsApp (429 OTP_COOLDOWN), 5 par heure et 10 par jour (429 OTP_RATE_LIMIT) ; 200 par jour et par compte ; hors Maroc, OTP_COUNTRY_PAUSED quand trop peu de codes sont confirmés.
- POST /v1/verify/check : 3 essais par session par défaut (réglables par application Verify), puis 429 MAX_ATTEMPTS.
- POST /v1/inbound-webhooks/catch/{id} (sans clé) : 120 requêtes par minute par webhook, 64 Ko par requête.
- Vos webhooks : nous attendons 10 s au plus votre réponse, puis réessayons 3 fois (1 min, 10 min, 1 h).
Webhooks
Recevoir les statuts et les réponses#
Mis à jour le
Créez un webhook avec POST /v1/webhooks ({"url": "https://…", "events": ["message.delivered", "message.failed"]}). La réponse contient le secret de signature, affiché une seule fois ; POST /v1/webhooks/{id}/rotate-secret en émet un nouveau. events accepte un nom exact, un espace de noms ("message.*") ou "*".
Chaque livraison est un POST JSON {"event", "data", "timestamp"}. Pour un statut de message, data vaut {id, to, channel, status, error_code?, error_message?, metadata?} — metadata est celle que vous avez envoyée. Répondez 2xx avant le timeout de 10 secondes, puis traitez de façon asynchrone ; dédupliquez sur data.id + event, car une livraison peut arriver deux fois.
Vérifiez X-EnvoiSMS-Signature-256: t=<secondes>,v1=<hex> : calculez HMAC-SHA256(secret, t + "." + corps brut), comparez à v1 en temps constant, et refusez si t a plus de 5 minutes. L'ancien en-tête X-EnvoiSMS-Signature (sha256=<hex> du corps seul, sans horodatage) est encore envoyé ; préférez le nouveau, qui empêche le rejeu. L'événement est aussi dans X-EnvoiSMS-Event.
- Reprises : une réponse 5xx, 429 ou une erreur réseau est retentée 3 fois (après 1 min, 10 min et 1 h, soit 4 tentatives). Une autre 4xx n'est pas retentée.
- Journal et rejeu : GET /v1/webhooks/logs liste les 100 dernières tentatives ; POST /v1/webhooks/logs/{log_id}/replay renvoie une livraison, signée à nouveau. POST /v1/webhooks/{id}/test envoie un exemple.
- Relais WhatsApp vers votre serveur (numéro connecté réglé en mode « webhook » dans le tableau de bord) : les messages et statuts arrivent au format natif de WhatsApp Cloud, signés par X-Hub-Signature-256: sha256=<hex> = HMAC-SHA256(webhook_secret du numéro, corps brut). Ce secret est affiché une fois lorsqu'il est créé ou renouvelé (rotate_webhook_secret). Sans secret, rien n'est relayé. Pas d'horodatage : dédupliquez sur l'identifiant du message.
Événements
message.sentLe message a été remis au réseau (opérateur ou WhatsApp).
message.deliveredLe téléphone du destinataire a confirmé la réception.
message.readWhatsApp : le destinataire a lu le message (s'il n'a pas désactivé les confirmations de lecture).
message.failedL'envoi a échoué ; error_code et error_message disent pourquoi (voir Codes d'échec de remise).
message.undeliverableLe réseau a confirmé que le message ne peut pas être remis (numéro inexistant, message expiré chez l'opérateur).
message.fallbackCascade : le message passe au canal suivant. channel = nouveau canal, previous_channel = l'ancien, error_code = la raison.
message.inboundUn contact vous a écrit (réponse SMS ou message WhatsApp, y compris un clic sur un bouton ou une liste).
message.reactionWhatsApp : un contact a réagi à l'un de vos messages avec un emoji.
contact.optout / contact.optinUn contact s'est désabonné (STOP) ou réabonné. Les envois vers un numéro désabonné sont refusés (403 OPTED_OUT).
lead.qualifiedLe bot WhatsApp de qualification a terminé de qualifier un prospect.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
// Keep the RAW body: the signature is computed over the exact bytes received.
app.post('/webhooks/envoisms', express.raw({ type: 'application/json' }), (req, res) => {
const header = req.get('X-EnvoiSMS-Signature-256') || '';
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto
.createHmac('sha256', process.env.ENVOISMS_WEBHOOK_SECRET)
.update(`${parts.t}.${req.body}`)
.digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
const valid = parts.v1?.length === expected.length
&& crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected));
if (!fresh || !valid) return res.sendStatus(401);
const { event, data } = JSON.parse(req.body);
res.sendStatus(200); // answer fast…
queue.push({ event, data }); // …then process (de-duplicate on data.id + event)
});POST /webhooks/envoisms HTTP/1.1
Content-Type: application/json
X-EnvoiSMS-Event: message.delivered
X-EnvoiSMS-Signature-256: t=1792663800,v1=5f0c…e9
X-EnvoiSMS-Signature: sha256=a41b…7d
User-Agent: EnvoiSMS-Webhook/1.0 (formerly SMSMaroc)
{
"event": "message.delivered",
"data": {
"id": "msg_9f2c4e1a7b3d",
"to": "+212612345678",
"channel": "sms",
"status": "delivered",
"metadata": { "order_id": "1042" }
},
"timestamp": "2026-10-22T10:30:04.000Z"
}Erreurs
Erreurs#
Une erreur renvoie un statut HTTP 4xx ou 5xx et toujours la même enveloppe. Branchez votre code sur error.code, jamais sur le texte : error.message est une phrase en anglais qui peut s'améliorer. Chaque code ci-dessous dit quoi faire.
HTTP/1.1 400 Bad Request
{
"error": {
"code": "INVALID_PHONE",
"message": "to must be E.164 format, for example +212612345678",
"docs": "https://envoisms.ma/en/docs#errors"
}
}Authentification et accès
UNAUTHORIZEDHTTP 401En-tête Authorization absent, clé inconnue ou révoquée, IP absente de la liste autorisée de la clé, ou compte inactif ; error.message dit lequel. Envoyez Authorization: Bearer <clé> et vérifiez la clé dans Tableau de bord → Clés API.
FORBIDDENHTTP 403La clé n'a pas la permission de cette route (voir Permissions des clés), ou la route exige une session du tableau de bord. Ajoutez la permission à la clé ou utilisez-en une autre.
ACCOUNT_SUSPENDEDHTTP 403Le compte est suspendu. Contactez le support ; aucun envoi n'est possible d'ici là.
FORBIDDEN_ASSETHTTP 403Ce numéro WhatsApp n'appartient pas à votre compte. Utilisez un phone_number_id de vos propres numéros connectés.
PAYMENT_NOT_SETTLEDINVALID_CREDENTIALMACHINE_PAYMENTS_DISABLEDHTTP 402 / 400 / 501Paiements machine (Authorization: Payment …) : le dépôt n'est pas encore confirmé (attendez puis réessayez), le justificatif ne se décode pas, ou la fonction est désactivée.
Requête mal formée
INVALID_JSONHTTP 400Le corps n'est pas du JSON valide. Envoyez Content-Type: application/json et un objet JSON.
MISSING_FIELDMISSING_FIELDSINVALID_FIELDMISSING_BODYMISSING_MESSAGEMISSING_URLHTTP 400Un champ obligatoire manque ou a une valeur hors limites ; error.message nomme le champ. Corrigez-le et renvoyez.
INVALID_PHONEHTTP 400Le numéro n'est pas au format E.164. Envoyez +indicatif puis le numéro, sans espaces : +212612345678.
INVALID_URLHTTP 400Une URL n'est pas en https:// ou n'est pas valide (webhook, média, bouton). Utilisez une URL https publique.
INVALID_CHANNELCHANNEL_NOT_CONFIGUREDCHANNEL_DISABLEDUNSUPPORTED_CHANNELHTTP 400Canal inconnu ou indisponible. Pour /v1/messages, channel vaut sms (défaut) ou whatsapp ; pour /v1/consents, whatsapp, sms ou any.
INVALID_PURPOSEINVALID_STATUSINVALID_SOURCEINVALID_EVIDENCEINVALID_DATEHTTP 400Consentements : purpose = marketing | transactional | otp | service ; status = granted | withdrawn ; source = api | form | import | dashboard ; evidence = objet JSON < 4 Ko ; dates ISO 8601, recorded_at jamais dans le futur.
INVALID_PERMISSIONSSANDBOX_IMMUTABLEHTTP 400Clés API : donnez au moins une permission valide ; une clé ne passe jamais de sandbox à live (ou l'inverse) — créez-en une nouvelle.
NOT_FOUNDLIST_NOT_FOUNDLEAD_NOT_FOUNDTEMPLATE_NOT_FOUNDAPP_NOT_FOUNDSHEET_NOT_FOUNDHTTP 404La ressource n'existe pas pour ce compte (ou pour ce mode de clé : une clé sandbox ne voit pas les objets live). Vérifiez l'identifiant et la clé.
METHOD_NOT_ALLOWEDHTTP 405Cette méthode HTTP n'existe pas sur cette route. Voir la référence de l'endpoint.
UNSUPPORTED_MEDIA_TYPEPAYLOAD_TOO_LARGEFILE_TOO_LARGEINVALID_FILE_TYPEINVALID_FORM_DATAMISSING_FILEEMPTY_SHEETPARSE_FAILEDMAPPING_ERRORFETCH_FAILEDINVALID_PATHNO_FILE_PROVIDEDNO_DATA_PROVIDEDUPLOAD_FAILEDHTTP 400 / 413 / 415 / 502Fichier ou import refusé : type non pris en charge, trop volumineux, vide, colonnes non reconnues ou URL injoignable. error.message dit lequel ; corrigez le fichier et renvoyez.
Envoi (SMS et commun)
INSUFFICIENT_BALANCEHTTP 402Solde trop bas pour cet envoi. Rechargez dans Tableau de bord → Facturation ; rien n'a été débité.
INTERNATIONAL_REQUIRES_TOPUPHTTP 403Les envois hors Maroc sont réservés aux comptes qui ont déjà rechargé. Faites une première recharge.
QUOTA_EXCEEDEDHTTP 429Le quota quotidien fixé sur votre compte est atteint ; il repart à minuit UTC. Relevez-le dans le tableau de bord si besoin. Après ce 429, réessayez avec une nouvelle Idempotency-Key.
OPTED_OUTHTTP 403Ce destinataire s'est désabonné de vos messages (STOP). Ne renvoyez pas ; retirez-le de vos listes. Il peut se réabonner lui-même.
SENDER_ID_TOO_LONGSENDER_ID_TOO_SHORTSENDER_ID_INVALIDHTTP 400Sender ID alphanumérique de 3 à 11 caractères (lettres, chiffres, espace). Corrigez-le ou omettez-le pour l'expéditeur par défaut.
SENDER_ID_GENERICHTTP 400Sender ID générique (INFO, ALERT, SERVICE…), pas une marque. Utilisez votre nom de marque ou omettez-le.
SENDER_ID_PROTECTED_BRANDHTTP 403Le Sender ID désigne ou imite une institution protégée (banque, opérateur, administration). Réservé au compte approuvé pour ce nom exact.
SENDER_ID_NOT_APPROVEDSENDER_ID_PENDINGSENDER_ID_IN_REVIEWSENDER_ID_REJECTEDSENDER_ID_APPROVEDHTTP 403 / 409Demande de Sender ID : pas encore approuvée, en cours d'examen, refusée, ou déjà approuvée (409). Suivez l'état dans Paramètres → Sender IDs.
SENDER_ID_NOT_REGISTEREDHTTP 403N'est plus renvoyé depuis le 27/09/2026 : un Sender ID non enregistré part par la route standard. Conservé pour que le code qui le teste reste valide.
SPAM_OR_PHISHING_DETECTEDCONTENT_BLOCKEDHTTP 400Contenu refusé : lien d'hameçonnage, message parlant au nom d'une banque, d'un opérateur ou d'une administration, ou violation des conditions d'usage. Le compte est suspendu en attente de revue. Contestation : POST /v1/moderation/appeals avec error.appeal.id, error.appeal.token et une note (sans clé API).
CONTENT_REFUSEDHTTP 400Contenu refusé par le contrôle anti-abus (premier refus). Rien n'est parti, le compte reste actif ; un second refus le suspend. Reformulez, ou contestez via POST /v1/moderation/appeals.
INVALID_APPEALAPPEAL_NOT_FOUNDAPPEAL_ALREADY_FILEDHTTP 400 / 404 / 409Contestation : id ou token absent ou erroné, ou une contestation est déjà ouverte pour ce refus.
EMPTY_BATCHTOO_MANY_MESSAGESHTTP 400POST /v1/messages/bulk accepte de 1 à 10 000 messages par appel. Découpez les lots plus grands.
SEND_PERSIST_FAILEDHTTP 502Le message n'a pas pu être enregistré ; rien n'est parti et rien n'a été débité. Réessayez avec la même Idempotency-Key.
WHATSAPP_NOT_CONNECTEDHTTP 403Aucun numéro WhatsApp Business connecté au compte. Connectez-le dans le tableau de bord ; pour un simple texte, omettez channel (SMS). Rien n'a été débité.
OUT_OF_24H_WINDOWHTTP 400Message WhatsApp libre à un contact qui ne vous a pas écrit depuis 24 h. Envoyez un modèle approuvé (template) ou un SMS. Rien n'a été débité.
WHATSAPP_ONLY_FIELDBUTTONS_NOT_SUPPORTEDINTERACTIVE_NOT_SUPPORTEDHTTP 400Un champ propre à WhatsApp (buttons, interactive, reaction, contacts, location, sticker, context) a été envoyé sur SMS. Ajoutez "channel": "whatsapp" ou retirez le champ.
CONFLICTING_MESSAGE_TYPESCASCADE_NOT_SUPPORTEDINVALID_MESSAGEHTTP 400Un message porte un seul type de contenu ; reaction, contacts, location et sticker ne se combinent avec rien et n'ont pas d'équivalent SMS (pas de cascade).
INVALID_REACTIONINVALID_CONTEXTINVALID_CONTACTSINVALID_LOCATIONINVALID_MEDIAHTTP 400Objet WhatsApp mal formé : reaction = {message_id, emoji} ; context = {message_id} ; contacts = 1 à 20 fiches avec name.formatted_name ; location = latitude/longitude numériques ; média = "id" OU "link" https, pas de légende sur audio/sticker.
INVALID_INTERACTIVEINVALID_INTERACTIVE_TYPEMISSING_INTERACTIVE_ACTIONINTERACTIVE_BODY_TOO_LONGINVALID_CTA_URL_ACTIONMISSING_CTA_DISPLAY_TEXTCTA_DISPLAY_TEXT_TOO_LONGINVALID_LOCATION_REQUEST_ACTIONINVALID_FLOW_ACTIONMISSING_FLOW_IDMISSING_FLOW_CTAFLOW_CTA_TOO_LONGMISSING_CAROUSEL_CARDSTOO_MANY_CAROUSEL_CARDSINVALID_PRODUCT_ACTIONINVALID_PRODUCT_LISTMISSING_ORDER_ACTIONMISSING_ORDER_CURRENCYMISSING_ORDER_ITEMSMISSING_ORDER_PARAMETERSMISSING_REFERENCE_IDINVALID_TOTAL_AMOUNTMISSING_PHONE_NUMBERHTTP 400Message interactive mal formé (liste, boutons, cta_url, flow, carrousel, produit, commande, location_request). error.message nomme le champ fautif et la limite WhatsApp dépassée.
MISSING_BUTTONS_ARRAYINVALID_BUTTONINVALID_BUTTON_COUNTINVALID_BUTTON_TEXTINVALID_BUTTON_TYPEINVALID_REPLY_BUTTONBUTTON_TEXT_TOO_LONGBUTTON_TITLE_TOO_LONGTOO_MANY_BUTTONSMISSING_COPY_CODEMISSING_LIST_BUTTONLIST_BUTTON_TOO_LONGMISSING_LIST_SECTIONSTOO_MANY_SECTIONSSECTION_TITLE_TOO_LONGINVALID_SECTION_ROWSTOO_MANY_ROWSMISSING_ROW_IDMISSING_ROW_TITLEROW_ID_TOO_LONGROW_TITLE_TOO_LONGROW_DESC_TOO_LONGHTTP 400Boutons ou liste hors des limites WhatsApp : 3 boutons max., titre de bouton 20 caractères, 10 sections et 10 lignes max., titre de ligne 24 caractères, description 72. error.message donne la limite exacte.
META_SERVICE_UNAVAILABLEHTTP 502WhatsApp était momentanément indisponible et l'envoi a échoué malgré les reprises automatiques. Réessayez dans quelques minutes.
Modèles et campagnes
TEMPLATE_PENDING_REVIEWTEMPLATE_REJECTED_BY_REVIEWHTTP 409Le modèle attend la revue EnvoiSMS (il part chez Meta une fois approuvé), ou cette revue l'a refusé : modifiez-le pour le renvoyer en revue.
TEMPLATE_EXISTSTEMPLATE_ALREADY_ON_METATEMPLATE_EDIT_NOT_ALLOWEDTEMPLATE_IMMUTABLE_FIELDHTTP 400 / 409Un modèle de ce nom existe déjà, est déjà chez Meta (synchronisez-le), ne peut pas être modifié dans son état actuel, ou vous changez un champ que Meta fige (nom, langue, catégorie d'un modèle approuvé) — créez-en un nouveau.
INVALID_LANGUAGEINVALID_META_TEMPLATEINVALID_AUTH_TEMPLATEMISSING_SAMPLE_VALUESMISSING_HEADER_EXAMPLEINVALID_HEADER_EXAMPLEHEADER_EXAMPLE_INVALID_SIZEHEADER_EXAMPLE_INVALID_TYPEHEADER_EXAMPLE_UNREACHABLEHTTP 400Le modèle ne respecte pas les règles de Meta : langue inconnue, exemples de variables manquants, média d'en-tête absent, injoignable, de mauvais type ou trop lourd, ou modèle d'authentification non conforme. error.message précise.
INVALID_WABAWHATSAPP_TOKEN_EXPIREDMETA_API_ERRORMETA_SUBMIT_FAILEDMETA_UPLOAD_FAILEDMETA_NOT_CONFIGUREDHTTP 400 / 502 / 503Meta a refusé l'opération sur votre compte WhatsApp Business : waba_id inconnu du compte, jeton de connexion expiré (reconnectez le compte dans le tableau de bord) ou erreur Meta. Réessayez ; si cela persiste, contactez le support.
TEMPLATE_NOT_APPROVEDMISSING_TEMPLATEMISSING_TEMPLATE_PARAMSHTTP 400Campagne ou conversation WhatsApp : le modèle n'est pas approuvé, est absent, ou des variables manquent. MISSING_TEMPLATE_PARAMS liste ce qu'il faut fournir dans template_variables (_header_media_url, _button_<i>, ou "static:valeur").
CONSENT_ATTESTATION_REQUIREDHTTP 400Une campagne WhatsApp marketing exige "consent_attested": true : vous confirmez que chaque contact de la liste a accepté de recevoir vos messages WhatsApp. Envoyez-le seulement si c'est vrai.
INVALID_WABA_SENDERMISSING_LISTEMPTY_LISTLIST_IN_USEHTTP 400Campagne : l'expéditeur WhatsApp n'est pas connecté à votre compte, la liste manque ou n'a aucun destinataire éligible, ou la liste est utilisée par une campagne active (et ne peut pas être supprimée).
ALREADY_RUNNINGCAMPAIGN_RUNNINGCAMPAIGN_NOT_EDITABLENOT_PAUSEDNOT_RUNNINGHTTP 400 / 409L'état de la campagne ne permet pas cette action : seule une campagne brouillon ou programmée se modifie, une campagne en cours se met en pause avant suppression.
Vérification (OTP)
INVALID_CODEHTTP 400Code erroné. error.message indique les essais restants ; laissez l'utilisateur réessayer.
MAX_ATTEMPTSHTTP 429Trop d'essais : la session est close. Démarrez-en une nouvelle avec /v1/verify/send.
SESSION_NOT_FOUNDSESSION_EXPIREDINVALID_STATEINVALID_SESSION_DATAHTTP 400 / 404 / 409Session inconnue, expirée, déjà vérifiée ou annulée, ou créée par une clé d'un autre mode (sandbox/live). Démarrez une nouvelle vérification, avec la même clé pour send, resend et check.
OTP_COOLDOWNOTP_RATE_LIMITHTTP 429Un code vient d'être envoyé à ce numéro (30 s en SMS, 60 s sur WhatsApp), ou trop de codes y sont partis dans l'heure. Attendez Retry-After ; proposez « Renvoyer » via /v1/verify/resend.
OTP_COUNTRY_PAUSEDHTTP 429Codes vers ce pays (hors Maroc) suspendus pour votre compte jusqu'à 00:00 UTC : trop peu des codes du jour y ont été confirmés, signe de trafic automatisé. Retry-After donne les secondes restantes.
COUNTRY_NOT_ALLOWEDHTTP 403Les codes vers ce pays ne sont pas ouverts pour votre compte ou votre application Verify. Contactez le support pour l'ouvrir.
WHATSAPP_UNAVAILABLEDELIVERY_FAILEDINVALID_CODE_TYPEBAD_GATEWAYHTTP 400 / 502 / 503Le code n'a pas pu partir sur WhatsApp (indisponible pour le compte, refusé, ou code non numérique). Rien n'a été débité ; renvoyez avec "channel": "sms".
LOOKUP_FAILEDLOOKUP_UNAVAILABLEHTTP 500 / 503La recherche d'opérateur (/v1/verify/lookup) a échoué ou est indisponible. Réessayez plus tard ; rien n'a été débité.
Débit et idempotence
RATE_LIMITEDTOO_MANY_REQUESTSRATE_LIMIT_EXCEEDEDHTTP 429Limite de requêtes par minute de la clé dépassée. Attendez Retry-After (secondes) ; réessayer avant ne fait que consommer la fenêtre suivante.
INVALID_IDEMPOTENCY_KEYHTTP 400Idempotency-Key dépasse 255 caractères. Utilisez un UUID ou un identifiant métier court.
IDEMPOTENCY_IN_FLIGHTHTTP 409La requête d'origine portant cette clé est encore en cours. Attendez Retry-After et réessayez avec la même clé.
IDEMPOTENCY_KEY_REUSEDHTTP 422Cette Idempotency-Key a déjà servi pour un corps de requête différent. Une clé désigne une seule requête : générez-en une nouvelle.
REPLAY_UNAVAILABLEHTTP 422Cette livraison de webhook n'a pas de contenu conservé à renvoyer. Elle ne peut pas être rejouée.
Compte et facturation
LIMIT_EXCEEDEDHTTP 403Nombre maximal de clés API ou de webhooks atteint pour votre plan. Révoquez-en un ou changez de plan.
INVALID_AMOUNTINVALID_PACKINVALID_PAYMENT_METHODUNSUPPORTED_METHODALREADY_COMPLETEDRECEIPT_NOT_AVAILABLEINVOICE_NOT_AVAILABLESTRIPE_INVOICES_UNAVAILABLENO_STRIPE_CUSTOMERHTTP 400 / 404 / 409Recharge : montant, pack ou moyen de paiement non valide, recharge déjà terminée, ou facture pas (encore) disponible pour cette transaction.
STRIPE_ERRORPORTAL_ERRORSTRIPE_NOT_CONFIGUREDPAYMENT_METHOD_NOT_CONFIGUREDNOT_AVAILABLEINVOICE_TEMPORARILY_UNAVAILABLEINVALID_SIGNATUREHTTP 401 / 500 / 502 / 503La page de paiement ou de facture n'a pas pu être obtenue de notre côté. Rien n'a été débité ; réessayez dans un instant.
Côté serveur
INTERNAL_ERRORDB_ERRORSERVICE_UNAVAILABLEBAD_REQUESTHTTP 400 / 500 / 503Erreur de notre côté. Réessayez avec un délai croissant (et la même Idempotency-Key pour un envoi) ; contactez le support si cela persiste.
Codes d'échec de remise
Un message accepté (202) peut échouer ensuite. Son error_code — dans GET /v1/messages/{id} et le webhook message.failed — prend l'une de ces valeurs. Un message WhatsApp refusé avant d'être parti est remboursé automatiquement.
META_WINDOW_EXPIRED24 h écoulées depuis le dernier message du contact. Envoyez un modèle approuvé ou un SMS.
META_UNDELIVERABLELe numéro n'est pas joignable sur WhatsApp. Pas de nouvel essai automatique : passez par SMS.
META_MARKETING_LIMITLimite de messages marketing par destinataire fixée par WhatsApp. Ne renvoyez pas tout de suite.
META_RATE_LIMITED / META_PAIR_RATE_LIMITEDDébit du numéro dépassé, ou trop de messages vers ce contact. Échec après reprises automatiques ; ralentissez.
META_POLICY_BLOCKED / META_ACCOUNT_LOCKED / META_PAYMENT_ISSUE / META_PHONE_NOT_REGISTEREDLe numéro ou le compte WhatsApp émetteur est restreint, verrouillé, sans moyen de paiement Meta valide, ou non enregistré. Vérifiez WhatsApp Manager ; vos fonds EnvoiSMS ne sont pas en cause.
META_TEMPLATE_NOT_FOUND / META_TEMPLATE_PARAM_MISMATCH / META_TEMPLATE_PARAM_FORMATModèle absent dans cette langue ou pas encore approuvé, mauvais nombre de variables, ou variable au mauvais format (média d'en-tête, bouton).
META_UNSUPPORTED_TYPECe type de message n'est pas pris en charge par WhatsApp pour ce destinataire.
META_GENERIC_ERROR / META_SERVICE_UNAVAILABLEErreur temporaire de WhatsApp, persistante après reprises automatiques. Renvoyez dans quelques minutes.
CASCADE_TIMEOUTCascade : pas d'accusé de remise dans cascade_timeout ; le message est passé au canal suivant (événement message.fallback).
Limites
Limites#
Par clé et par minute. Valeur par défaut d'une nouvelle clé selon le plan : Starter 60, Business 180, Pro 500, Enterprise 1 200. Au-delà : 429 RATE_LIMITED avec Retry-After.
Jusqu'à 10 000 messages par appel à POST /v1/messages/bulk.
Code de 4 à 8 chiffres (6 par défaut), valable 60 s à 30 min (10 min par défaut). Renvoi possible après 30 s en SMS, 60 s sur WhatsApp.
Idempotency-Key de 255 caractères au plus, conservée 24 h par compte.
SDK
Bibliothèques clientes#
Optionnelles : chaque exemple de cette page fonctionne en HTTP simple, sans dépendance.
npm install envoismspip install envoismscomposer require envoisms/envoisms-phpcomposer require envoisms/laravel-otpgo get github.com/n4ouri/envoisms-gonpx -y @envoisms/mcp-serverEnvoyer un message
Met en file un SMS ou un message WhatsApp vers un destinataire et débite son prix. Répond 202 avec l’identifiant du message ; le résultat de remise arrive ensuite par webhook ou via GET /v1/messages/:id.
| Champ | Type | Statut | Description |
|---|---|---|---|
| to | string | Requis | Numéro du destinataire au format E.164 (+212612345678). |
| message | string | Requis | Texte du message, 1 600 caractères au plus (au-delà, il est tronqué). Alias : body. Facultatif sur WhatsApp quand la requête porte un template, un média, une réaction, des contacts, une localisation ou un message interactif. |
| sender_id | string | Optionnel | SMS : votre Sender ID approuvé (lettres, chiffres et espaces, 11 caractères au plus ; un en-tête générique comme INFO ou le nom d’une banque est refusé). WhatsApp : le numéro connecté qui envoie. Alias : from, sender. |
| channel | string | Optionnel | "sms" (défaut) ou "whatsapp". "whatsapp" envoie depuis votre propre numéro WhatsApp Business connecté (sinon 403 WHATSAPP_NOT_CONNECTED). Hors template, un message WhatsApp n’est accepté que si le contact vous a écrit dans les dernières 24 h, sinon 400 OUT_OF_24H_WINDOW sans débit. |
| cascade | boolean | string[] | Optionnel | true : tente WhatsApp puis bascule sur SMS — immédiatement si WhatsApp refuse ou signale l’échec, ou si aucun accusé de réception n’arrive dans cascade_timeout. Accepte aussi une liste ordonnée, par exemple ["whatsapp", "sms"]. Seul l’envoi réellement parti est facturé ; un WhatsApp resté sans accusé est parti et est facturé avec son SMS de repli, puis remboursé si WhatsApp signale ensuite son échec. |
| cascade_timeout | integer | Optionnel | Avec cascade : secondes d’attente d’un accusé de réception avant le canal suivant, de 30 à 43200. Défaut : 120. Donnez à votre template WhatsApp une durée de vie (TTL) qui ne dépasse pas cette valeur, pour qu’un téléphone qui revient en ligne ne reçoive pas les deux messages. |
| template | object | Optionnel | (WhatsApp) Template approuvé de votre compte : {"name", "language", "components"?}. Les valeurs des variables du corps vont dans metadata.variables. Seul type de message autorisé hors fenêtre de 24 h ; tarifé selon la catégorie du template. |
| metadata | object | Optionnel | Paires clé-valeur conservées avec le message et renvoyées dans les webhooks (pas dans GET /v1/messages). Clés lues par la plateforme : variables (valeurs du template), media_url et media_type (en-tête média du template), purpose: "otp" (voir les notes). |
| buttons | array | Optionnel | (WhatsApp) Jusqu’à 3 boutons {"type", "text"} ; text de 25 caractères au plus. type "copy" exige copy_code, "url" exige url (http ou https), "call" exige phone_number. |
| interactive | object | Optionnel | (WhatsApp) Message interactif au format WhatsApp : liste ("list"), boutons de réponse ("button", 1 à 3), formulaire ("flow"), bouton lien ("cta_url") ou demande de localisation ("location_request_message"). |
| image | video | audio | document | sticker | object | Optionnel | (WhatsApp) Média par "id" (média déjà téléversé) ou par "link" (URL https), jamais les deux. "caption" pour image, vidéo et document ; "filename" pour document. |
| reaction | object | Optionnel | (WhatsApp) {"message_id": wamid, "emoji": "👍"} — réagit à un message de la conversation ; un emoji vide retire la réaction. Non facturé. |
| contacts | array | Optionnel | (WhatsApp) Fiches contact (20 au plus) : name.formatted_name obligatoire ; phones, emails, urls, addresses, org, birthday facultatifs. |
| location | object | Optionnel | (WhatsApp) Épingle de localisation : {"latitude", "longitude", "name"?, "address"?}. |
| context | object | Optionnel | (WhatsApp) {"message_id": wamid} — répond en citant un message précédent. Valable sur tous les types sauf reaction. |
| phone_number_id | string | Optionnel | (WhatsApp) Identifiant Meta du numéro connecté qui envoie ; par défaut votre numéro par défaut. |
- Permission de clé API requise : send.
- Idempotence : envoyez un en-tête Idempotency-Key (255 caractères au plus) pour rejouer une requête sans double envoi. Pendant 24 h, la même clé avec le même corps renvoie la réponse d’origine (en-tête Idempotency-Replayed: true) ; avec un autre corps, 422 IDEMPOTENCY_KEY_REUSED ; pendant que la première requête est en cours, 409 IDEMPOTENCY_IN_FLIGHT.
- Clé sandbox (env_test_…) : rien n’est envoyé ni débité ; cost vaut 0, et la réponse ajoute "sandbox": true, "simulated_cost" (le prix qu’aurait payé une clé live) et, le cas échéant, "warnings" (ce qu’une clé live aurait refusé, par exemple une fenêtre de 24 h fermée).
- Codes à usage unique : /v1/verify/send gère tout le cycle et reste la voie recommandée. Si vous générez vos codes vous-même et les envoyez ici, ajoutez metadata: {"purpose": "otp"} : en cas d’échec de remise confirmé par le réseau vers un numéro marocain alors que le code a moins de 10 minutes, la plateforme le renvoie une fois par une autre route SMS, avec le même identifiant de message et sans coût supplémentaire.
- Erreurs fréquentes : 400 INVALID_PHONE, 400 OUT_OF_24H_WINDOW, 400 SENDER_ID_GENERIC, 402 INSUFFICIENT_BALANCE, 403 WHATSAPP_NOT_CONNECTED, 403 SENDER_ID_PROTECTED_BRAND. Aucune requête refusée n’est débitée.
- Mise en forme : les sauts de ligne (\n) passent sur tous les canaux. Le SMS est du texte brut ; WhatsApp interprète *gras*, _italique_ et ~barré~.
curl -X POST "https://api.envoisms.ma/v1/messages" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+212612345678",
"message": "Your order #1042 has shipped."
}'{
"id": "msg_3f9a1c7e5b2d4a6f8e0c1b3d",
"to": "+212612345678",
"channel": "sms",
"cascade": false,
"status": "queued",
"cost": { "eur": 0.072, "mad": 0.792 },
"segments": 1,
"created_at": "2026-10-22T09:14:03.512Z"
}Envoi groupé
Met en file jusqu’à 10 000 messages en un appel, chacun avec son destinataire et son texte. Répond 202 avec un batch_id, les messages acceptés et, le cas échéant, ceux refusés.
| Champ | Type | Statut | Description |
|---|---|---|---|
| messages | array | Requis | Tableau de 1 à 10 000 objets : "to" (E.164), "message" (ou "body"), et facultativement "metadata" et "buttons" (WhatsApp). |
| sender_id | string | Optionnel | Expéditeur commun à tout le lot (mêmes règles que POST /v1/messages). Alias : from, sender. |
| channel | string | Optionnel | "sms" (défaut) ou "whatsapp". En WhatsApp, l’envoi groupé ne porte pas de template : chaque destinataire doit être dans sa fenêtre de 24 h. |
- Permission de clé API requise : send.
- Le lot est validé en entier avant tout envoi : une seule entrée invalide fait échouer la requête en 400 INVALID_MESSAGE, avec la liste des 20 premières entrées fautives (index et règle). Le solde est vérifié pour le lot entier (402 INSUFFICIENT_BALANCE).
- "rejected" liste les entrées refusées une à une (désinscrit, fenêtre de 24 h fermée…) avec leur code ; elles ne sont pas débitées. Il n’apparaît que s’il y en a. "estimated_cost" est le prix estimé du lot avant refus.
- Idempotency-Key accepté, avec les mêmes règles que POST /v1/messages.
curl -X POST "https://api.envoisms.ma/v1/messages/bulk" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"messages": [
{
"to": "+212611111111",
"message": "Your appointment is tomorrow at 10:00."
},
{
"to": "+212622222222",
"message": "Your appointment is tomorrow at 11:30."
}
]
}'{
"batch_id": "batch_9a3c5e7f1b2d4c6e8a0f2b4d",
"total": 2,
"channel": "sms",
"estimated_cost": { "eur": 0.144, "mad": 1.584 },
"messages": [
{ "id": "msg_1a2b3c4d5e6f7a8b9c0d1e2f", "to": "+212611111111", "status": "queued" },
{ "id": "msg_3c4d5e6f7a8b9c0d1e2f3a4b", "to": "+212622222222", "status": "queued" }
],
"rejected": [
{ "to": "+212633333333", "code": "OPTED_OUT", "message": "This number opted out of messages from your account" }
]
}Indicateur de saisie WhatsApp
Marque comme lu un message WhatsApp reçu et affiche « en train d’écrire » à son auteur pendant que vous préparez la réponse (jusqu’à votre réponse ou environ 25 s). Rien n’est stocké ni facturé.
| Champ | Type | Statut | Description |
|---|---|---|---|
| message_id | string | Requis | Identifiant WhatsApp (wamid) du message reçu auquel vous répondez. |
| typing_indicator | boolean | Optionnel | false n’envoie que l’accusé de lecture. Défaut : true. |
| phone_number_id | string | Optionnel | Numéro connecté qui a reçu le message ; par défaut votre numéro par défaut. |
- Permission de clé API requise : send.
- Avec une clé sandbox, rien n’est transmis à WhatsApp et la réponse est {"status": "simulated", "sandbox": true, "message_id", "typing_indicator"}.
curl -X POST "https://api.envoisms.ma/v1/messages/typing" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message_id": "wamid.HBgMMjEyNjEyMzQ1Njc4FQIAEhg",
"typing_indicator": true
}'{
"status": "ok",
"message_id": "wamid.HBgLMjEyNjEyMzQ1Njc4FQIAEhgg",
"typing_indicator": true
}Lister les messages
Renvoie les messages du compte, du plus récent au plus ancien, avec filtres et pagination. format=csv exporte tout le jeu filtré.
| Paramètre | Type | Statut | Description |
|---|---|---|---|
| limit | integer | Optionnel | Nombre de résultats : 50 par défaut, 200 au plus. |
| offset | integer | Optionnel | Nombre de résultats à sauter (pagination). Défaut : 0. |
| status | string | Optionnel | queued, sent, delivered, read (WhatsApp), failed, undeliverable ou unconfirmed. |
| channel | string | Optionnel | sms ou whatsapp. |
| campaign_id | string | Optionnel | Messages d’une campagne (cmp_…). |
| from_date | string | Optionnel | Créés à partir de cette date (UTC). Écrivez-la AAAA-MM-JJ : elle est comparée à created_at, au format "AAAA-MM-JJ HH:MM:SS". |
| to_date | string | Optionnel | Créés jusqu’à cette date (UTC), même comparaison. Pour inclure toute une journée, passez le lendemain. |
| format | string | Optionnel | csv : télécharge tout le jeu filtré (jusqu’au plafond d’export, signalé dans le fichier) au lieu d’une page. |
- Permission de clé API requise : status.
- Une clé ne voit que les messages de son propre mode : une clé sandbox ne liste que des envois simulés, une clé live que des envois réels.
curl -X GET "https://api.envoisms.ma/v1/messages?limit=50" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "msg_3f9a1c7e5b2d4a6f8e0c1b3d",
"campaign_id": null,
"to": "+212612345678",
"channel": "sms",
"body": "Votre commande 4512 est expédiée.",
"sender_id": "MABOUTIQUE",
"unicode": 0,
"segments": 1,
"status": "delivered",
"error_code": null,
"error_message": null,
"cost_eur": 0.072,
"cost_mad": 0.792,
"scheduled_at": null,
"sent_at": "2026-10-22 09:14:05",
"delivered_at": "2026-10-22 09:14:09",
"read_at": null,
"failed_at": null,
"created_at": "2026-10-22 09:14:03",
"operator": "Maroc Telecom",
"sandbox": 0
}
],
"limit": 50,
"offset": 0,
"total": 1
}Consulter un message
Renvoie un message et son état de remise. 404 NOT_FOUND si l’identifiant n’appartient pas au compte ou au mode de la clé.
- Permission de clé API requise : status.
- Statuts : queued, sent, delivered, read (WhatsApp), failed, undeliverable, unconfirmed. "unconfirmed" signifie que l’opérateur n’a ni confirmé ni infirmé la remise ; un accusé arrivé plus tard peut encore le remplacer.
- Le champ metadata fourni à l’envoi n’est pas renvoyé ici ; il accompagne les webhooks.
curl -X GET "https://api.envoisms.ma/v1/messages/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "msg_3f9a1c7e5b2d4a6f8e0c1b3d",
"campaign_id": null,
"to": "+212612345678",
"channel": "sms",
"body": "Votre commande 4512 est expédiée.",
"sender_id": "MABOUTIQUE",
"unicode": 0,
"segments": 1,
"status": "delivered",
"error_code": null,
"error_message": null,
"cost_eur": 0.072,
"cost_mad": 0.792,
"scheduled_at": null,
"sent_at": "2026-10-22 09:14:05",
"delivered_at": "2026-10-22 09:14:09",
"read_at": null,
"failed_at": null,
"created_at": "2026-10-22 09:14:03",
"operator": "Maroc Telecom",
"sandbox": 0
}Lister les templates
Renvoie les templates actifs du compte (SMS et WhatsApp), du plus récent au plus ancien, quel que soit leur statut.
- Permission de clé API requise : send.
- "variables" est une chaîne JSON qui décrit la structure du template : variables, valeurs d’exemple, en-tête, pied de page, boutons, carrousel et revue.
- Statuts WhatsApp : pending_admin (revue EnvoiSMS), pending_meta, approved, rejected, paused, disabled, in_appeal, flagged, deleted_on_meta. Un template SMS est approved dès sa création.
curl -X GET "https://api.envoisms.ma/v1/templates" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "tpl_4c1e9a7b3d5f2e8a6c0b1d3f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"name": "order_shipped",
"channel": "whatsapp",
"body": "Bonjour {{1}}, votre commande {{2}} est expédiée.",
"variables": "{\"variables\":[\"1\",\"2\"],\"parameter_format\":\"positional\",\"sample_values\":{\"1\":\"Amine\",\"2\":\"4512\"},\"header_type\":\"none\",\"header_text\":null,\"footer\":null,\"buttons\":[]}",
"category": "utility",
"active": 1,
"created_at": "2026-10-20 14:02:11",
"status": "approved",
"rejection_reason": null,
"meta_template_id": "1203948576102938",
"language": "fr",
"quality_score": "GREEN",
"quality_updated_at": "2026-10-21 08:00:00",
"waba_id": null
}
]
}Créer un template
Crée un template SMS (utilisable aussitôt) ou WhatsApp (revu par EnvoiSMS, puis soumis à Meta sur votre propre compte WhatsApp Business). Répond 201 avec la ligne créée.
| Champ | Type | Statut | Description |
|---|---|---|---|
| name | string | Requis | Nom du template. Mis en minuscules ; tout caractère hors a-z, 0-9 et _ devient _. |
| channel | string | Requis | sms, whatsapp, telegram ou any. |
| body | string | Requis | Texte du template. WhatsApp : 1 024 caractères au plus ; variables toutes numérotées à partir de {{1}} et consécutives, ou toutes nommées en minuscules ({{prenom}}) ; jamais en début ou en fin de texte, jamais collées. Non requis pour un template WhatsApp "authentication", dont Meta fixe le texte. |
| category | string | Optionnel | marketing (défaut), utility ou authentication. otp vaut authentication, transactional vaut utility. Meta peut réattribuer la catégorie à la revue. |
| language | string | Optionnel | (WhatsApp) Code langue Meta, par exemple fr, ar, en_US, es. Défaut : fr. |
| sample_values | object | string[] | Optionnel | (WhatsApp) Une valeur d’exemple par variable, exigée par Meta : {"1": "Amine"} ou ["Amine"]. Sans elle, 400 MISSING_SAMPLE_VALUES. |
| variables | string[] | Optionnel | (SMS) Noms des variables du texte, conservés tels quels. |
| header_type | string | Optionnel | (WhatsApp) none (défaut), text, image, video ou document. Un en-tête text prend header_text (60 caractères, une variable au plus, avec header_sample) ; un en-tête média exige header_example_url. |
| header_text | header_sample | header_example_url | string | Optionnel | (WhatsApp) Texte de l’en-tête ; valeur d’exemple de sa variable ; lien https public vers un fichier d’exemple pour un en-tête image, vidéo ou document (400 MISSING_HEADER_EXAMPLE s’il manque). |
| footer | string | Optionnel | (WhatsApp) Pied de page, 60 caractères au plus. |
| buttons | array | Optionnel | (WhatsApp) Jusqu’à 10 boutons {"type", "text"} (25 caractères). Types : quick_reply ; url (avec url http(s), et url_example si le lien se termine par une variable) ; phone_number (avec phone_number) ; copy_code (avec copy_code) ; flow (avec flow_id, l’identifiant d’un de vos WhatsApp Flows publiés, et navigate_screen, "LEAD" par défaut). |
| carousel | object | Optionnel | (WhatsApp, marketing ou utility) {"header_type": "image" | "video", "cards": [...]} : 2 à 10 cartes, chacune {"header_example_url" (https), "body" (160 caractères, sans variable), "buttons" (1 ou 2, quick_reply ou url sans variable)}. Toutes les cartes portent les mêmes boutons. Un template carrousel n’a ni en-tête, ni pied de page, ni boutons propres. |
| add_security_recommendation | code_expiration_minutes | otp_type | supported_apps | mixed | Optionnel | (WhatsApp, authentication) Mention de sécurité de Meta (booléen) ; validité du code, 1 à 90 minutes ; COPY_CODE (défaut), ONE_TAP ou ZERO_TAP ; supported_apps [{"package_name", "signature_hash" (11 caractères)}], exigé pour ONE_TAP et ZERO_TAP. ZERO_TAP exige aussi zero_tap_terms_accepted: true. |
| waba_id | string | Optionnel | (WhatsApp) Compte WhatsApp Business visé quand le compte en a plusieurs ; doit être l’un de vos comptes connectés (sinon 400 INVALID_WABA). |
- Permission de clé API requise : send.
- Revue : un template WhatsApp est créé en pending_admin. Des contrôles automatiques (usurpation d’une banque, lien d’hameçonnage, hébergeur de liens jetables, compte inactif) passent aussitôt ; s’ils ne trouvent rien, le template est soumis à Meta (pending_meta) et la réponse le montre déjà. Sinon, il reste en pending_admin pour une revue humaine. Seule Meta l’approuve (approved) et fixe sa catégorie finale.
- Erreurs : 400 MISSING_FIELD, INVALID_CHANNEL, INVALID_LANGUAGE, INVALID_META_TEMPLATE, MISSING_SAMPLE_VALUES, MISSING_HEADER_EXAMPLE, INVALID_AUTH_TEMPLATE, INVALID_WABA ; 409 TEMPLATE_EXISTS (même nom et même langue déjà actifs ; un template rejeté du même nom est remplacé).
curl -X POST "https://api.envoisms.ma/v1/templates" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "order_shipped",
"channel": "whatsapp",
"category": "utility",
"language": "fr",
"body": "Bonjour {{1}}, votre commande {{2}} est expédiée.",
"sample_values": [
"Karim",
"#1042"
]
}'{
"id": "tpl_8e2a4c6b1d3f5a7c9e0b2d4f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"name": "catalogue_automne",
"channel": "whatsapp",
"body": "Découvrez notre sélection d’automne, {{1}} !",
"variables": "{\"header_type\":\"none\",\"header_text\":null,\"footer\":null,\"buttons\":[],\"carousel\":{\"header_type\":\"image\",\"cards\":[{\"header_example_url\":\"https://example.ma/veste.jpg\",\"body\":\"Veste en laine\",\"buttons\":[{\"type\":\"url\",\"text\":\"Voir\",\"url\":\"https://example.ma/veste\"}]},{\"header_example_url\":\"https://example.ma/pull.jpg\",\"body\":\"Pull en coton\",\"buttons\":[{\"type\":\"url\",\"text\":\"Voir\",\"url\":\"https://example.ma/pull\"}]}]},\"variables\":[\"1\"],\"parameter_format\":\"positional\",\"sample_values\":{\"1\":\"Amine\"}}",
"category": "marketing",
"active": 1,
"created_at": "2026-10-22 09:20:41",
"status": "pending_meta",
"rejection_reason": null,
"meta_template_id": "1203948576102944",
"language": "fr",
"quality_score": null,
"quality_updated_at": null,
"waba_id": null
}Consulter un template
Renvoie un template actif du compte, avec son statut et, s’il a été rejeté, rejection_reason. 404 NOT_FOUND sinon.
- Permission de clé API requise : send.
curl -X GET "https://api.envoisms.ma/v1/templates/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "tpl_4c1e9a7b3d5f2e8a6c0b1d3f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"name": "order_shipped",
"channel": "whatsapp",
"body": "Bonjour {{1}}, votre commande {{2}} est expédiée.",
"variables": "{\"variables\":[\"1\",\"2\"],\"parameter_format\":\"positional\",\"sample_values\":{\"1\":\"Amine\",\"2\":\"4512\"},\"header_type\":\"none\",\"header_text\":null,\"footer\":null,\"buttons\":[]}",
"category": "utility",
"active": 1,
"created_at": "2026-10-20 14:02:11",
"status": "approved",
"rejection_reason": null,
"meta_template_id": "1203948576102938",
"language": "fr",
"quality_score": "GREEN",
"quality_updated_at": "2026-10-21 08:00:00",
"waba_id": null
}Modifier un template
Modifie un template avec les mêmes champs que la création ; seuls les champs envoyés changent. Répond avec la ligne à jour.
| Champ | Type | Statut | Description |
|---|---|---|---|
| name | body | category | language | sample_values | header_* | footer | buttons | carousel | mixed | Optionnel | Mêmes champs et mêmes règles que POST /v1/templates. Un template SMS n’accepte que name, body, category et variables. |
- Permission de clé API requise : send.
- WhatsApp, template pas encore chez Meta : la modification le renvoie en revue (pending_admin, puis contrôles automatiques). Template déjà chez Meta : la modification est soumise à Meta et le repasse en pending_meta ; le nom et la langue ne changent plus (400 TEMPLATE_IMMUTABLE_FIELD), pas plus que la catégorie d’un template approuvé ; aucune modification pendant pending_meta, in_appeal ou disabled (409 TEMPLATE_EDIT_NOT_ALLOWED).
curl -X PATCH "https://api.envoisms.ma/v1/templates/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "tpl_4c1e9a7b3d5f2e8a6c0b1d3f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"name": "order_shipped",
"channel": "whatsapp",
"body": "Bonjour {{1}}, votre commande {{2}} est en route.",
"variables": "{\"variables\":[\"1\",\"2\"],\"parameter_format\":\"positional\",\"sample_values\":{\"1\":\"Amine\",\"2\":\"4512\"},\"header_type\":\"none\",\"header_text\":null,\"footer\":null,\"buttons\":[]}",
"category": "utility",
"active": 1,
"created_at": "2026-10-20 14:02:11",
"status": "pending_meta",
"rejection_reason": null,
"meta_template_id": "1203948576102938",
"language": "fr",
"quality_score": "GREEN",
"quality_updated_at": "2026-10-21 08:00:00",
"waba_id": null
}Supprimer un template
Désactive le template. Un template WhatsApp déjà chez Meta y est aussi supprimé ; meta_deleted dit si Meta l’a fait, meta_note pourquoi pas.
- Permission de clé API requise : send.
curl -X DELETE "https://api.envoisms.ma/v1/templates/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"deleted": true,
"id": "tpl_4c1e9a7b3d5f2e8a6c0b1d3f",
"meta_deleted": true
}Soumettre un template à Meta
Soumet à Meta un template WhatsApp déjà validé par la revue EnvoiSMS mais absent de Meta (supprimé par Meta, par exemple). Ce n’est pas un moyen de contourner la revue.
- Permission de clé API requise : send.
- Refus : 409 TEMPLATE_PENDING_REVIEW (le template attend la revue EnvoiSMS ; il partira chez Meta une fois validé), 409 TEMPLATE_REJECTED_BY_REVIEW (rejeté par la revue ; modifiez-le pour le renvoyer en revue), 409 TEMPLATE_ALREADY_ON_META, 400 INVALID_CHANNEL (template non WhatsApp).
curl -X POST "https://api.envoisms.ma/v1/templates/{id}/submit-meta" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"ok": true,
"template": {
"id": "tpl_4c1e9a7b3d5f2e8a6c0b1d3f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"name": "order_shipped",
"channel": "whatsapp",
"body": "Bonjour {{1}}, votre commande {{2}} est expédiée.",
"variables": "{\"variables\":[\"1\",\"2\"],\"parameter_format\":\"positional\",\"sample_values\":{\"1\":\"Amine\",\"2\":\"4512\"},\"header_type\":\"none\",\"header_text\":null,\"footer\":null,\"buttons\":[]}",
"category": "utility",
"active": 1,
"created_at": "2026-10-20 14:02:11",
"status": "pending_meta",
"rejection_reason": null,
"meta_template_id": "1203948576102951",
"language": "fr",
"quality_score": null,
"quality_updated_at": null,
"waba_id": null
},
"meta": { "id": "1203948576102951", "status": "PENDING", "category": "UTILITY" }
}Synchroniser les templates depuis Meta
Importe les templates de vos comptes WhatsApp Business (statut, catégorie, qualité). Un template que Meta ne détient plus passe en deleted_on_meta, sauf si la synchronisation est incomplète (complete: false).
- Permission de clé API requise : send.
curl -X POST "https://api.envoisms.ma/v1/templates/sync" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"ok": true,
"wabas": ["104857392017465"],
"fetched": 12,
"updated": 9,
"inserted": 3,
"deduped": 0,
"marked_deleted": 1,
"complete": true,
"errors": []
}Lister les Sender IDs
Renvoie vos demandes de Sender ID SMS et leur statut, ainsi qu’un nom suggéré d’après votre profil.
- Permission de clé API requise : sender_id.
curl -X GET "https://api.envoisms.ma/v1/sender-ids" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "sid_6a2c8e4b0d1f3a5c7e9b2d4f",
"sender_id": "MABOUTIQUE",
"status": "approved",
"rc_url": "/v1/sender-ids/rc/acct_7b2e4c6a8d0f1e3b5c7a9d2f/rc_1f3a5c7e9b2d4f6a8c0e2b4d.pdf",
"note": null,
"requested_at": "2026-10-02 10:15:00",
"reviewed_at": "2026-10-09 16:40:00",
"company_website": "https://maboutique.ma",
"sample_content": "Votre commande 4512 est expédiée.",
"message_type": "transactional",
"estimated_volume": "5000/mois",
"operator_emailed_at": "2026-10-02 10:20:00"
}
],
"suggestedSenderId": "MABOUTIQUE"
}Demander un Sender ID
Dépose une demande de Sender ID SMS, accompagnée du registre de commerce en PDF. Corps en multipart/form-data. Répond 201 (200 si la demande existante est mise à jour) avec le statut pending.
| Champ | Type | Statut | Description |
|---|---|---|---|
| sender_id | string | Requis | Nom demandé : lettres sans accent, chiffres et espaces, 11 caractères au plus, au moins une lettre. Un en-tête générique (INFO, ALERT…) ou le nom d’une institution protégée est refusé. |
| file | file | Requis | Registre de commerce, PDF de 5 Mo au plus. |
| message_type | string | Optionnel | transactional (défaut) ou promotional. |
| company_website | sample_content | estimated_volume | string | Optionnel | Site de l’entreprise, exemple de message et volume estimé, transmis aux opérateurs avec la demande. |
- Permission de clé API requise : sender_id.
- Erreurs : 400 INVALID_FORM_DATA, MISSING_FIELD, INVALID_FIELD, SENDER_ID_GENERIC, INVALID_FILE_TYPE, FILE_TOO_LARGE ; 403 SENDER_ID_PROTECTED_BRAND ; 409 SENDER_ID_PENDING ou SENDER_ID_APPROVED.
curl -X POST "https://api.envoisms.ma/v1/sender-ids" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "sid_6a2c8e4b0d1f3a5c7e9b2d4f",
"sender_id": "MABOUTIQUE",
"status": "pending",
"rc_url": "/v1/sender-ids/rc/acct_7b2e4c6a8d0f1e3b5c7a9d2f/rc_1f3a5c7e9b2d4f6a8c0e2b4d.pdf"
}Connexion WhatsApp Business
Renvoie l’état de vos numéros WhatsApp Business connectés et la liste de contrôle du numéro par défaut (readiness) : ce qui l’empêche d’envoyer, selon Meta. Lecture seule ; aucun secret n’est renvoyé.
- Toute clé API valide : aucune permission particulière n’est exigée.
- readiness : can_send vaut AVAILABLE, LIMITED, BLOCKED ou unknown (Meta n’a pas répondu) ; blockers liste les raisons de Meta (entity, reason, resolution) ; payment_method vaut missing quand Meta signale l’absence de moyen de paiement, unchecked sinon ; business_verification est le statut de vérification de l’entreprise chez Meta. readiness vaut null sans numéro connecté. Ce que Meta n’a pas dit est renvoyé null, jamais comme une valeur saine par défaut.
- Connecter, déconnecter ou reconfigurer un numéro, et toute écriture sous /v1/whatsapp (profil, réglages, outils), se fait depuis une session du tableau de bord : une clé API reçoit 403 FORBIDDEN. Seuls POST /v1/whatsapp/lookup-phone et POST /v1/whatsapp/media/upload restent ouverts aux clés.
curl -X GET "https://api.envoisms.ma/v1/whatsapp/waba-connection" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"readiness": {
"waba_id": "104857392017465",
"payment_method": "unchecked",
"can_send": "AVAILABLE",
"blockers": [],
"business_verification": "verified",
"approved_templates": 4,
"checked_at": "2026-10-22T08:00:12.000Z"
},
"connected": true,
"mode": "custom_waba",
"gateway_number": "+212 708-789095",
"waba_id": "104857392017465",
"phone_number_id": "109283746501928",
"phone_number": "+212522000000",
"name": "Ma Boutique",
"status": "connected",
"error": null,
"quality_rating": "GREEN",
"messaging_limit_tier": "TIER_1K",
"connected_at": "2026-09-30 11:02:44",
"meta_app_id": "1029384756102938",
"meta_config_id": "2039485761029384",
"preverified_phone_ids": [],
"connections": [
{
"id": "5f1c9e3a7b2d4f6a8c0e1b3d5f7a9c2e",
"waba_id": "104857392017465",
"phone_number_id": "109283746501928",
"phone_number": "+212522000000",
"verified_name": "Ma Boutique",
"status": "connected",
"is_default": true,
"webhook_url": null,
"settings": {},
"created_at": "2026-09-30 11:02:44",
"updated_at": "2026-10-20 08:15:02"
}
]
}Profil WhatsApp Business
Renvoie le profil public d’un de vos numéros tel que Meta le détient : à propos, adresse, description, e-mail, photo, sites web, secteur.
| Paramètre | Type | Statut | Description |
|---|---|---|---|
| phone_number_id | string | Optionnel | Numéro connecté visé ; par défaut votre numéro par défaut. |
- Toute clé API valide : aucune permission particulière n’est exigée.
- La mise à jour du profil se fait depuis le tableau de bord ; avec une clé API, POST /v1/whatsapp/profile répond 403 FORBIDDEN.
curl -X GET "https://api.envoisms.ma/v1/whatsapp/profile" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"ok": true,
"phone_number_id": "109283746501928",
"profile": {
"about": "Service client",
"address": "Casablanca, Maroc",
"description": "Prêt-à-porter, livraison partout au Maroc.",
"email": "contact@maboutique.ma",
"profile_picture_url": "https://pps.whatsapp.net/v/t61/...",
"websites": ["https://maboutique.ma"],
"vertical": "APPAREL"
}
}Envoyer un code de vérification
Génère un code à usage unique, l’envoie et ouvre une session de vérification. Vous ne stockez aucun code : validez la saisie de l’utilisateur avec POST /v1/verify/check. Répond 202.
| Champ | Type | Statut | Description |
|---|---|---|---|
| to | string | Requis | Destinataire au format E.164. |
| channel | string | Optionnel | "sms" (défaut) ou "whatsapp". En WhatsApp, le code part comme template d’authentification — depuis votre propre numéro si vous en avez connecté un et qu’il a un template d’authentification approuvé, sinon depuis le numéro EnvoiSMS — et passe en SMS si Meta refuse l’envoi. |
| app_id | string | Optionnel | Application Verify (vra_…) créée dans le tableau de bord : elle fixe longueur, validité, nombre d’essais, canaux et cascade, et remplace code_length et expiry. |
| code_length | integer | Optionnel | Longueur du code, de 4 à 8 chiffres. Défaut : 6. |
| expiry | integer | Optionnel | Validité du code en secondes, de 60 à 1800. Défaut : 600. Sur WhatsApp, plafonnée à 600. |
| brand | string | Optionnel | (SMS) Nom affiché dans le message, 32 caractères au plus. Défaut : EnvoiSMS. Un nom générique est remplacé par ENVOISMS ; le nom d’une institution protégée est refusé (403 SENDER_ID_PROTECTED_BRAND). |
| template | string | Optionnel | (SMS) Texte personnalisé avec {{code}} et {{brand}}. |
| otp_button_text | string | Optionnel | (WhatsApp) Libellé du bouton de copie, 25 caractères au plus. |
| otp_type | package_name | signature_hash | string | Optionnel | (WhatsApp) copy_code (défaut), one_tap, zero_tap ou url ; pour one_tap et zero_tap, le nom du paquet Android et le hash de signature de l’application qui reçoit le code. |
| web_otp_domain | app_hash | string | Optionnel | (SMS) Domaine pour le remplissage automatique WebOTP (ajoute « @domaine #code » au SMS) ; hash de 11 caractères de l’application Android pour l’API SMS Retriever. |
| auto_cascade | boolean | Optionnel | Envoie aussi le SMS de repli quand le délai du premier canal expire sans remise. Remplace le réglage de l’application Verify. |
- Permission de clé API requise : verify.
- Sur WhatsApp, la réponse porte "channel": "whatsapp", "resend_after_seconds": 60, "fallback_channels": ["sms", "whatsapp"] et "cost" ({eur, mad}), sans message_id. Avec une clé sandbox, rien n’est envoyé ni débité et la réponse ajoute "sandbox": true, "simulated": true et "sandbox_code", le code à soumettre à /v1/verify/check.
- Limites par numéro : un envoi toutes les 30 s en SMS et 60 s sur WhatsApp (429 OTP_COOLDOWN, avec Retry-After), 5 par heure et 10 par jour (429 OTP_RATE_LIMIT) ; 200 envois par jour et par compte.
- Remplissage automatique : sur votre site, ajoutez autocomplete="one-time-code" et inputmode="numeric" au champ de saisie du code.
curl -X POST "https://api.envoisms.ma/v1/verify/send" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "+212612345678",
"brand": "MyApp"
}'{
"session_id": "vrf_7e2a9c4b1d3f5a7c9e0b2d4f",
"message_id": "msg_2b4d6f8a0c1e3b5d7f9a1c3e",
"to": "+212612345678",
"channel": "sms",
"expires_at": "2026-10-22T09:24:03.000Z",
"resend_after_seconds": 30,
"fallback_channels": [],
"status": "sent"
}Renvoyer le code
Renvoie le code d’une session encore ouverte, par SMS ou WhatsApp, une fois la temporisation écoulée. Le code ne change pas. Répond 200.
| Champ | Type | Statut | Description |
|---|---|---|---|
| session_id | string | Requis | Session renvoyée par /v1/verify/send (vrf_…). |
| channel | string | Optionnel | "sms" (défaut) ou "whatsapp". |
- Permission de clé API requise : verify.
- La temporisation court depuis le dernier envoi effectif : 30 s après un SMS, 60 s après un WhatsApp. GET /v1/verify/:id donne le décompte (resend_available_in_seconds) et le 429 OTP_COOLDOWN porte Retry-After. La fenêtre est propre à votre compte et au numéro.
- Erreurs : 404 SESSION_NOT_FOUND (inconnue, expirée, ou d’un autre mode de clé), 400 SESSION_EXPIRED, 400 INVALID_STATE (session déjà vérifiée ou close), 400 UNSUPPORTED_CHANNEL, 429 OTP_COOLDOWN. Avec une clé sandbox, la réponse ajoute "sandbox": true, "simulated": true et "sandbox_code".
curl -X POST "https://api.envoisms.ma/v1/verify/resend" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"session_id": "vrf_…",
"channel": "sms"
}'{
"session_id": "vrf_7e2a9c4b1d3f5a7c9e0b2d4f",
"message_id": "msg_8c0e2b4d6f1a3c5e7b9d1f3a",
"to": "+212612345678",
"channel": "sms",
"status": "sent",
"resend_after_seconds": 30,
"expires_at": "2026-10-22T09:24:03.000Z"
}Vérifier un code
Compare le code saisi par l’utilisateur à celui de la session. Un code juste clôt la session : un second contrôle répond 404.
| Champ | Type | Statut | Description |
|---|---|---|---|
| session_id | string | Requis | Session renvoyée par /v1/verify/send. |
| code | string | Requis | Le code saisi par l’utilisateur. |
- Permission de clé API requise : status.
- Erreurs : 400 INVALID_CODE (le message indique les essais restants), 429 MAX_ATTEMPTS (la session passe en failed), 404 SESSION_NOT_FOUND — y compris pour une session expirée ou déjà vérifiée. /v1/verify ne renvoie jamais EXPIRED_CODE.
curl -X POST "https://api.envoisms.ma/v1/verify/check" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"session_id": "vrf_…",
"code": "483921"
}'{
"session_id": "vrf_7e2a9c4b1d3f5a7c9e0b2d4f",
"verified": true,
"verified_at": "2026-10-22T09:15:12.408Z"
}Annuler une session
Clôt une session en attente : le code envoyé ne sera plus accepté. Également disponible en POST /v1/verify/:id/void.
| Champ | Type | Statut | Description |
|---|---|---|---|
| session_id | string | Requis | Session à annuler (absent si l’identifiant est dans le chemin). |
- Permission de clé API requise : status.
- Erreurs : 404 SESSION_NOT_FOUND, 409 INVALID_STATE (session déjà vérifiée, échouée ou expirée). Si l’application Verify a une callback_url, elle est notifiée (expired).
curl -X POST "https://api.envoisms.ma/v1/verify/void" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"session_id": "vrf_…"
}'{
"session_id": "vrf_7e2a9c4b1d3f5a7c9e0b2d4f",
"status": "expired",
"voided": true
}Consulter une session
Renvoie l’état d’une session de vérification : statut, essais, décompte avant renvoi et résultat de remise de chaque envoi.
- Permission de clé API requise : status.
- status vaut pending, verified, failed ou expired. 404 NOT_FOUND si la session n’appartient pas au compte.
curl -X GET "https://api.envoisms.ma/v1/verify/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "vrf_7e2a9c4b1d3f5a7c9e0b2d4f",
"session_id": "vrf_7e2a9c4b1d3f5a7c9e0b2d4f",
"to": "+212612345678",
"channel": "sms",
"status": "pending",
"attempts_used": 1,
"attempts_remaining": 2,
"max_attempts": 3,
"resend_available_in_seconds": 0,
"can_resend_now": true,
"expires_at": "2026-10-22T09:24:03.000Z",
"verified_at": null,
"created_at": "2026-10-22 09:14:03",
"app_id": null,
"app_name": null,
"attempted_channels": [],
"deliveries": [
{
"channel": "sms",
"kind": "send",
"status": "delivered",
"simulated": false,
"submitted_at": "2026-10-22 09:14:03",
"delivered_at": "2026-10-22 09:14:07",
"read_at": null,
"failed_at": null,
"error_code": null,
"error_message": null
}
],
"callback_delivered": null,
"cascade_triggered": false
}Identifier l’opérateur d’un numéro
Valide un numéro et renvoie son pays et, pour un mobile marocain, l’opérateur qui le dessert, portabilité comprise. Facturé à chaque consultation réussie ; un numéro invalide (400) n’est pas facturé.
| Paramètre | Type | Statut | Description |
|---|---|---|---|
| number | string | Optionnel | Alternative au paramètre de chemin : GET /v1/lookup?number=%2B212612345678. |
- Permission de clé API requise : send.
- Erreurs : 400 MISSING_FIELD, 400 INVALID_PHONE, 402 INSUFFICIENT_BALANCE. Avec une clé sandbox, la réponse est réelle mais rien n’est débité, et elle porte "sandbox": true. type vaut mobile ou unknown ; operator vaut null quand il est inconnu.
curl -X GET "https://api.envoisms.ma/v1/lookup/{number}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "lkp_5d7f9b1c3e5a7c9e1b3d5f7a",
"number": "+212612345678",
"valid": true,
"country_iso": "MA",
"type": "mobile",
"operator": "Maroc Telecom",
"ported": false,
"cost": { "eur": 0.005, "mad": 0.055 }
}Valider le format d’un numéro
Valide le format d’un numéro et renvoie ses formes locale et internationale, son pays, sa localisation, l’opérateur d’attribution et le type de ligne. Non facturé.
| Champ | Type | Statut | Description |
|---|---|---|---|
| number | string | Requis | Numéro, au format local ou international. |
| country_code | string | Optionnel | Code pays ISO à 2 lettres (MA, FR…), recommandé pour un numéro au format local. |
- Permission de clé API requise : status.
- Le carrier est l’opérateur d’attribution de la tranche, pas forcément celui qui dessert un numéro porté : pour l’opérateur réel, utilisez GET /v1/lookup/:number. Erreurs : 400 MISSING_FIELD ; 503 LOOKUP_UNAVAILABLE quand le service est momentanément indisponible.
curl -X POST "https://api.envoisms.ma/v1/verify/lookup" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"number": "+212612345678"
}'{
"valid": true,
"number": "212612345678",
"local_format": "0612345678",
"international_format": "+212612345678",
"country_prefix": "+212",
"country_code": "MA",
"country_name": "Morocco",
"location": null,
"carrier": "Maroc Telecom",
"line_type": "mobile"
}Lister les conversations
Renvoie les fils de discussion WhatsApp et SMS du compte, avec l’état de la fenêtre de 24 h de chacun et des compteurs globaux.
| Paramètre | Type | Statut | Description |
|---|---|---|---|
| limit | integer | Optionnel | Nombre de résultats : 50 par défaut, 100 au plus. |
| offset | integer | Optionnel | Nombre de résultats à sauter (pagination). Défaut : 0. |
| filter | string | Optionnel | all (défaut), unread, active_window ou needs_human. |
| channel | string | Optionnel | all (défaut), whatsapp ou sms. |
| phone_number_id | assigned_to | search | string | Optionnel | Numéro WhatsApp connecté ; agent assigné ; recherche dans le numéro, le nom et le dernier message. |
- Permission de clé API requise : send.
curl -X GET "https://api.envoisms.ma/v1/conversations?limit=50" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "th_212612345678",
"phone": "212612345678",
"contact_name": "Yassine Alami",
"channel": "whatsapp",
"lead_status": "new",
"unread_count": 1,
"bot_muted": false,
"handling_agent": null,
"notes": null,
"tags": [],
"last_inbound_at": "2026-10-22 08:42:00",
"last_outbound_at": "2026-10-21 17:10:00",
"last_message_preview": "Bonjour, la veste existe en taille L ?",
"last_message_role": "customer",
"last_message_at": "2026-10-22 08:42:00",
"is_window_active": true,
"window_expires_at": "2026-10-23T08:42:00.000Z",
"window_seconds_remaining": 84960,
"waba_name": "Ma Boutique",
"waba_phone": "+212522000000",
"phone_number_id": "109283746501928"
}
],
"total": 1,
"limit": 50,
"offset": 0,
"stats": {
"total_threads": 1,
"unread_threads": 1,
"active_window_threads": 1,
"human_takeover_threads": 0
}
}Consulter une conversation
Renvoie un fil et ses messages (500 au plus), du plus ancien au plus récent, avec l’état de la fenêtre de 24 h.
- Permission de clé API requise : send.
- :phone accepte le numéro avec ou sans +. role vaut customer pour un message reçu, agent ou bot pour un message envoyé.
curl -X GET "https://api.envoisms.ma/v1/conversations/{phone}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"thread": {
"id": "th_212612345678",
"phone": "212612345678",
"contact_name": "Yassine Alami",
"channel": "whatsapp",
"lead_status": "new",
"unread_count": 0,
"bot_muted": true,
"handling_agent": "Salma",
"notes": null,
"tags": [],
"last_inbound_at": "2026-10-22 08:42:00",
"last_outbound_at": "2026-10-22 08:45:10",
"is_window_active": true,
"window_expires_at": "2026-10-23T08:42:00.000Z",
"window_seconds_remaining": 84960,
"window": {
"is_window_active": true,
"window_expires_at": "2026-10-23T08:42:00.000Z",
"window_seconds_remaining": 84960
},
"waba_name": "Ma Boutique",
"waba_phone": "+212522000000",
"phone_number_id": "109283746501928"
},
"messages": [
{
"id": "wamsg_1c3e5a7c9e1b3d5f7a9c1e3b",
"phone": "212612345678",
"role": "customer",
"message": "Bonjour, la veste existe en taille L ?",
"wamid": "wamid.HBgLMjEyNjEyMzQ1Njc4FQIAEhgg",
"status": "read",
"media_type": "text",
"media_url": null,
"metadata": {},
"read_at": "2026-10-22 08:43:00",
"created_at": "2026-10-22 08:42:00"
}
]
}Répondre dans une conversation
Envoie une réponse d’agent (texte, média ou template) et met le bot en pause sur ce fil. L’envoi passe par le même circuit que POST /v1/messages : tarification, liste de désinscription et débit s’appliquent. Répond 201.
| Champ | Type | Statut | Description |
|---|---|---|---|
| message | string | Optionnel | Texte de la réponse, ou légende du média. Obligatoire sans template ni média (400 MISSING_MESSAGE). |
| channel | string | Optionnel | "whatsapp" (défaut) ou "sms". |
| template_id | template_name | template_language | variables | mixed | Optionnel | Template WhatsApp approuvé à envoyer à la place d’un texte libre (par identifiant ou par nom et langue), avec ses valeurs (tableau ou objet). |
| media_url | media_type | filename | string | Optionnel | Média joint : URL https (ou data URI), type image ou document, nom de fichier du document. |
| phone_number_id | string | Optionnel | Numéro WhatsApp connecté qui répond. |
- Permission de clé API requise : send.
- Un message WhatsApp libre exige que le contact ait écrit dans les dernières 24 h (sinon 400 OUT_OF_24H_WINDOW) ; un template approuvé part à tout moment. Autres erreurs : 403 WHATSAPP_NOT_CONNECTED, 404 TEMPLATE_NOT_FOUND, 400 TEMPLATE_NOT_APPROVED.
curl -X POST "https://api.envoisms.ma/v1/conversations/{phone}/messages" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"message": "Thanks, we will call you back today."
}'{
"ok": true,
"message": {
"id": "msg_9e1b3d5f7a9c1e3b5d7f9a1c",
"phone": "212612345678",
"role": "agent",
"message": "Oui, la taille L est disponible.",
"wamid": "wamid.HBgLMjEyNjEyMzQ1Njc4FQIAERgS",
"status": "sent",
"channel": "whatsapp",
"media_type": "text",
"metadata": { "agent": "Salma" },
"created_at": "2026-10-22T08:45:10.221Z"
}
}Lister les leads qualifiés
Renvoie les leads enregistrés par votre bot WhatsApp, du plus récent au plus ancien, avec leur score (0 à 100) et leur statut. format=csv télécharge la même liste.
| Paramètre | Type | Statut | Description |
|---|---|---|---|
| limit | integer | Optionnel | Nombre de résultats : 100 par défaut, 500 au plus. |
| status | string | Optionnel | new, contacted, qualified, Fournisseur, won ou lost. |
| preset | string | Optionnel | Secteur du bot (par exemple medical_equipment) ; supplier regroupe les demandes de fournisseurs. |
| search | string | Optionnel | Recherche dans le numéro, le nom, la catégorie et les notes. |
| format | string | Optionnel | json (défaut) ou csv. |
- Toute clé API valide : aucune permission particulière n’est exigée.
- Pas de pagination par offset : limit borne la liste. metrics est calculé sur les leads renvoyés ; hot compte ceux dont le score atteint 80.
curl -X GET "https://api.envoisms.ma/v1/qualified-leads?limit=50" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"ok": true,
"leads": [
{
"id": "lead_3a5c7e9b1d3f5a7c9e1b3d5f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"phone_number_id": "109283746501928",
"contact_phone": "+212612345678",
"contact_name": "Cabinet Benjelloun",
"preset": "medical_equipment",
"category": "Échographe",
"bant_score": 85,
"urgency": "immediate",
"budget": "200 000 - 300 000 MAD",
"authority": "decision_maker",
"notes": null,
"source_type": "direct_inbound",
"status": "new",
"created_at": "2026-10-21 09:15:00",
"updated_at": "2026-10-21 09:15:00"
}
],
"metrics": { "total": 1, "hot": 1, "average_score": 85 }
}Mettre à jour un lead
Change le statut, les notes ou le nom d’un lead. Répond avec le lead à jour.
| Champ | Type | Statut | Description |
|---|---|---|---|
| status | string | Optionnel | new, contacted, qualified, Fournisseur, won ou lost (sinon 400 INVALID_STATUS). |
| notes | string | Optionnel | Notes internes de suivi. |
| contact_name | string | Optionnel | Nom du contact ou de l’établissement. |
| value_minor | currency | integer | string | Optionnel | Valeur de l’affaire en centimes et sa devise, transmises à Meta avec un passage à won. |
- Toute clé API valide : aucune permission particulière n’est exigée.
- Le passage à qualified ou à won est signalé à Meta (QualifiedLead, Purchase) quand le lead est venu d’une publicité qui renvoie vers WhatsApp ; seulement lors d’un vrai changement de statut. 404 LEAD_NOT_FOUND si le lead n’appartient pas au compte.
curl -X PATCH "https://api.envoisms.ma/v1/qualified-leads/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"status": "contacted",
"notes": "Called back on 22/10"
}'{
"ok": true,
"lead": {
"id": "lead_3a5c7e9b1d3f5a7c9e1b3d5f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"phone_number_id": "109283746501928",
"contact_phone": "+212612345678",
"contact_name": "Cabinet Benjelloun",
"preset": "medical_equipment",
"category": "Échographe",
"bant_score": 85,
"urgency": "immediate",
"budget": "200 000 - 300 000 MAD",
"authority": "decision_maker",
"notes": "Rendez-vous fixé jeudi.",
"source_type": "direct_inbound",
"status": "contacted",
"created_at": "2026-10-21 09:15:00",
"updated_at": "2026-10-22 10:02:31",
"user_id": null
}
}Lister les contacts
Renvoie les contacts du compte, avec recherche et filtre par liste.
| Paramètre | Type | Statut | Description |
|---|---|---|---|
| limit | integer | Optionnel | Nombre de résultats : 100 par défaut, 500 au plus. |
| offset | integer | Optionnel | Nombre de résultats à sauter (pagination). Défaut : 0. |
| q | string | Optionnel | Recherche dans le nom, le téléphone et l’e-mail. |
| list_id | string | Optionnel | Seulement les membres de cette liste (lst_…). |
- Permission de clé API requise : contacts.
curl -X GET "https://api.envoisms.ma/v1/contacts?limit=50" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "ctc_2d4f6a8c0e1b3d5f7a9c1e3b",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"phone": "+212612345678",
"name": "Karim Bennani",
"email": "karim@example.ma",
"custom1": "VIP",
"custom2": null,
"custom3": null,
"opted_out": 0,
"opted_out_at": null,
"created_at": "2026-10-10 14:20:00"
}
],
"limit": 100,
"offset": 0,
"total": 1
}Créer ou mettre à jour un contact
Crée un contact, ou met à jour celui qui a déjà ce numéro (nom, e-mail et champs personnalisés sont remplacés). Répond 201 avec le contact.
| Champ | Type | Statut | Description |
|---|---|---|---|
| phone | string | Requis | Numéro au format E.164 (400 INVALID_PHONE sinon). |
| name | email | string | Optionnel | Nom et e-mail du contact. |
| list_id | string | Optionnel | Ajoute aussi le contact à cette liste (404 LIST_NOT_FOUND si elle n’est pas à vous). |
| custom_fields | object | Optionnel | {"custom1", "custom2", "custom3"} : trois champs libres, utilisables dans les campagnes. |
- Permission de clé API requise : contacts.
curl -X POST "https://api.envoisms.ma/v1/contacts" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+212612345678",
"name": "Karim Bennani",
"email": "karim@example.ma"
}'{
"id": "ctc_2d4f6a8c0e1b3d5f7a9c1e3b",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"phone": "+212612345678",
"name": "Karim Bennani",
"email": "karim@example.ma",
"custom1": "VIP",
"custom2": null,
"custom3": null,
"opted_out": 0,
"opted_out_at": null,
"created_at": "2026-10-10 14:20:00"
}Importer des contacts
Met en file un import de contacts, en JSON ou en fichier CSV, et répond 202 aussitôt ; l’import s’exécute ensuite par lots de 100.
- Permission de clé API requise : contacts.
- La réponse est donnée à la mise en file : total compte les lignes reçues, et imported le reprend tel quel. Les numéros invalides sont écartés pendant l’import ; vérifiez le résultat avec GET /v1/contacts. Autres types de contenu : 415 UNSUPPORTED_MEDIA_TYPE.
curl -X POST "https://api.envoisms.ma/v1/contacts/import" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"contacts": [
{
"phone": "+212611111111",
"name": "Karim"
},
{
"phone": "+212622222222",
"name": "Salma"
}
]
}'{
"jobId": "job_4b6d8f0a2c4e6b8d0f2a4c6e",
"total": 150,
"imported": 150,
"skipped": 0,
"status": "queued"
}Supprimer un contact
Supprime définitivement un contact. 404 NOT_FOUND s’il n’existe pas.
- Permission de clé API requise : contacts.
curl -X DELETE "https://api.envoisms.ma/v1/contacts/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"deleted": true,
"id": "ctc_2d4f6a8c0e1b3d5f7a9c1e3b"
}Lister les listes de contacts
Renvoie les listes de contacts du compte, avec leur nombre de membres.
- Permission de clé API requise : contacts.
curl -X GET "https://api.envoisms.ma/v1/contacts/lists" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "lst_f84b2d6a8c0e1f3b5d7a9c2e",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"name": "Clients fidèles",
"description": "Clients ayant commandé au moins deux fois",
"count": 1420,
"created_at": "2026-09-15 09:00:00"
}
]
}Créer une liste
Crée une liste de contacts vide, à remplir par POST /v1/contacts ou /v1/contacts/import. Répond 201.
| Champ | Type | Statut | Description |
|---|---|---|---|
| name | string | Requis | Nom de la liste. |
| description | string | Optionnel | Description libre. |
- Permission de clé API requise : contacts.
curl -X POST "https://api.envoisms.ma/v1/contacts/lists" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Newsletter",
"description": "Customers who subscribed at checkout"
}'{
"id": "lst_f84b2d6a8c0e1f3b5d7a9c2e",
"name": "Clients fidèles",
"description": "Clients ayant commandé au moins deux fois",
"count": 0
}Renommer une liste
Remplace le nom et la description d’une liste. 404 NOT_FOUND si elle n’existe pas.
| Champ | Type | Statut | Description |
|---|---|---|---|
| name | string | Requis | Nouveau nom. |
| description | string | Optionnel | Nouvelle description ; absente, elle est effacée. |
- Permission de clé API requise : contacts.
curl -X POST "https://api.envoisms.ma/v1/contacts/lists/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "lst_f84b2d6a8c0e1f3b5d7a9c2e",
"name": "Clients fidèles 2026",
"description": null
}Supprimer une liste
Supprime une liste et ses appartenances ; les contacts eux-mêmes sont conservés. Refusé (400 LIST_IN_USE) tant qu’une campagne programmée, en cours ou en pause l’utilise.
- Permission de clé API requise : contacts.
curl -X DELETE "https://api.envoisms.ma/v1/contacts/lists/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"deleted": true,
"id": "lst_f84b2d6a8c0e1f3b5d7a9c2e"
}Lister les désinscriptions
Renvoie les numéros désinscrits de vos envois. Aucun message ne part vers eux (403 OPTED_OUT).
- Permission de clé API requise : optouts.
curl -X GET "https://api.envoisms.ma/v1/optouts?limit=50" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{ "phone": "+212611111111", "opted_out_at": "2026-10-01 12:00:00" }
],
"limit": 100,
"offset": 0,
"total": 1
}Ajouter une désinscription
Ajoute un numéro à votre liste de désinscription. Répond 201.
| Champ | Type | Statut | Description |
|---|---|---|---|
| phone | string | Requis | Numéro au format E.164. |
- Permission de clé API requise : optouts.
curl -X POST "https://api.envoisms.ma/v1/optouts" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+212612345678"
}'{
"phone": "+212611111111",
"opted_out": true
}Retirer une désinscription
Retire un numéro de la liste de désinscription. Ne le faites que sur un nouvel accord de la personne, à consigner avec POST /v1/consents.
- Permission de clé API requise : optouts.
curl -X DELETE "https://api.envoisms.ma/v1/optouts/{phone}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"phone": "+212611111111",
"opted_out": false
}Lister les consentements
Registre de consentements en ajout seul (loi 09-08) : chaque accord et chaque retrait est un événement daté, avec sa source et sa preuve. La plateforme y inscrit elle-même les mots-clés STOP/START reçus et la première conversation ouverte par le client.
| Paramètre | Type | Statut | Description |
|---|---|---|---|
| limit | integer | Optionnel | Nombre de résultats : 100 par défaut, 500 au plus. |
| offset | integer | Optionnel | Nombre de résultats à sauter (pagination). Défaut : 0. |
| phone | string | Optionnel | Numéro E.164. |
| purpose | string | Optionnel | marketing, transactional, otp ou service. |
| status | string | Optionnel | granted ou withdrawn. |
| from | to | string | Optionnel | Bornes ISO 8601 sur recorded_at. |
| format | string | Optionnel | csv : télécharge tout le jeu filtré. |
- Permission de clé API requise : contacts.
curl -X GET "https://api.envoisms.ma/v1/consents?limit=50" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "cns_9f2a4c6e8b0d1f3a5c7e9b2d",
"phone": "+212612345678",
"channel": "whatsapp",
"purpose": "marketing",
"status": "withdrawn",
"source": "whatsapp_keyword",
"evidence": { "keyword": "stop", "lang": "fr" },
"recorded_at": "2026-10-05T09:00:00.000Z",
"created_at": "2026-10-05T09:00:00.000Z"
}
],
"limit": 100,
"offset": 0,
"total": 1
}Enregistrer un consentement
Consigne qu’une personne a donné ou retiré son consentement, avec la preuve que vous détenez. Un retrait marketing place aussitôt le numéro sur la liste de désinscription. Répond 201.
| Champ | Type | Statut | Description |
|---|---|---|---|
| phone | string | Requis | Numéro au format E.164. |
| purpose | string | Requis | marketing, transactional, otp ou service. |
| status | string | Optionnel | granted (défaut) ou withdrawn. |
| channel | string | Optionnel | whatsapp, sms ou any (défaut). |
| source | string | Optionnel | api (défaut), form, import ou dashboard. Les sources par mot-clé sont réservées à la plateforme. |
| evidence | object | Optionnel | Objet JSON libre de moins de 4 096 octets : texte présenté, URL, IP, référence. |
| recorded_at | string | Optionnel | Date ISO 8601 de l’acte (défaut : maintenant) ; antidatable pour un import, jamais dans le futur. |
- Permission de clé API requise : contacts.
- Erreurs : 400 INVALID_PHONE, INVALID_PURPOSE, INVALID_STATUS, INVALID_CHANNEL, INVALID_SOURCE, INVALID_EVIDENCE, INVALID_DATE.
curl -X POST "https://api.envoisms.ma/v1/consents" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"phone": "+212612345678",
"purpose": "marketing",
"status": "granted",
"channel": "whatsapp",
"source": "form",
"evidence": {
"form": "checkout",
"ip": "196.200.1.4"
}
}'{
"id": "cns_1b3d5f7a9c2e4b6d8f0a2c4e",
"phone": "+212612345678",
"channel": "any",
"purpose": "marketing",
"status": "granted",
"source": "form",
"evidence": { "text": "J’accepte de recevoir les offres par WhatsApp", "url": "https://example.ma/inscription" },
"recorded_at": "2026-10-01T10:00:00.000Z",
"created_at": "2026-10-01T10:00:00.000Z"
}État et historique d’un numéro
Renvoie l’état courant par finalité (tiré de l’événement le plus récent ; unknown sans historique, jamais présumé accordé), la présence sur la liste de désinscription et l’historique (500 événements au plus).
- Permission de clé API requise : contacts.
curl -X GET "https://api.envoisms.ma/v1/consents/{phone}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"phone": "+212612345678",
"opted_out": true,
"opted_out_at": "2026-10-05T09:00:00.000Z",
"state": {
"marketing": { "status": "withdrawn", "channel": "whatsapp", "source": "whatsapp_keyword", "recorded_at": "2026-10-05T09:00:00.000Z", "record_id": "cns_9f2a4c6e8b0d1f3a5c7e9b2d" },
"transactional": { "status": "unknown" },
"otp": { "status": "unknown" },
"service": { "status": "granted", "channel": "whatsapp", "source": "whatsapp_inbound", "recorded_at": "2026-07-01T09:00:00.000Z", "record_id": "cns_0a2c4e6b8d1f3a5c7e9b0d2f" }
},
"history": []
}Lister les campagnes
Renvoie les campagnes du compte, de la plus récente à la plus ancienne, avec leurs compteurs.
- Permission de clé API requise : campaigns.
- Statuts : draft, scheduled, running, paused, completed, failed, cancelled.
curl -X GET "https://api.envoisms.ma/v1/campaigns?limit=50" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"name": "Soldes d’automne",
"channel": "sms",
"list_id": "lst_f84b2d6a8c0e1f3b5d7a9c2e",
"template_id": null,
"body": "Bonjour {{name}}, -30% sur la collection avec le code AUT30.",
"sender_id": "MABOUTIQUE",
"status": "completed",
"scheduled_at": null,
"started_at": "2026-10-15 10:00:00",
"completed_at": "2026-10-15 10:06:12",
"total_count": 1420,
"sent_count": 1412,
"delivered_count": 1380,
"failed_count": 8,
"cost_eur": 102.24,
"cost_mad": 1124.64,
"created_at": "2026-10-14 16:30:00",
"buttons": null,
"template_variables": null
}
],
"total": 1,
"limit": 50,
"offset": 0
}Créer une campagne
Crée une campagne en brouillon, ou programmée si scheduled_at est fourni. Rien ne part avant POST /v1/campaigns/:id/send. Répond 201.
| Champ | Type | Statut | Description |
|---|---|---|---|
| name | string | Requis | Nom de la campagne. |
| body | string | Optionnel | (SMS) Texte du message. body ou template_id est obligatoire. |
| template_id | string | Optionnel | Template du compte. Obligatoire en WhatsApp, où il doit être approuvé par Meta (400 TEMPLATE_NOT_APPROVED). |
| channel | string | Optionnel | "sms" (défaut) ou "whatsapp" (exige un numéro WhatsApp Business connecté, sinon 403 WHATSAPP_NOT_CONNECTED). |
| list_id | string | Optionnel | Liste destinataire ; nécessaire pour lancer la campagne (404 LIST_NOT_FOUND si elle n’est pas à vous). |
| sender_id | string | Optionnel | SMS : Sender ID (mêmes règles que POST /v1/messages ; défaut EnvoiSMS). WhatsApp : numéro connecté qui envoie (défaut : votre numéro par défaut ; 400 INVALID_WABA_SENDER s’il n’est pas à vous). |
| scheduled_at | string | Optionnel | Date ISO 8601 ; met la campagne en statut scheduled. |
| template_variables | object | Optionnel | (WhatsApp) Valeur de chaque paramètre du template, par clé. Clé "1", "2"… ou nom de variable : un champ du contact (name, phone, email, custom1, custom2, custom3) ou un texte fixe préfixé "static:". "_header_media_url" : lien https du média d’en-tête (obligatoire pour un en-tête image, vidéo ou document). "_button_<i>" : partie variable du bouton lien d’index i (à partir de 0). Sans valeur, {{1}} lit name, {{2}} custom1 et {{3}} custom2. |
| consent_attested | boolean | Optionnel | (WhatsApp, template marketing) true : vous attestez que chaque contact de la liste a accepté de recevoir vos messages WhatsApp. Obligatoire pour un template marketing (sinon 400 CONSENT_ATTESTATION_REQUIRED) ; l’attestation est horodatée. |
| buttons | array | Optionnel | (WhatsApp) Boutons, même format que POST /v1/messages. Refusés en SMS (400 BUTTONS_NOT_SUPPORTED). |
- Permission de clé API requise : campaigns.
- WhatsApp : tout ce que le template exige par contact est vérifié à la création ; ce qui manque est nommé dans 400 MISSING_TEMPLATE_PARAMS. Autres erreurs : 400 MISSING_TEMPLATE, 404 TEMPLATE_NOT_FOUND.
curl -X POST "https://api.envoisms.ma/v1/campaigns" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Summer sale",
"channel": "sms",
"body": "Summer sale: -20% until Sunday. Reply STOP to unsubscribe.",
"list_id": "lst_…"
}'{
"id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f",
"status": "draft"
}Consulter une campagne
Renvoie une campagne, son statut et ses compteurs d’envoi. 404 NOT_FOUND sinon.
- Permission de clé API requise : campaigns.
curl -X GET "https://api.envoisms.ma/v1/campaigns/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"name": "Nouvelle collection",
"channel": "whatsapp",
"list_id": "lst_f84b2d6a8c0e1f3b5d7a9c2e",
"template_id": "tpl_8e2a4c6b1d3f5a7c9e0b2d4f",
"body": null,
"sender_id": "+212522000000",
"status": "running",
"scheduled_at": null,
"started_at": "2026-10-22 10:00:00",
"completed_at": null,
"total_count": 1420,
"sent_count": 840,
"delivered_count": 790,
"failed_count": 3,
"cost_eur": 0,
"cost_mad": 0,
"created_at": "2026-10-21 18:12:40",
"buttons": null,
"template_variables": "{\"1\":\"name\"}"
}Modifier une campagne
Modifie une campagne en brouillon ou programmée ; seuls les champs envoyés changent. Répond avec la campagne à jour.
| Champ | Type | Statut | Description |
|---|---|---|---|
| name | body | sender_id | scheduled_at | string | Optionnel | Nouveau nom, texte, expéditeur ou date de programmation (ISO 8601). Une valeur vide laisse le champ inchangé. |
- Permission de clé API requise : campaigns.
- 409 CAMPAIGN_NOT_EDITABLE si la campagne n’est ni draft ni scheduled ; 404 NOT_FOUND si elle n’existe pas.
curl -X PATCH "https://api.envoisms.ma/v1/campaigns/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"scheduled_at": "2026-11-01T09:00:00Z"
}'{
"id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f",
"account_id": "acct_7b2e4c6a8d0f1e3b5c7a9d2f",
"name": "Soldes d’automne — relance",
"channel": "sms",
"list_id": "lst_f84b2d6a8c0e1f3b5d7a9c2e",
"template_id": null,
"body": "Dernier jour : -30% avec le code AUT30.",
"sender_id": "MABOUTIQUE",
"status": "scheduled",
"scheduled_at": "2026-10-25T09:00:00Z",
"started_at": null,
"completed_at": null,
"total_count": 0,
"sent_count": 0,
"delivered_count": 0,
"failed_count": 0,
"cost_eur": 0,
"cost_mad": 0,
"created_at": "2026-10-22 11:05:00",
"buttons": null,
"template_variables": null
}Annuler une campagne
Passe la campagne en cancelled ; elle reste consultable. Une campagne en cours doit d’abord être mise en pause (409 CAMPAIGN_RUNNING).
- Permission de clé API requise : campaigns.
curl -X DELETE "https://api.envoisms.ma/v1/campaigns/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"deleted": true,
"id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f"
}Lancer une campagne
Lance la campagne vers les membres non désinscrits de sa liste. L’envoi se fait en arrière-plan ; suivez-le avec GET /v1/campaigns/:id. Répond 202.
- Permission de clé API requise : campaigns.
- Erreurs : 400 ALREADY_RUNNING, 400 MISSING_LIST, 400 EMPTY_LIST (aucun destinataire éligible), 400 MISSING_BODY, 404 NOT_FOUND. Chaque message est tarifé et débité comme un envoi unitaire.
curl -X POST "https://api.envoisms.ma/v1/campaigns/{id}/send" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f",
"status": "running",
"total": 1420
}Mettre une campagne en pause
Suspend une campagne en cours. 409 NOT_RUNNING si elle ne l’est pas.
- Permission de clé API requise : campaigns.
curl -X POST "https://api.envoisms.ma/v1/campaigns/{id}/pause" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"paused": true,
"id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f"
}Reprendre une campagne
Reprend une campagne en pause là où elle s’était arrêtée. 409 NOT_PAUSED si elle n’est pas en pause.
- Permission de clé API requise : campaigns.
curl -X POST "https://api.envoisms.ma/v1/campaigns/{id}/resume" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"resumed": true,
"id": "cmp_8d2a4c6e1b3f5a7c9e0b2d4f"
}Lister les webhooks
Renvoie vos webhooks actifs. Le secret n’apparaît que sous forme masquée (secret_hint) ; il n’est montré en entier qu’à la création et à la rotation.
- Permission de clé API requise : webhooks.
curl -X GET "https://api.envoisms.ma/v1/webhooks" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e",
"name": "Webhook Endpoint",
"target_type": "custom",
"format": "standard",
"url": "https://example.ma/hooks/envoisms",
"events": "[\"message.delivered\",\"message.failed\"]",
"active": 1,
"last_triggered_at": "2026-10-22 09:14:09",
"last_status": 200,
"created_at": "2026-10-01 10:00:00",
"secret_hint": "••••9c2e"
}
]
}Créer un webhook
Enregistre une URL HTTPS qui recevra les événements choisis. Répond 201 avec le secret de signature, affiché cette seule fois.
| Champ | Type | Statut | Description |
|---|---|---|---|
| url | string | Requis | URL HTTPS publique (400 INVALID_URL sinon). |
| events | string[] | Optionnel | message.sent, message.delivered, message.read, message.failed, message.undeliverable, message.fallback, message.inbound, message.reaction, contact.optout, contact.optin, lead.qualified ; jokers "message.*" et "*". Défaut : ["message.delivered", "message.failed"]. |
| secret | string | Optionnel | Secret de signature ; généré (sec_…) s’il est absent. |
| name | format | string | Optionnel | Libellé ; format standard ({"event", "data", "timestamp"}) ou flat (champs de data aussi à la racine, pour un tableur). |
- Permission de clé API requise : webhooks.
- Signature : chaque envoi porte X-EnvoiSMS-Signature-256: t=<horodatage unix>,v1=<HMAC-SHA256 hexadécimal du secret sur "t.corps">, et X-EnvoiSMS-Event. Rejetez un horodatage de plus de 5 minutes. Un échec (5xx, 429 ou réseau) est retenté après 60 s, 10 min puis 1 h : 4 tentatives au total.
- Le nombre de webhooks actifs dépend du plan (Starter 2, Business 5, Pro 15, Enterprise 50) ; au-delà, 403 LIMIT_EXCEEDED.
curl -X POST "https://api.envoisms.ma/v1/webhooks" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.ma/webhooks/envoisms",
"events": [
"message.delivered",
"message.failed",
"message.inbound"
]
}'{
"id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e",
"name": "Webhook Endpoint",
"target_type": "custom",
"format": "standard",
"url": "https://example.ma/hooks/envoisms",
"events": ["message.delivered", "message.failed"],
"secret": "sec_3d5f7a9c1e3b5d7f9a1c9c2e",
"active": true
}Modifier un webhook
Modifie l’URL, les événements, le nom, le format ou l’état actif d’un webhook ; seuls les champs envoyés changent.
| Champ | Type | Statut | Description |
|---|---|---|---|
| url | events | name | format | mixed | Optionnel | Mêmes règles qu’à la création. |
| active | boolean | Optionnel | false suspend les envois vers ce webhook. |
- Permission de clé API requise : webhooks.
curl -X PATCH "https://api.envoisms.ma/v1/webhooks/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"active": false
}'{
"updated": true,
"id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e"
}Supprimer un webhook
Désactive le webhook : plus aucun événement ne lui est envoyé et il disparaît de la liste.
- Permission de clé API requise : webhooks.
curl -X DELETE "https://api.envoisms.ma/v1/webhooks/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"deleted": true,
"id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e"
}Tester un webhook
Envoie un événement d’exemple, signé comme un vrai, à l’URL du webhook. Répond 202 avec l’exemple envoyé.
| Champ | Type | Statut | Description |
|---|---|---|---|
| event | string | Optionnel | Nom d’événement à simuler (défaut : lead.qualified). Les données d’exemple sont celles d’un message reçu pour message.inbound, d’un lead qualifié sinon. |
- Permission de clé API requise : webhooks.
curl -X POST "https://api.envoisms.ma/v1/webhooks/{id}/test" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"event": "message.delivered"
}'{
"queued": true,
"id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e",
"event": "message.inbound",
"sample": {
"event": "message.inbound",
"data": {
"message_id": "msg_demo_k3x9f2ab",
"from": "+212612345678",
"to": "+212708789095",
"channel": "whatsapp",
"text": "Bonjour, je souhaite des informations sur vos tarifs et solutions.",
"contact_name": "Karim Bennani",
"timestamp": "2026-10-22T09:30:00.000Z"
},
"timestamp": "2026-10-22T09:30:00.000Z"
}
}Changer le secret d’un webhook
Remplace le secret de signature et renvoie le nouveau, cette seule fois. Les envois déjà en file restent signés avec l’ancien : acceptez les deux pendant quelques minutes.
- Permission de clé API requise : webhooks.
curl -X POST "https://api.envoisms.ma/v1/webhooks/{id}/rotate-secret" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e",
"secret": "sec_7a9c1e3b5d7f9a1c3e5b0d2f",
"rotated": true
}Journal des envois webhook
Renvoie les 100 dernières tentatives d’envoi, de la plus récente à la plus ancienne, avec le corps envoyé et la réponse de votre serveur.
| Paramètre | Type | Statut | Description |
|---|---|---|---|
| webhook_id | string | Optionnel | Seulement les envois de ce webhook. |
- Permission de clé API requise : webhooks.
curl -X GET "https://api.envoisms.ma/v1/webhooks/logs" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "0b6f3c2e-8a4d-4f1b-9c7e-2d5a1f8e6b30",
"webhook_id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e",
"event": "message.delivered",
"url": "https://example.ma/hooks/envoisms",
"payload": "{\"event\":\"message.delivered\",\"data\":{\"id\":\"msg_3f9a1c7e5b2d4a6f8e0c1b3d\",\"to\":\"+212612345678\",\"channel\":\"sms\",\"status\":\"delivered\"},\"timestamp\":\"2026-10-22T09:14:09.120Z\"}",
"status_code": 500,
"error_message": null,
"attempt": 1,
"delivered": 0,
"created_at": "2026-10-22 09:14:09"
}
],
"max_attempts": 4
}Rejouer un envoi webhook
Renvoie un envoi journalisé — même événement, mêmes données, nouvelle signature — à l’URL actuelle du webhook, avec les tentatives habituelles. Répond 202.
- Permission de clé API requise : webhooks.
- 404 NOT_FOUND si le journal est inconnu ou si son webhook a été supprimé ; 422 REPLAY_UNAVAILABLE si le corps envoyé n’a pas été conservé.
curl -X POST "https://api.envoisms.ma/v1/webhooks/logs/{log_id}/replay" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"queued": true,
"replay_of": "0b6f3c2e-8a4d-4f1b-9c7e-2d5a1f8e6b30",
"webhook_id": "whk_5f2b7d9a1c3e5b7d9f1a3c5e",
"event": "message.delivered"
}Lister les webhooks entrants
Renvoie vos webhooks entrants : des URL de capture qui transforment un appel d’un outil tiers (formulaire, Zapier, Make…) en message envoyé.
- Toute clé API valide : aucune permission particulière n’est exigée.
curl -X GET "https://api.envoisms.ma/v1/inbound-webhooks" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "inwh_9a8b7c6d5e4f3a2b1c0d9e8f",
"name": "Formulaire de contact",
"channel": "whatsapp",
"template_id": "tpl_4c1e9a7b3d5f2e8a6c0b1d3f",
"active": 1,
"created_at": "2026-10-12 14:00:00"
}
]
}Créer un webhook entrant
Crée une URL de capture, POST /v1/inbound-webhooks/catch/:id : chaque appel JSON reçu envoie un message au numéro lu dans le corps. Répond 201.
| Champ | Type | Statut | Description |
|---|---|---|---|
| name | string | Requis | Libellé de la source. |
| channel | string | Requis | sms ou whatsapp. |
| mapping_config | object | Requis | {"to_path": "contact.phone", "variables": {"1": "contact.name"}} : chemin du numéro dans le JSON reçu, et chemin de chaque variable du template. |
| template_id | string | Optionnel | Template à envoyer (identifiant ou nom). |
- Toute clé API valide : aucune permission particulière n’est exigée.
- L’URL de capture ne demande pas de clé : son identifiant fait office de secret, ne le publiez pas. Elle accepte 64 Ko au plus et 120 appels par minute, répond 202 {"success": true, "messageId"} et débite votre solde comme un envoi normal. Suppression : DELETE /v1/inbound-webhooks/:id.
curl -X POST "https://api.envoisms.ma/v1/inbound-webhooks" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "Website form",
"channel": "whatsapp",
"mapping_config": {
"to_path": "phone"
}
}'{
"id": "inwh_9a8b7c6d5e4f3a2b1c0d9e8f"
}Consulter le solde
Renvoie le solde prépayé en dirhams et en euros, le plan et sa date d’échéance.
- Permission de clé API requise : balance.
- GET /v1/balance renvoie les quatre premiers champs avec la même permission.
curl -X GET "https://api.envoisms.ma/v1/billing/balance" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"balance_mad": 1492.5,
"balance_eur": 135.68,
"primary_currency": "MAD",
"plan": "business",
"plan_expires_at": "2026-12-31 23:59:59",
"plan_days_left": 70
}Lister les transactions
Renvoie les mouvements du solde, du plus récent au plus ancien : recharges, consommation, remboursements, ajustements et crédits offerts, chacun avec le solde après opération.
- Permission de clé API requise : billing.
curl -X GET "https://api.envoisms.ma/v1/billing/transactions?limit=50" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "tx_6e8a0c2e4b6d8f0a2c4e6b8d",
"type": "usage",
"status": "completed",
"amount_eur": -0.072,
"amount_mad": -0.792,
"balance_after_eur": 135.68,
"balance_after_mad": 1492.5,
"payment_method": null,
"description": "SMS +212612345678",
"reference": "msg_3f9a1c7e5b2d4a6f8e0c1b3d",
"stripe_invoice_id": null,
"created_at": "2026-10-22 09:14:03"
}
],
"limit": 50,
"offset": 0,
"total": 1,
"type": null
}Statut des paiements machine
Indique si un agent sans compte peut payer l’accès à l’API par un dépôt en USDC (HTTP 402, protocole MPP), et à quelles conditions. Sans authentification.
- Désactivé, la réponse est {"enabled": false}.
- Parcours : appelez /v1/messages, /v1/waba, /v1/verify ou /v1/mcp sans en-tête Authorization, avec X-Payment-Method: mpp (ou Accept: application/problem+json) ; la réponse est 402 avec un en-tête WWW-Authenticate: Payment qui décrit le dépôt. Déposez deposit_usd en USDC, puis rejouez l’appel avec Authorization: Payment <credential>. La réponse porte la clé API attribuée dans X-Machine-Session-Key (une seule fois) et le reçu dans Payment-Receipt ; utilisez ensuite cette clé en Bearer.
- Sans X-Payment-Method ni Accept: application/problem+json, un appel non authentifié reçoit 401 UNAUTHORIZED. Un dépôt non encore confirmé donne 402 PAYMENT_NOT_SETTLED ; un credential illisible, 400 INVALID_CREDENTIAL. La clé attribuée n’a que les permissions send, status, balance et verify. 5 défis par minute et par IP au plus.
curl -X GET "https://api.envoisms.ma/v1/machine-payments/status" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"enabled": true,
"deposit_usd": 5,
"credit_mad": 46.5,
"networks": ["base", "tempo"],
"permissions": ["send", "status", "balance", "verify"]
}Statistiques d’envoi
Renvoie, sur une fenêtre de jours, les volumes, la remise confirmée, la dépense débitée et leur répartition par canal, opérateur, statut et jour.
| Paramètre | Type | Statut | Description |
|---|---|---|---|
| days | integer | Optionnel | Taille de la fenêtre, 30 par défaut, 365 au plus. |
| campaign_id | string | Optionnel | Statistiques d’une seule campagne (la réponse omet alors error_diagnostics et waba_breakdown). |
- Permission de clé API requise : analytics.
- delivery_rate se calcule sur les seuls messages dont la remise est confirmée ou infirmée ; coverage_pct dit quelle part des messages a reçu cette confirmation. Une valeur inconnue vaut null, jamais 100. cost_* est ce qui a été débité, remboursements déduits. Réponse mise en cache 60 secondes (en-tête X-Cache).
curl -X GET "https://api.envoisms.ma/v1/analytics" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"window_days": 30,
"campaign_id": null,
"summary": {
"total": 420,
"delivered": 398,
"failed": 6,
"unconfirmed": 16,
"delivery_rate": 98.51,
"coverage_pct": 96.19,
"cost_eur": 30.24,
"cost_mad": 332.64,
"avg_latency": 4.2
},
"channels": [
{ "channel": "sms", "total": 420, "cost_eur": 30.24, "cost_mad": 332.64, "fallback_sends": 0 }
],
"operators": [
{ "operator": "Maroc Telecom", "total": 250, "delivered": 240, "failed": 2, "unconfirmed": 8, "cost_eur": 18, "cost_mad": 198, "delivery_rate": 99.17, "coverage_pct": 96.8, "avg_latency": null }
],
"status_distribution": [
{ "status": "delivered", "total": 398 },
{ "status": "unconfirmed", "total": 16 },
{ "status": "failed", "total": 6 }
],
"daily": [
{ "day": "2026-10-21", "total": 14, "sms_total": 14, "whatsapp_total": 0, "delivered": 13, "failed": 0, "unconfirmed": 1, "cost_eur": 1.008, "cost_mad": 11.088 }
],
"error_diagnostics": [],
"waba_breakdown": []
}Lister les clés API
Renvoie les clés API du compte, actives et révoquées, sans jamais leur valeur complète.
- Permission de clé API requise : api_keys.
- permissions et ip_whitelist sont des chaînes JSON (ip_whitelist vaut null sans restriction).
curl -X GET "https://api.envoisms.ma/v1/api-keys" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"data": [
{
"id": "key_e84c2a6b8d0f1e3c5a7b9d2f",
"name": "Serveur de production",
"key_prefix": "smr_a8f7d6c5",
"ip_whitelist": "[\"196.200.1.4\"]",
"rate_limit": 180,
"permissions": "[\"send\",\"verify\",\"status\",\"balance\"]",
"active": 1,
"last_used_at": "2026-10-22 09:14:03",
"created_at": "2026-10-01 08:00:00",
"sandbox": false
}
]
}Créer une clé API
Crée une clé live (smr_…) ou sandbox (env_test_…). Répond 201 ; la valeur complète (api_key) n’est montrée qu’une fois.
| Champ | Type | Statut | Description |
|---|---|---|---|
| name | string | Optionnel | Libellé de la clé (défaut : "API key"). |
| permissions | string[] | Optionnel | Parmi send, verify, status, balance, contacts, campaigns, webhooks, billing, analytics, api_keys, optouts, sender_id. Défaut : send, verify, status, balance. Les valeurs inconnues sont ignorées. |
| ip_whitelist | string[] | Optionnel | Adresses IP seules autorisées à utiliser la clé. |
| rate_limit | integer | Optionnel | Requêtes par minute, de 10 à 2000. Défaut selon le plan : Starter 60, Business 180, Pro 500, Enterprise 1 200. |
| sandbox | boolean | Optionnel | true crée une clé de test : requêtes validées et enregistrées, rien envoyé, rien facturé. Le mode d’une clé est définitif. |
- Permission de clé API requise : api_keys.
- Le nombre de clés live actives dépend du plan, et les clés sandbox ont leur propre plafond ; au-delà, 403 LIMIT_EXCEEDED. Une clé sandbox ajoute "note" à la réponse.
curl -X POST "https://api.envoisms.ma/v1/api-keys" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "OTP backend",
"permissions": [
"verify",
"status"
],
"sandbox": true
}'{
"id": "key_e84c2a6b8d0f1e3c5a7b9d2f",
"name": "Serveur de production",
"key_prefix": "smr_a8f7d6c5",
"api_key": "smr_a8f7d6c5…",
"permissions": ["send", "verify", "status", "balance"],
"rate_limit": 180,
"sandbox": false,
"warning": "The full API key is shown once. Store it securely."
}Modifier une clé API
Modifie le nom, les permissions, les IP autorisées, le débit ou l’état actif d’une clé.
| Champ | Type | Statut | Description |
|---|---|---|---|
| name | permissions | ip_whitelist | mixed | Optionnel | Mêmes règles qu’à la création. |
| rate_limit | integer | Optionnel | Requêtes par minute, de 10 à 1000. |
| active | boolean | Optionnel | false suspend la clé. |
- Permission de clé API requise : api_keys.
- Le mode sandbox ou live ne se change pas : 400 SANDBOX_IMMUTABLE. Créez une nouvelle clé.
curl -X PATCH "https://api.envoisms.ma/v1/api-keys/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "OTP backend (rotated)"
}'{
"updated": true,
"id": "key_e84c2a6b8d0f1e3c5a7b9d2f"
}Révoquer une clé API
Révoque définitivement une clé : elle n’authentifie plus aucune requête.
- Permission de clé API requise : api_keys.
curl -X DELETE "https://api.envoisms.ma/v1/api-keys/{id}" \
-H "Authorization: Bearer $ENVOISMS_API_KEY"{
"revoked": true,
"id": "key_e84c2a6b8d0f1e3c5a7b9d2f"
}