- Architecture
- Télécoms
- SaaS
Un moteur de fallback OTP multi-canal : Telegram, WhatsApp, SMS
Comment réduire le coût d'un code d'authentification sans dégrader la délivrabilité, en hiérarchisant les canaux par coût puis par taux de succès observé.
Le SMS est le canal par défaut pour les codes à usage unique. Il est aussi le plus cher et, sur certains réseaux, le moins fiable. L'idée derrière ChatLink est simple : essayer d'abord les canaux gratuits, et ne payer que lorsqu'il n'y a plus d'alternative.
Le classement des canaux n'est pas statique
Un ordre fixe (Telegram, puis WhatsApp, puis SMS) fonctionne en moyenne mais ignore la réalité : un utilisateur sans compte Telegram fait systématiquement échouer la première tentative, ajoutant quelques secondes avant le canal utile.
Le critère pertinent est donc une combinaison de deux signaux :
- le coût unitaire du canal, connu à l'avance ;
- le taux de succès observé pour ce canal, sur ce préfixe opérateur, sur les derniers jours.
type Channel = "telegram" | "whatsapp" | "sms";
async function rankChannels(msisdn: string): Promise<Channel[]> {
const stats = await getDeliveryStats(prefixOf(msisdn));
return (["telegram", "whatsapp", "sms"] as Channel[])
.map((channel) => ({
channel,
// Coût attendu = prix du canal pondéré par sa probabilité d'échec
expected: COST[channel] / Math.max(stats[channel].successRate, 0.05),
}))
.sort((a, b) => a.expected - b.expected)
.map((entry) => entry.channel);
}Le plancher à 0.05 évite qu'un canal ayant connu une panne temporaire soit exclu définitivement : il garde une chance d'être réessayé une fois le service rétabli.
Distinguer l'échec d'envoi de l'échec de réception
C'est la nuance qui fait ou défait un moteur de fallback. Trois états bien différents :
| État | Signification | Action |
|---|---|---|
rejected | Le canal refuse l'envoi (pas de compte, numéro invalide) | Basculer immédiatement |
sent | Le canal a accepté, livraison non confirmée | Attendre le délai de grâce |
delivered | Accusé de réception reçu | Arrêter la chaîne |
Basculer sur un simple sent sans confirmation envoie deux codes différents au même utilisateur, qui saisit alors le premier reçu, souvent déjà invalidé par le second. C'est le bug le plus coûteux en support.
Le délai de grâce
Entre sent et la bascule, il faut attendre. Trop court, on double les envois ; trop long, l'utilisateur abandonne.
En pratique, un délai adaptatif fonctionne mieux qu'une constante : la médiane du temps de livraison observé sur ce canal, plus une marge. Sur Telegram, cela donne environ deux secondes ; sur SMS, une quinzaine.
const grace = Math.min(stats[channel].p50DeliveryMs * 2.5, 20_000);Un seul code, plusieurs canaux
Le code est généré une fois, stocké avec son empreinte et son horodatage, puis réutilisé par chaque tentative de canal. Sans cette règle, la bascule invalide le code précédent et l'utilisateur se retrouve systématiquement avec un code périmé.
const otp = await createOtp({ msisdn, ttlSeconds: 300 });
for (const channel of await rankChannels(msisdn)) {
const result = await send(channel, msisdn, otp.code);
if (result.status === "delivered") return result;
if (result.status === "sent" && (await waitForAck(otp.id, grace))) return result;
}Mesurer le coût réel, pas le coût affiché
Le vrai indicateur n'est pas le prix du SMS mais le coût par authentification réussie. Un canal gratuit avec 40 % de succès, suivi d'un SMS payant dans les 60 % de cas restants, coûte plus cher qu'il n'y paraît, mais reste nettement moins cher que 100 % de SMS.
C'est cette métrique, suivie par opérateur et par semaine, qui pilote le réglage du moteur. Sans elle, on optimise à l'intuition une chaîne dont chaque maillon a un comportement différent selon le réseau.
Articles liés
Intégrer les API de paiement des agrégateurs sans se faire piéger
Mobile Money, cartes bancaires, agrégateurs multiples : le modèle unifié, l'idempotence et la réconciliation qui évitent les doubles débits en production.
- Paiement
- FinTech
- Architecture
- API
Ma méthode pour un front-end Angular qui reste maintenable
Découpage par domaine, composants standalone, signaux contre RxJS, intercepteurs et détection de changement : les décisions que je prends au démarrage d'un projet Angular.
- Angular
- TypeScript
- Architecture
- Frontend