G-SMS.

G-SMS · Cas d’usage

Authentification par OTP au Cameroun : le code à usage unique par SMS

Vérifier qu’un utilisateur possède bien le numéro qu’il déclare, à l’inscription comme à chaque connexion sensible. Voici comment le faire avec G-SMS, ce que cela coûte réellement, et les règles qui évitent qu’un dispositif de sécurité ne devienne une porte ouverte.

Le problème : au Cameroun, l’identité passe par le numéro

Une adresse e-mail se crée en trente secondes et s’abandonne aussi vite. Un numéro de téléphone camerounais, lui, est rattaché à une SIM identifiée, sert au Mobile Money et reste le canal par lequel un client sera réellement joint. C’est l’identifiant de fait du marché.

Tant que ce numéro n’est pas vérifié, votre base de comptes contient des numéros mal saisis, des numéros inexistants et des doublons. Le jour où vous devez joindre un acheteur pour une livraison ou un remboursement, la donnée sur laquelle vous comptiez n’existe pas.

Ce que coûte l’absence de vérification

Chiffrez-le sur vos propres volumes plutôt que sur des moyennes de marché. Si votre support passe cinq minutes à réactiver manuellement un compte dont le numéro est faux, et qu’il le fait vingt fois par mois, cela représente près de deux heures de travail mensuelles. À n’importe quel taux horaire, c’est déjà plus cher que les quelques milliers de francs d’OTP qui l’auraient évité.

S’y ajoutent les effets qui ne se voient pas dans un ticket de support : les comptes multiples créés pour capter plusieurs fois une promotion d’inscription, et les paniers abandonnés faute de pouvoir rappeler l’acheteur.

La solution en trois étapes

G-SMS transporte le message. La génération du code, sa conservation et sa vérification restent dans votre application — c’est ce qui garantit que le secret ne circule pas.

1. Générer le code et le stocker haché

import { createHash, randomInt } from 'node:crypto';

// Six chiffres : saisissables au clavier du téléphone, dictables au téléphone.
const code = String(randomInt(0, 1_000_000)).padStart(6, '0');

await db.otpChallenges.upsert({
  phone,                                            // +237690000000 (E.164)
  codeHash: createHash('sha256').update(code).digest('hex'),  // jamais en clair
  expiresAt: Date.now() + 5 * 60_000,               // 5 minutes
  attempts: 0,
});

2. Envoyer le SMS

const response = await fetch('https://api.sms.gworldship.com/v1/sms', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer gsk_votre_cle_api',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    to: phone,
    text: `Votre code de connexion est ${code}. Il expire dans 5 minutes.`,
    clientReference: `otp-${userId}-${challengeId}`,   // rejoue sans double envoi
  }),
});

if (response.status === 202) {
  const sms = await response.json();
  // sms.messageId, sms.segments, sms.priceXaf, sms.balanceAfterXaf
} else if (response.status === 409) {
  // Déjà envoyé avec cette référence : la réponse EST le message, pas une erreur.
} else {
  const error = await response.json();   // { code, message }
  logger.warn('G-SMS %s %s', response.status, error.code);
}

L’opérateur est déduit du numéro : vous n’avez jamais à le préciser. La réponse indique le nombre de segments facturés, le prix débité et le solde restant.

HTTP/1.1 202 Accepted

{
  "messageId": "b7d1f0c2-5a3e-4c19-9f0a-2d6e8c41b3aa",
  "to": "+237690000000",
  "operator": "ORANGE",
  "segments": 1,
  "priceXaf": 5,
  "status": "QUEUED",
  "clientReference": "otp-4821-3",
  "balanceAfterXaf": 4995,
  "createdAt": "2026-09-12T09:14:07Z"
}

3. Vérifier la saisie

import { createHash, timingSafeEqual } from 'node:crypto';

const challenge = await db.otpChallenges.find(phone);

if (!challenge || challenge.expiresAt < Date.now()) return reject('EXPIRED');
if (challenge.attempts >= 3) return reject('TOO_MANY_ATTEMPTS');

await db.otpChallenges.increment(phone, 'attempts');

// Comparaison à temps constant : une comparaison naïve fuit la bonne réponse.
const valid = timingSafeEqual(
  createHash('sha256').update(submitted).digest(),
  Buffer.from(challenge.codeHash, 'hex'),
);

if (!valid) return reject('INVALID');

await db.otpChallenges.delete(phone);   // un code ne sert qu'une seule fois
return accept();

Le même mécanisme — une requête, une référence client, un statut de livraison — sert ensuite à vos notifications transactionnelles et à vos rappels de rendez-vous. Seules changent les règles de délai et de fréquence.

Le coût réel, en francs CFA

Hypothèse : 1 000 inscriptions par mois, un code par inscription, répartition supposée de 85 % vers Orange et MTN et 15 % vers Camtel et Nexttel. Ajustez cette répartition à votre base réelle.

PosteVolumeTarif unitaireCoût mensuel
Codes envoyés vers Orange et MTN8505 F4 250 F
Codes envoyés vers Camtel et Nexttel1507 F1 050 F
Renvois à la demande (10 % des inscriptions)1005 F500 F
Total1 100 SMS5 800 F

