Retour au site

Guide de l'API

Envoyer des SMS depuis votre logiciel, et savoir ce qu'ils deviennent. Toutes les adresses ci-dessous sont relatives a https://winalco-sms-relay.com.

Demarrage rapide

Trois etapes, environ cinq minutes. Le plan gratuit suffit pour tout tester : il accepte 5 SMS par jour, sans carte bancaire.

  • 1. Prenez une cle. Dans la console, Cles API - Creer. Le secret wak_... n'est affiche qu'une fois.
  • 2. Enrolez un telephone. Sans telephone enrole, vos messages restent en file : c'est une SIM reelle qui les envoie, pas nos serveurs.
  • 3. Appelez POST /api/v1/sms/send. Vous recevez un id ; gardez-le, c'est lui qui sert au suivi.
curl -X POST https://winalco-sms-relay.com/api/v1/sms/send \
  -H "X-Api-Key: wak_votre_cle" \
  -H "Content-Type: application/json" \
  -d '{"to":"0661234567","message":"Bonjour"}'

Le reste de ce guide detaille chaque etape, puis le webhook - la facon recommandee de connaitre le sort de vos messages sans interroger l'API en boucle.

Il n'y a pas de bac a sable separe : un envoi de test part vraiment, vers le numero que vous indiquez. Utilisez votre propre numero pour vos premiers essais.

Authentification

Chaque requete porte l'en-tete X-Api-Key. La cle determine votre compte : elle ne voit et n'agit que sur vos donnees.

Generez-la dans la console : Cles API - Creer. Le secret wak_... n'est affiche qu'une seule fois.

Gardez la cle sur votre serveur. Placee dans une application mobile ou une page web, elle est lisible par n'importe qui - et permet d'envoyer des SMS a vos frais. Si elle fuite, revoquez-la depuis la console : les appels suivants recevront un 401 immediatement.

Envoyer un SMS

POST /api/v1/sms/send
X-Api-Key: wak_...
Content-Type: application/json

{
  "to": "0661234567",
  "message": "Votre code de confirmation est 4821"
}

Reponse 201 Created :

{
  "id": "0f4c9b2e-7a15-4f8c-9e3d-1b2a3c4d5e6f",
  "to": "0661234567",
  "status": "pending",
  "errorCode": null,
  "createdUtc": "2026-07-26T15:30:00Z",
  "updatedUtc": "2026-07-26T15:30:00Z"
}

Le numero est accepte sous toutes ses formes courantes : 0661234567, +213661234567, 00213661234567. Conservez l'id : c'est lui qui sert au suivi.

status vaut pending a la creation : le message est en file, aucun telephone ne l'a encore pris. Ce n'est pas une erreur, c'est l'etat normal des premieres secondes.

Choisir le telephone emetteur

Par defaut, c'est nous qui choisissons la SIM : celle du meme reseau que le destinataire en priorite, puis la moins chargee. Vous n'avez rien a faire, et c'est presque toujours le bon choix.

Le champ optionnel preferredRelayId force un telephone precis - son identifiant est visible dans la console, section Relais. Utile quand un numero doit toujours partir de la meme SIM, par exemple parce que vos clients y repondent.

{
  "to": "0661234567",
  "message": "Votre commande est prete",
  "preferredRelayId": "7d1f0a44-2c6b-4d90-9f31-88bb0f2a1c05"
}
Un telephone prefere hors ligne ne bloque pas l'envoi : le message part d'une autre SIM plutot que d'attendre indefiniment. La preference est un souhait, pas un verrou.

Ne pas envoyer deux fois le meme SMS

Si votre connexion coupe apres que nous ayons accepte l'envoi mais avant que la reponse vous parvienne, vous ne pouvez pas savoir si le SMS est parti. Rejouer l'appel enverrait un second message : le destinataire le recoit deux fois, et vous le payez deux fois.

Ajoutez un en-tete Idempotency-Key avec une valeur unique par envoi - typiquement l'identifiant de la commande ou de la facture concernee, ou un UUID que vous generez. Maximum 200 caracteres.

