Winalco SMS Relay - Guide de l'API

Envoyer des SMS depuis votre logiciel, et savoir ce qu'ils deviennent. Base : https://sms-relay.winalco.dz

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.

Exemple complet

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"}'

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.

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 },
    { "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 :

CodeSens
201Tous les destinataires acceptes.
207Une partie seulement - lisez results.
400Aucun destinataire accepte.
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.

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 :

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_required400 Le champ to est vide. Corrigez la fiche : reessayer a l'identique echouera pareil.
message_required400 Le champ message est vide.
daily_quota_exceeded429 Quota du jour atteint. details.limit donne le plafond. Reessayez demain.
monthly_quota_exceeded429 Quota du mois atteint. Distinct du precedent : celui-ci ne se resout pas demain.
no_recipients400 Envoi groupe avec une liste to vide.
too_many_recipients400 Plus de details.max destinataires. Decoupez le lot.
idempotency_key_too_long400 Cle d'idempotence au-dela de details.maxLength caracteres.
invalid_api_key401 Cle absente, inconnue ou revoquee. N'insistez pas : verifiez l'en-tete.
message_not_found404 GET /sms/{id} sur un identifiant qui n'est pas a vous.
internal_error500 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.

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.

Codes de reponse

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.
404Message inconnu pour votre compte.Verifiez l'id.
429Quota depasse.Attendez le renouvellement ou changez d'abonnement.

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. 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.