Soit 5,80 F par inscription vérifiée. Le message d’exemple ci-dessus fait 61 caractères : il tient dans un seul segment, donc le tarif affiché est le coût réel. Ajoutez un émoji et il bascule en UCS-2 : il reste dans un segment, mais la marge tombe de 99 à 7 caractères. Le même message avec un émoji et un nom de service un peu long passerait à deux segments, donc au double du prix. Vérifier un message dans le calculateur →

Les messages qui échouent sont recrédités automatiquement : votre dépense réelle porte sur les messages effectivement pris en charge par l’opérateur.

Les règles qui font qu’un OTP protège vraiment

  • Cinq minutes de validité. Au-delà, invalidez le code plutôt que de le laisser vivre.
  • Trois tentatives de saisie. À la quatrième, le code est détruit et l’utilisateur doit en demander un nouveau.
  • Un seul code actif par numéro. Un nouvel envoi remplace le précédent ; deux codes valables en même temps doublent la surface d’attaque.
  • Un renvoi après soixante secondes, trois renvois par heure et par numéro. Sans ce plafond, votre facture devient la variable d’ajustement de qui déclenche des envois en boucle.
  • Le code ne sort jamais de votre serveur autrement que par le SMS. Ni dans la réponse HTTP, ni dans les journaux, ni dans un outil de suivi d’erreurs.
  • Comparaison à temps constant. Une comparaison de chaînes classique s’arrête au premier caractère faux et renseigne l’attaquant.
  • Le code tôt dans le texte, écrit une seule fois. Android et iOS savent alors le proposer en remplissage automatique.
  • Une référence client unique par tentative. Si votre serveur réessaie après une coupure réseau, G-SMS reconnaît la référence et ne facture pas un second envoi.
Le doublon de référence répond 409 avec le message déjà envoyé, au format habituel — pas avec un objet d’erreur. Un client qui traite tous les codes 4xx de la même manière se casse exactement là. Comprendre l’idempotence →

Que faire quand le code n’arrive pas

SituationCe que renvoie l’APILa bonne réaction
Envois vers l’opérateur suspendus503 OPERATOR_DISABLEDRien n’est débité. Réessayez après le délai indiqué par Retry-After, ou proposez un autre moyen de connexion.
Solde épuisé402 INSUFFICIENT_BALANCESurveillez balanceAfterXaf à chaque envoi et alertez bien avant d’atteindre zéro.
Cadence trop élevée429 RATE_LIMITEDLa limite est de 10 requêtes par seconde et par clé. Réessayez après une seconde.
Le SMS part mais n’est jamais remisStatut FAILEDLe crédit est remboursé. Affichez un bouton de renvoi, et retentez au bout de soixante secondes.
Aucun accusé au bout de 24 heuresFAILED / TIMEOUTLe message est considéré comme perdu et remboursé. Traitez-le comme un échec ordinaire.

Les changements de statut vous sont poussés par webhook signé, sans avoir à interroger l’API. Suivre les statuts de livraison →

Ce que G-SMS ne fait pas

  • G-SMS ne génère pas et ne vérifie pas les codes. Il n’existe pas d’endpoint qui produirait un OTP à votre place : le secret, sa durée de vie et le compteur de tentatives restent chez vous. C’est délibéré — un code vérifié ailleurs que dans votre application est un code que vous ne maîtrisez plus.
  • Pas de nom d’expéditeur personnalisé. Le destinataire voit le numéro de la SIM d’envoi, jamais un nom de marque. Conséquence pratique : nommez-vous dans le texte du message.
  • Pas de réception de SMS. Aucune vérification par message entrant, aucune réponse « OUI » de l’utilisateur. C’est un choix de conception définitif, pas une fonctionnalité à venir.
  • Pas de canal de secours vocal. Si un numéro ne reçoit rien, prévoyez une autre voie dans votre application plutôt que de compter sur un appel automatique.
📵 L’expéditeur vu par le destinataire est le numéro de la SIM d’envoi, pas un nom de marque : G-SMS est conçu pour les SMS transactionnels (codes OTP, notifications), pas pour les campagnes marketing.

Questions fréquentes

Combien coûte un OTP par SMS au Cameroun ?

5 F CFA par segment vers Orange et MTN, 7 F vers Camtel et Nexttel. Un code à six chiffres tient dans un seul segment : le code envoyé coûte donc 5 ou 7 F, sans abonnement ni engagement.

Combien de temps un code doit-il rester valable ?

Cinq minutes suffisent. Plus court, l’utilisateur qui change d’application pour lire son SMS revient trop tard ; plus long, la fenêtre offerte à un attaquant s’élargit sans bénéfice.

Que se passe-t-il si le SMS n’arrive pas ?

Le message passe en FAILED et le montant débité est recrédité automatiquement, une seule fois. Votre application propose un renvoi après soixante secondes.

Prêt à envoyer ton premier SMS ?

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

Les 100 F offerts couvrent vingt codes de test vers Orange ou MTN.

Créer un compte