POST /api/v1/sms/send
X-Api-Key: wak_...
Idempotency-Key: commande-2026-04178
Content-Type: application/json

{ "to": "0661234567", "message": "Votre commande est prete" }
ReponseSens
201 CreatedPremier appel : le SMS a ete mis en file.
200 OKCle deja vue : voici le message d'origine, rien n'a ete renvoye.

Dans les deux cas le corps est identique - meme id, meme etat. Vous pouvez donc rejouer un appel sans risque autant de fois que necessaire.

Utilisez une cle differente pour chaque envoi distinct. Reutiliser la meme cle pour deux SMS reellement differents ferait retourner le premier message sans jamais envoyer le second.

Envoi groupe

Le meme message vers plusieurs numeros, en un appel au lieu d'un par destinataire. Maximum 500 numeros par appel.

POST /api/v1/sms/send-bulk
X-Api-Key: wak_...
Content-Type: application/json

{
  "to": ["0661234567", "0770000001", "0550000002"],
  "message": "Votre commande est prete"
}

Reponse :

{
  "accepted": 2,
  "rejected": 1,
  "results": [
    { "to": "0661234567", "id": "0f4c...", "status": "pending", "message": null, "code": null },
    { "to": "0770000001", "id": "1a2b...", "status": "pending", "message": null, "code": null },
    { "to": "0550000002", "id": null, "status": "rejected",
      "message": "Quota mensuel atteint.", "code": "monthly_quota_exceeded" }
  ]
}

Un lot peut passer en partie : le quota peut etre atteint en cours de route, ou un numero etre invalide. Le code de reponse le dit sans ambiguite :

CodeSens
201Tous les destinataires acceptes.
207Une partie seulement - lisez results.
400Aucun destinataire accepte.

Chaque entree rejetee porte son propre code, le meme vocabulaire que les codes d'erreur : vous distinguez un quota atteint d'un numero invalide, destinataire par destinataire, sans analyser de texte.

Chaque destinataire compte pour un SMS dans votre quota : un envoi vers 50 numeros en consomme 50. Parcourez toujours results plutot que de vous fier au seul code HTTP.

Consulter votre consommation

A verifier avant une campagne, plutot que de decouvrir le 429 au 200e message.

GET /api/v1/sms/usage
X-Api-Key: wak_...
{
  "plan": "starter",
  "dailyQuota": 200,
  "dailyUsed": 47,
  "dailyRemaining": 153,
  "monthlyQuota": 5000,
  "monthlyUsed": 1204,
  "monthlyRemaining": 3796,
  "relayCount": 2
}

Un quota a null signifie « illimite » ; le remaining correspondant vaut alors null aussi. relayCount donne le nombre de telephones enroles - a zero, rien ne partira.

Suivre l'etat d'un message

GET /api/v1/sms/{id}
X-Api-Key: wak_...
StatutSignification
pendingEn file, en attente d'un telephone disponible.
claimedUn telephone l'a pris en charge.
sendingEn cours de remise au reseau mobile.
sentLe telephone a remis le SMS au reseau mobile. Etat final.
failedEchec ; errorCode porte la cause. Etat final.
canceledAnnule depuis la console. Etat final.
sent ne veut pas dire « recu par le destinataire ». Il signifie que le telephone emetteur a remis le message au reseau de son operateur, et que celui-ci l'a accepte. Un telephone eteint, hors couverture ou dont la memoire est pleine peut donc donner sent sans que le message soit lu. Nous ne recuperons pas l'accuse de reception de l'operateur.

En pratique la quasi-totalite des sent arrivent. Mais si votre usage exige une preuve de reception - un envoi a valeur contractuelle ou juridique - sent ne la constitue pas.
Interroger cette adresse en boucle fonctionne, mais coute des requetes et vous fait decouvrir l'echec en retard. Le webhook ci-dessous fait le chemin inverse : c'est nous qui vous appelons.

Webhook : etre prevenu au lieu d'interroger

Dans la console, section Cles API, indiquez votre URL sous Webhook de statut. Nous l'appelons en POST a chaque etat final d'un message - sent, failed ou canceled.

Quelle URL indiquer

