G-SMS.

Idempotence : le champ clientReference

Envoyer deux fois le même SMS coûte deux fois son prix et agace le destinataire. Un champ facultatif suffit à rendre l'envoi rejouable sans risque : clientReference. C'est la protection la plus simple de l'API, et celle dont la réponse surprend le plus.

Le problème

Un appel HTTP peut réussir côté serveur et échouer côté client : la connexion tombe après le débit, avant que la réponse ne revienne. Le code appelant ne sait pas si le message est parti. S'il réessaie, il envoie un second SMS ; s'il abandonne, il en oublie peut-être un.

Les trois situations où cela arrive vraiment :

  • un réseau instable, très ordinaire depuis un serveur mal connecté ;
  • une file de travaux qui rejoue une tâche après un redémarrage, parce qu'elle ne l'a pas vue se terminer ;
  • un utilisateur qui clique deux fois, ou un formulaire soumis deux fois par le navigateur.

Poser une référence

Il suffit d'ajouter clientReference au corps de la requête. Sa valeur t'appartient : G-SMS ne l'interprète pas, il se contente de refuser de la servir deux fois.

curl -X POST 'https://api.sms.gworldship.com/v1/sms' \
  -H 'Authorization: Bearer gsk_votre_cle_api' \
  -H 'Content-Type: application/json' \
  -d '{
        "to": "+237677123456",
        "text": "Commande 10427 confirmee.",
        "clientReference": "commande-10427-confirmation"
      }'
HTTP/1.1 202 Accepted

{
  "messageId": "0d0a6f4e-1c3b-4f6a-9a1e-52a0f6d2b311",
  "to": "+237677123456",
  "operator": "MTN",
  "segments": 1,
  "priceXaf": 5,
  "status": "QUEUED",
  "clientReference": "commande-10427-confirmation",
  "balanceAfterXaf": 4995,
  "createdAt": "2026-09-12T10:15:00Z"
}

Le contrôle est fait avant l'estimation du prix et avant le débit : un doublon ne coûte rien, n'entame aucun quota et ne crée aucun message.

Même en cas d'appels simultanés portant la même référence, un seul message est créé : la requête perdante reçoit le message gagnant et sa transaction, débit compris, est annulée.

Ce que renvoie un doublon — le piège n° 1

Rejoue exactement la même requête : le statut est 409 Conflict, mais le corps n'est pas un objet d'erreur. C'est le message déjà envoyé, dans sa forme habituelle, avec son statut à jour.

HTTP/1.1 409 Conflict

{
  "messageId": "0d0a6f4e-1c3b-4f6a-9a1e-52a0f6d2b311",
  "to": "+237677123456",
  "operator": "MTN",
  "segments": 1,
  "priceXaf": 5,
  "status": "DELIVERED",
  "clientReference": "commande-10427-confirmation",
  "createdAt": "2026-09-12T10:15:00Z",
  "sentAt": "2026-09-12T10:15:04Z",
  "deliveredAt": "2026-09-12T10:15:09Z"
}

Ni code, ni message : ces deux champs-là n'existent pas dans cette réponse. Le seul écart avec un 202 est l'absence de balanceAfterXaf, puisqu'aucun débit n'a eu lieu. Pour comparaison, voici à quoi ressemble une erreur ordinaire :

HTTP/1.1 402 Payment Required

{
  "code": "INSUFFICIENT_BALANCE",
  "message": "Solde insuffisant pour couvrir 5 XAF"
}
Un client qui traite tous les 4xx de façon uniforme se casse exactement ici : il lève une exception dont le code vaut undefined, alors que le SMS est bel et bien parti. Le symptôme classique est une file qui rejoue indéfiniment un envoi déjà réussi.
// Ce code paraît raisonnable. Il est faux.
const reponse = await fetch(url, options);
if (!reponse.ok) {
    const erreur = await reponse.json();
    throw new Error(erreur.code); // sur un 409 : undefined
}

Ce choix a une logique : sur un doublon, ce qui intéresse l'appelant n'est pas le motif du refus, c'est l'état du message qu'il croyait devoir créer. Encore faut-il l'avoir prévu.

Code défensif

La règle tient en une phrase : 202 et 409 sont deux succès, tout le reste est une erreur au format { code, message }.

async function envoyerSms(to, text, clientReference) {
    const reponse = await fetch('https://api.sms.gworldship.com/v1/sms', {
        method: 'POST',
        headers: {
            Authorization: 'Bearer ' + process.env.GSMS_API_KEY,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({ to, text, clientReference }),
    });

    const corps = await reponse.json();

    // 202 : message créé et débité. 409 : ce message existait déjà, rien n'a
    // été débité. Dans les deux cas le corps EST le message, pas une erreur.
    if (reponse.status === 202 || reponse.status === 409) {
        return { message: corps, dejaEnvoye: reponse.status === 409 };
    }

    // Tout le reste suit la forme { code, message }.
    throw new GSmsError(corps.code, corps.message, reponse.status);
}

Le drapeau dejaEnvoye a son utilité : il distingue « je viens d'envoyer » de « c'était déjà fait », ce qui évite de recompter un envoi dans des statistiques ou de renvoyer une notification interne.

Dans un langage sans distinction naturelle entre les deux, la même règle s'écrit en une condition : accepter 202 et 409, et ne lire code que sur les autres statuts.

Bien choisir sa référence

RègleDescription
Dérivée du métiercommande-10427-confirmation, otp-inscription-8821, facture-2026-09-441 : la référence se recalcule à l'identique si l'opération est rejouée.
Stable entre deux tentativesUne valeur tirée au hasard ou un horodatage à la milliseconde change à chaque essai : la protection ne joue plus.
64 caractères au plusAu-delà, la requête est refusée avec 400 VALIDATION_ERROR.
Unique par applicationDeux applications du même compte peuvent utiliser la même référence sans se gêner. À l'intérieur d'une application, elle ne sert qu'une fois.
Sans donnée personnelleElle est renvoyée dans les réponses, dans les webhooks et lisible en console : un numéro ou un nom n'y ont pas leur place.

Une référence reste prise tant que le message correspondant figure au journal. Pour des envois volontairement répétés — une relance, un code renvoyé à la demande — il faut donc une référence différente à chaque fois : otp-inscription-8821-envoi-2 plutôt que otp-inscription-8821. Sinon le second envoi ne partira jamais, et c'est le premier message qui sera renvoyé en réponse.

La référence voyage ensuite avec le message : elle est renvoyée par GET /v1/sms/{messageId} et présente dans chaque événement de webhook, ce qui permet de réconcilier un accusé de réception avec une commande sans conserver de table de correspondance.

Prêt à envoyer ton premier SMS ?

Crée un compte, reçois 100 F de crédit et une clé API immédiate.

Créer un compte