Envoyer des SMS depuis votre logiciel, et savoir ce qu'ils deviennent.
Base : https://sms-relay.winalco.dz
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.
401 immediatement.
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.
curl -X POST https://sms-relay.winalco.dz/api/v1/sms/send \
-H "X-Api-Key: wak_votre_cle" \
-H "Content-Type: application/json" \
-d '{"to":"0661234567","message":"Bonjour"}'
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.
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" }
| Reponse | Sens |
|---|---|
201 Created | Premier appel : le SMS a ete mis en file. |
200 OK | Cle 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.
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 },
{ "to": "0770000001", "id": "1a2b...", "status": "pending", "message": null },
{ "to": "0550000002", "id": null, "status": "rejected", "message": "Quota mensuel atteint." }
]
}
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 :
| Code | Sens |
|---|---|
201 | Tous les destinataires acceptes. |
207 | Une partie seulement - lisez results. |
400 | Aucun destinataire accepte. |
results plutot
que de vous fier au seul code HTTP.
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.
GET /api/v1/sms/{id}
X-Api-Key: wak_...
| Statut | Signification |
|---|---|
pending | En file, en attente d'un telephone disponible. |
claimed | Un telephone l'a pris en charge. |
sending | En cours de remise au reseau mobile. |
sent | Le telephone a remis le SMS au reseau mobile. Etat final. |
failed | Echec ; errorCode porte la cause. Etat final. |
canceled | Annule 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.
sent arrivent. Mais si
votre usage exige une preuve de reception - un envoi a valeur
contractuelle ou juridique - sent ne la constitue pas.
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.
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 technologie | URL typique |
|---|---|
| Site PHP | https://boutique.dz/webhooks/winalco-sms.php |
| WordPress / WooCommerce | https://boutique.dz/wp-json/winalco/v1/sms |
| Laravel / Symfony | https://erp.entreprise.dz/api/webhooks/sms |
| Node / Express | https://api.entreprise.dz/webhooks/sms |
Le chemin est libre. Quatre contraintes seulement :
https - obligatoire, refuse sinon : la
charge utile contient les numeros de vos destinataires.
localhost,
ni 192.168.x.x, ni un nom interne. Notre serveur appelle
depuis l'exterieur ; ces adresses sont refusees a l'enregistrement.
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.
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}.
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.
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);
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.
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.
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.
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))
}
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.
Toute erreur rend le meme corps :
{
"code": "daily_quota_exceeded",
"message": "Quota journalier atteint (200 SMS aujourd'hui).",
"details": { "limit": 200, "used": 200 }
}
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.
| Code | Statut | Sens et reaction attendue |
|---|---|---|
destination_required | 400 | Le champ to est vide. Corrigez la fiche : reessayer a l'identique echouera pareil. |
message_required | 400 | Le champ message est vide. |
daily_quota_exceeded | 429 | Quota du jour atteint. details.limit donne le plafond. Reessayez demain. |
monthly_quota_exceeded | 429 | Quota du mois atteint. Distinct du precedent : celui-ci ne se resout pas demain. |
no_recipients | 400 | Envoi groupe avec une liste to vide. |
too_many_recipients | 400 | Plus de details.max destinataires. Decoupez le lot. |
idempotency_key_too_long | 400 | Cle d'idempotence au-dela de details.maxLength caracteres. |
invalid_api_key | 401 | Cle absente, inconnue ou revoquee. N'insistez pas : verifiez l'en-tete. |
message_not_found | 404 | GET /sms/{id} sur un identifiant qui n'est pas a vous. |
internal_error | 500 | Panne de notre cote. Reessayez plus tard ; signalez-le si cela persiste. |
Dans un envoi groupe, chaque entree rejetee de
results porte son propre code : vous distinguez
ainsi un quota atteint d'un numero invalide, destinataire par
destinataire, sans analyser de texte.
message. N'echouez jamais parce qu'un
code vous est inconnu.
| Code | Sens | Que faire |
|---|---|---|
200 | Rejeu d'une Idempotency-Key deja vue. | Rien de plus a faire : le SMS etait deja en file. |
201 | SMS accepte et mis en file. | Conservez l'id. |
207 | Envoi groupe partiellement accepte. | Lisez results destinataire par destinataire. |
400 | to ou message manquant ou invalide. | Corrigez la requete ; la reessayer telle quelle echouera pareil. |
401 | Cle absente, invalide ou revoquee. | Verifiez l'en-tete X-Api-Key. |
404 | Message inconnu pour votre compte. | Verifiez l'id. |
429 | Quota depasse. | Attendez le renouvellement ou changez d'abonnement. |
Chaque erreur porte aussi un code stable : voir
Codes d'erreur.
Vos SMS partent de vraies cartes SIM, dans des telephones que vous avez enroles. Deux consequences utiles a connaitre :
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.