Une adresse de votre propre serveur - pas une adresse Winalco. Ce champ ne configure rien chez nous : il nous dit ou aller frapper chez vous. Vous creez une page qui accepte les POST, et vous collez son adresse.

Votre technologieURL typique
Site PHPhttps://boutique.dz/webhooks/winalco-sms.php
WordPress / WooCommercehttps://boutique.dz/wp-json/winalco/v1/sms
Laravel / Symfonyhttps://erp.entreprise.dz/api/webhooks/sms
Node / Expresshttps://api.entreprise.dz/webhooks/sms

Le chemin est libre. Quatre contraintes seulement :

  • En https - obligatoire, refuse sinon : la charge utile contient les numeros de vos destinataires.
  • Joignable depuis Internet - ni localhost, ni 192.168.x.x, ni un nom interne. Notre serveur appelle depuis l'exterieur ; ces adresses sont refusees a l'enregistrement.
  • Sans authentification - nous ne pouvons pas nous connecter. Ne placez pas cette page derriere un mot de passe ni une restriction par IP : c'est la signature qui prouve que l'appel vient de nous, pas un identifiant.
  • Reponse en moins de 10 secondes - au-dela nous coupons et considerons l'appel en echec.

A l'enregistrement, un secret de signature whs_... vous est donne une seule fois. Notez-le : il sert a verifier nos appels, et n'est jamais reaffiche.

Coller une URL avant d'avoir ecrit le code de reception ne sert a rien : nous appellerons dans le vide et enregistrerons des echecs. Preparez la page d'abord.

Desactiver le webhook efface aussi le secret. Ce n'est donc pas une pause : en le reactivant plus tard vous obtiendrez un nouveau secret, et votre code cessera d'accepter nos appels tant qu'il n'est pas mis a jour.

Ce que vous recevez

POST https://votre-serveur.dz/webhooks/sms
Content-Type: application/json
X-Winalco-Signature: t=1785000000,v1=8f3c...
X-Winalco-Delivery: 3c5e...

{
  "event": "message.sent",
  "id": "0f4c9b2e-7a15-4f8c-9e3d-1b2a3c4d5e6f",
  "to": "0661234567",
  "status": "sent",
  "errorCode": null,
  "createdUtc": "2026-07-26T15:30:00Z",
  "updatedUtc": "2026-07-26T15:30:12Z"
}

Repondez 200 des reception. Toute autre reponse - ou aucune - declenche de nouvelles tentatives : apres 1 min, 5 min, 30 min, 2 h, puis 6 h. Un serveur indisponible une matinee ne perd donc pas ses evenements. Passe ces cinq reprises, nous abandonnons ; l'etat reste consultable via GET /api/v1/sms/{id}.

Pourquoi verifier la signature

Votre URL est publique : n'importe qui peut lui envoyer un faux evenement « SMS envoye ». La signature prouve que l'appel vient de nous. Elle vaut HMAC-SHA256 de la chaine horodatage.corps, avec votre secret comme cle.

La page complete, en PHP

Copiez ce fichier tel quel. Vous n'avez que trois choses a adapter : le secret, votre connexion base de donnees, et le bloc 6 - votre logique metier.

La table a creer une fois :

create table sms_evenements (
  livraison_id varchar(64) primary key,
  sms_id       varchar(64) not null,
  statut       varchar(20) not null,
  recu_le      datetime    not null
);

winalco-sms.php :

<?php
// Recoit les notifications de statut de Winalco SMS Relay.

$secret = 'whs_votre_secret_donne_une_seule_fois';

// 1. Le corps BRUT : la signature porte sur ces octets exacts. Un JSON
//    re-encode ne donnerait plus la meme signature.
$corps     = file_get_contents('php://input');
$entete    = $_SERVER['HTTP_X_WINALCO_SIGNATURE'] ?? '';
$livraison = $_SERVER['HTTP_X_WINALCO_DELIVERY'] ?? '';

// 2. Decouper l'en-tete "t=...,v1=..."
$parts = [];
foreach (explode(',', $entete) as $morceau) {
    [$cle, $valeur] = array_pad(explode('=', $morceau, 2), 2, '');
    $parts[$cle] = $valeur;
}

// 3. Ecarter un evenement rejoue depuis une copie capturee.
if (!isset($parts['t'], $parts['v1']) || abs(time() - (int) $parts['t']) > 300) {
    http_response_code(400);
    exit;
}

// 4. Prouver que l'appel vient de Winalco.
$attendue = hash_hmac('sha256', $parts['t'] . '.' . $corps, $secret);
if (!hash_equals($attendue, $parts['v1'])) {
    http_response_code(401);
    exit;
}

$e = json_decode($corps, true);
if (!is_array($e) || !isset($e['id'], $e['status'])) {
    http_response_code(400);
    exit;
}

$pdo = new PDO('mysql:host=localhost;dbname=boutique;charset=utf8mb4', 'utilisateur', 'motdepasse');

// 5. Tolerer un doublon (voir la section suivante).
$deja = $pdo->prepare('select 1 from sms_evenements where livraison_id = ?');
$deja->execute([$livraison]);
if ($deja->fetch()) {
    http_response_code(200);
    exit;
}

$pdo->prepare('insert into sms_evenements (livraison_id, sms_id, statut, recu_le)
               values (?, ?, ?, now())')
    ->execute([$livraison, $e['id'], $e['status']]);

// 6. VOTRE logique metier. Ici : marquer la commande liee a ce SMS.
if ($e['status'] === 'failed') {
    $pdo->prepare('update commandes set client_prevenu = 0, a_rappeler = 1 where sms_id = ?')
        ->execute([$e['id']]);
} elseif ($e['status'] === 'sent') {
    $pdo->prepare('update commandes set client_prevenu = 1 where sms_id = ?')
        ->execute([$e['id']]);
}

http_response_code(200);
Pour que le bloc 6 fonctionne, vous devez avoir conserve l'id renvoye a l'envoi, dans votre table commandes. C'est le fil qui relie notre notification a votre donnee metier : sans lui, vous recevez un statut sans savoir a quoi il correspond.

Recevoir deux fois le meme evenement

Cela arrive, et c'est normal : si votre page traite l'appel mais que sa reponse se perd en route, nous considerons l'envoi en echec et reessayons. Vous recevrez le meme evenement une seconde fois.

L'en-tete X-Winalco-Delivery porte un identifiant unique par tentative de livraison logique : il est identique entre les reprises d'un meme evenement. C'est lui qu'on enregistre pour reconnaitre un doublon - le bloc 5 ci-dessus. Sans ce garde-fou, une action metier serait executee deux fois : deux emails, deux relances, deux lignes en comptabilite.

Si votre traitement est lent

Le script ci-dessus fait trois requetes SQL : c'est instantane, tres en dessous de nos 10 secondes. Mais si vous devez envoyer un email, appeler une autre API ou generer un document, enregistrez l'evenement, repondez 200, et traitez apres - via une file ou une tache planifiee. En PHP-FPM, fastcgi_finish_request() rend la reponse puis laisse le script continuer.

Sinon nous coupons a 10 secondes, comptons l'appel en echec, et reessayons : vous traiterez plusieurs fois le meme evenement.

Verification seule, en Node.js

const crypto = require('crypto')

function verifier(secret, entete, corps) {
  const parts = Object.fromEntries(entete.split(',').map((p) => p.split('=')))
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false

  const attendue = crypto
    .createHmac('sha256', secret)
    .update(`${parts.t}.${corps}`)
    .digest('hex')

  // timingSafeEqual : la comparaison ne doit pas trahir le secret par sa duree.
  return crypto.timingSafeEqual(Buffer.from(attendue), Buffer.from(parts.v1))
}
Comparez toujours les signatures avec hash_equals / timingSafeEqual, jamais avec ==. Et calculez le HMAC sur le corps brut : un JSON re-serialise ne donne pas les memes octets, donc pas la meme signature.

Codes d'erreur

Toute erreur rend le meme corps :

{
  "code": "daily_quota_exceeded",
  "message": "Quota journalier atteint (200 SMS aujourd'hui).",
  "details": { "limit": 200, "used": 200 }
}
Branchez votre code sur code, jamais sur message. Le code ne changera jamais : c'est le contrat. Le message est du francais destine a vos journaux, et nous le reformulons librement - une comparaison de texte cassera sans prevenir. details porte les valeurs chiffrees, pour que vous redigiez votre propre phrase dans votre langue.
CodeStatutSens et reaction attendue
destination_required400Le champ to est vide. Corrigez la fiche : reessayer a l'identique echouera pareil.
message_required400Le champ message est vide.
daily_quota_exceeded429Quota du jour atteint. details.limit donne le plafond. Reessayez demain.
monthly_quota_exceeded429Quota du mois atteint. Distinct du precedent : celui-ci ne se resout pas demain.
trial_expired403Periode d'essai terminee. Ne reessayez pas : seule une souscription debloque le compte. details.endedUtc donne la date de fin.
no_recipients400Envoi groupe avec une liste to vide.
too_many_recipients400Plus de details.max destinataires. Decoupez le lot.
idempotency_key_too_long400Cle d'idempotence au-dela de details.maxLength caracteres.
invalid_api_key401Cle absente, inconnue ou revoquee. N'insistez pas : verifiez l'en-tete.
message_not_found404GET /sms/{id} sur un identifiant qui n'est pas a vous.
internal_error500Panne de notre cote. Reessayez plus tard ; signalez-le si cela persiste.
Un code inconnu de cette table peut apparaitre (nous en ajouterons). Traitez-le comme le statut HTTP l'indique - 4xx : corriger, 5xx : reessayer - et lisez message. N'echouez jamais parce qu'un code vous est inconnu.

Le cas particulier de l'essai gratuit

A la fin de la periode d'essai, l'envoi est refuse en 403 - pas en 429. La distinction est deliberee : un 429 veut dire « ralentissez et reessayez », et un client bien ecrit rejouerait l'envoi indefiniment sans que rien ne se debloque.

{
  "code": "trial_expired",
  "message": "Periode d'essai terminee.",
  "details": { "endedUtc": "2026-08-01T00:00:00Z" }
}

Le reste du compte reste intact : la console, les cles, l'historique et les telephones enroles ne bougent pas. Seul l'envoi s'arrete, jusqu'a la souscription d'un plan.

Codes de reponse HTTP

CodeSensQue faire
200Rejeu d'une Idempotency-Key deja vue.Rien de plus a faire : le SMS etait deja en file.
201SMS accepte et mis en file.Conservez l'id.
207Envoi groupe partiellement accepte.Lisez results destinataire par destinataire.
400to ou message manquant ou invalide.Corrigez la requete ; la reessayer telle quelle echouera pareil.
401Cle absente, invalide ou revoquee.Verifiez l'en-tete X-Api-Key.
403Essai gratuit termine.Souscrivez un plan. Reessayer ne changera rien.
404Message inconnu pour votre compte.Verifiez l'id.
429Quota depasse.Attendez le renouvellement ou changez d'abonnement.
500Panne de notre cote.Reessayez plus tard, avec un delai croissant.

Chaque erreur porte aussi un code stable : voir Codes d'erreur.

Ce qui se passe entre l'appel et le SMS

Vos SMS partent de vraies cartes SIM, dans des telephones que vous avez enroles. Trois consequences utiles a connaitre :

  • Le delai n'est pas nul. Un message attend qu'un telephone le prenne - quelques secondes en general. Un envoi n'est pas instantane comme un appel a une API de paiement.
  • L'operateur du destinataire compte. Un SMS part en priorite d'une SIM du meme reseau (05, 06, 07), beaucoup moins chere. Si aucune SIM du bon reseau n'est disponible, le message peut attendre brievement. Ce comportement se regle dans la console, section Relais.
  • Le debit suit le nombre de telephones. Un telephone envoie quelques SMS par minute, pas des milliers. Pour une campagne, c'est le nombre de SIM enrolees qui fixe la cadence - pas notre API.

Pour un code de confirmation, reglez le repli sur « tout de suite » : la rapidite prime sur le cout. Pour une campagne, laissez l'attente faire son travail.