Aller au contenu principal

Guide de la passerelle USSD

← Retour à la documentation principale

Ce guide couvre la passerelle USSD OmniSS7, qui relie les dialogues USSD SS7/MAP aux rappels HTTP/JSON, permettant aux développeurs tiers de créer des applications USSD avec un simple point de terminaison HTTP.

Table des matières

  1. Aperçu
  2. Architecture
  3. Activation de la passerelle USSD
  4. Configuration
  5. Protocole de rappel HTTP
  6. USSD d'origine réseau (API Push)
  7. Cycle de vie de la session
  8. Gestion des erreurs
  9. Métriques et surveillance
  10. Serveur de rappel d'exemple
  11. Dépannage

Aperçu

La passerelle USSD gère deux directions de trafic USSD :

  • D'origine mobile (entrant) — Un abonné compose un code court (par exemple, *100#). La passerelle reçoit la MAP processUnstructuredSS-Request (opcode 59), la transmet à votre rappel HTTP et renvoie votre réponse via SS7.
  • D'origine réseau (sortant) — Votre application envoie un message USSD à un abonné via l'API REST. La passerelle envoie une MAP unstructuredSS-Request (opcode 60) via SS7 et achemine la réponse de l'abonné vers votre rappel.

Les deux directions prennent en charge des dialogues multi-tours — des menus interactifs où l'abonné répond et reçoit des invites de suivi.

Caractéristiques clés

PropriétéValeur
TransportHTTP POST synchrone par tour
EncodageAlphabet par défaut GSM 7 bits (DCS 0x0F) selon 3GPP TS 23.038
Longueur max du texte182 caractères (configurable)
Suivi de sessionUUID généré par la passerelle par dialogue
AuthentificationAucune (fait confiance au réseau SS7)
RoutageCorrespondance de préfixe de code court aux URL de rappel

Références 3GPP

SpécificationPertinence
3GPP TS 23.090USSD Étape 2 — architecture et procédures
3GPP TS 24.090USSD Étape 3 — détails du protocole
3GPP TS 29.002Protocole MAP — USSD-Arg, USSD-Res, opcodes 59/60/61
3GPP TS 23.038Alphabet par défaut GSM 7 bits et schéma de codage de données

Architecture

Flux d'origine mobile (entrant)

Flux d'origine réseau (Push sortant)

Vue d'ensemble des composants


Activation de la passerelle USSD

La passerelle USSD nécessite que le mode client MAP soit activé, ainsi que son propre drapeau de fonctionnalité.

config :omniss7,
map_client_enabled: true,
ussd_gateway_enabled: true

La passerelle nécessite également une connexion M3UA fonctionnelle (voir Guide du client MAP pour la configuration M3UA).


Configuration

Paramètres de la passerelle USSD

config :omniss7,
ussd_gateway_enabled: true,
ussd_gateway: %{
# Routage par code court — correspondance de préfixe la plus longue
routes: [
%{pattern: "*100", url: "http://balance-app:9000/ussd"},
%{pattern: "*200", url: "http://topup-app:9000/ussd"},
%{pattern: "*", url: "http://default-app:9000/ussd"}
],

# Délais d'expiration de session
session_timeout_ms: 180_000, # Durée totale de la session (3 minutes)
turn_timeout_ms: 30_000, # Temps d'attente maximum pour la réponse de l'abonné par tour (30 secondes)

# Paramètres de rappel HTTP
http_timeout_ms: 5_000, # Délai d'expiration pour le POST HTTP vers votre application (5 secondes)

# Limites de texte
max_text_length: 182 # Max GSM 7 bits (tronque avec avertissement si dépassé)
}

Référence des paramètres

ParamètreTypeRequisPar défautDescription
ussd_gateway_enabledBooléenOuifalseInterrupteur principal pour la fonctionnalité de la passerelle USSD
ussd_gateway.routesListe de MapsOui[]Règles de routage par préfixe de code court. Chaque entrée a pattern (chaîne de préfixe) et url (URL de rappel). La correspondance de préfixe la plus longue l'emporte.
ussd_gateway.session_timeout_msEntierNon180_000Durée maximale de la session en millisecondes. La session est terminée avec une erreur si elle est dépassée.
ussd_gateway.turn_timeout_msEntierNon30_000Temps maximum d'attente pour la réponse de l'abonné dans un dialogue multi-tours, en millisecondes.
ussd_gateway.http_timeout_msEntierNon5_000Délai d'expiration de la requête HTTP pour les rappels vers votre application, en millisecondes. Couvre à la fois le temps de connexion et le temps de réponse.
ussd_gateway.max_text_lengthEntierNon182Nombre maximum de caractères dans une chaîne de texte USSD. Les textes dépassant cette limite sont tronqués et un avertissement est enregistré.

Paramètres de routage

Chaque entrée dans la liste routes est une carte :

ParamètreTypeRequisDescription
patternChaîneOuiPréfixe de code court à correspondre. Utilisez "*" comme solution de repli. Les préfixes plus longs ont la priorité.
urlChaîneOuiURL du point de terminaison HTTP pour recevoir les POST de rappel pour les codes courts correspondants.

Correspondance des routes

Les routes sont correspondantes par préfixe le plus long d'abord. Pour la chaîne de composition *100# :

  1. "*100" correspond (longueur 4) — sélectionné
  2. "*10" correspond (longueur 3) — ignoré, plus court
  3. "*" correspond (longueur 1) — solution de repli

Si aucune route ne correspond, la passerelle renvoie une erreur MAP au mobile et enregistre un avertissement.


Protocole de rappel HTTP

Votre application reçoit des requêtes HTTP POST de la passerelle et répond avec des instructions JSON.

Requête de la passerelle vers votre application

Content-Type: application/json

Premier tour (initiation de session) :

{
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"msisdn": "+254712345678",
"type": "initiation",
"text": "*100#",
"turn": 1
}

Tours suivants (l'abonné a répondu) :

{
"session_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"msisdn": "+254712345678",
"type": "response",
"text": "1",
"turn": 2
}

Champs de requête

ChampTypeDescription
session_idChaîneUUID généré par la passerelle. Unique par dialogue USSD. Utilisez ceci pour corréler les tours.
msisdnChaîneMSISDN de l'abonné (si disponible à partir du message MAP). Peut être vide pour certains réseaux.
typeChaîne"initiation" pour le premier tour, "response" pour les réponses ultérieures de l'abonné.
textChaîneLa chaîne de composition (par exemple, *100#) lors de l'initiation, ou l'entrée de l'abonné (par exemple, 1) lors de la réponse.
turnEntierCompteur de tours commençant à 1. S'incrémente avec chaque interaction de l'abonné.

Réponse de votre application

Votre application doit répondre avec JSON contenant une action et un text :

Continuer (afficher le menu, attendre l'entrée de l'abonné) :

{
"action": "continue",
"text": "1. Solde\n2. Recharger\n3. Transférer"
}

Fin (afficher le message final, fermer la session) :

{
"action": "end",
"text": "Votre solde est de 5,00 $"
}

Champs de réponse

ChampTypeRequisDescription
actionChaîneOui"continue" pour garder la session ouverte et attendre l'entrée de l'abonné, ou "end" pour afficher un message final et fermer la session.
textChaîneOuiTexte à afficher sur le téléphone de l'abonné. La longueur maximale est régie par max_text_length (par défaut 182). Utilisez \n pour les sauts de ligne.

USSD d'origine réseau (API Push)

Envoyez un message USSD à un abonné depuis votre application.

Point de terminaison

POST /api/ussd/send

Requête

{
"msisdn": "+254712345678",
"text": "Vous avez une facture en attente. Répondez 1 pour payer.",
"callback_url": "http://billing-app:9000/ussd"
}

Champs de requête

ChampTypeRequisDescription
msisdnChaîneOuiMSISDN de l'abonné destinataire au format international.
textChaîneOuiTexte USSD initial à afficher. Encodé en GSM 7 bits.
callback_urlChaîneOuiURL pour recevoir la réponse de l'abonné via le protocole de rappel standard.

Réponse

Succès (200 OK) :

{
"session_id": "xyz-789-abc-123",
"status": "sent"
}

Réponses d'erreur :

Statut HTTPCorpsCause
400{"error": "requête invalide", "required": ["msisdn", "text", "callback_url"]}Manque un ou plusieurs champs requis, ou le corps n'est pas un JSON valide
500{"error": "{:gsm7_encode_failed, ...}"}Le texte contient des caractères non présents dans l'alphabet GSM 7 bits. L'échec de l'encodage est signalé par la branche d'erreur générique, donc le statut est 500, pas 400.
500{"error": "..."}Toute autre erreur d'envoi (par exemple, connectivité M3UA). Le champ error contient la raison interne inspectée.
503{"error": "passerelle USSD non activée"}ussd_gateway_enabled est false

Remarque sur les erreurs de validation vs. d'envoi : Les problèmes de validation de champ (clés manquantes, JSON mal formé) renvoient 400 requête invalide. Une fois la validation réussie, tout échec du chemin d'envoi — y compris les échecs d'encodage GSM 7 bits — est signalé comme 500 avec la raison inspectée dans le champ error.

Exemple cURL

curl -X POST http://localhost:8080/api/ussd/send \
-H "Content-Type: application/json" \
-d '{
"msisdn": "+254712345678",
"text": "Vous avez une facture en attente. Répondez 1 pour payer.",
"callback_url": "http://billing-app:9000/ussd"
}'

Cycle de vie de la session

Chaque dialogue USSD est suivi comme une session avec un session_id unique.

États de la session

Session à un tour

Si votre rappel renvoie "end" lors du premier tour, aucune session persistante n'est créée. La passerelle envoie un MAP End avec le résultat et renvoie immédiatement.

Session multi-tours

Si votre rappel renvoie "continue", la passerelle :

  1. Crée un Session GenServer enregistré dans UssdGateway.Registry
  2. Envoie un MAP Continue avec opcode 60 (unstructuredSS-Request) au mobile
  3. Attend la réponse de l'abonné (jusqu'à turn_timeout_ms)
  4. Transmet la réponse à votre rappel
  5. Répète jusqu'à ce que votre rappel renvoie "end" ou qu'un délai d'attente se produise

Comportement de délai d'attente

Délai d'attentePar défautEffet
Délai d'attente de tour30 secondesSi l'abonné ne répond pas dans cette fenêtre, la session est terminée avec une erreur MAP.
Délai d'attente de session3 minutesDurée totale de la session. Termine la session indépendamment de l'activité.
Délai d'attente de rappel HTTP5 secondesSi votre application ne répond pas à temps, la passerelle envoie une erreur MAP au mobile et termine la session.

Gestion des erreurs

La passerelle gère les échecs avec grâce et tente toujours d'envoyer une réponse d'erreur MAP au mobile afin que l'abonné voie un message significatif plutôt qu'un délai d'attente réseau.

ScénarioAction de la passerelleCode d'erreur MAP
Délai d'attente de rappel HTTP ou 5xxTerminer la session, envoyer MAP End avec erreur34 (systemFailure)
JSON invalide du rappelTerminer la session, envoyer MAP End avec erreur34 (systemFailure)
Le texte USSD dépasse max_text_lengthTronquer le texte, enregistrer un avertissement, continuer normalementN/A (tronqué, pas une erreur)
Délai d'attente de l'abonné (pas de réponse)Terminer la session, envoyer MAP End avec erreur34 (systemFailure)
Aucune route ne correspond au code courtEnvoyer MAP End avec erreur, enregistrer un avertissement34 (systemFailure)
Échec du Session GenServerLa session meurt, l'abonné voit un délai d'attente réseauN/A (sortie de processus)
Passerelle USSD non activéeRetourner Facility Not Supported21 (facilityNotSupported)

Métriques et surveillance

La passerelle USSD expose des métriques Prometheus sur le point de terminaison standard /metrics (port 8080).

Métriques USSD

Métrique: ussd_requests_total Type: Compteur Description: Total des requêtes USSD traitées Étiquettes:

  • direction"inbound" (d'origine mobile) ou "outbound" (push d'origine réseau)

Métrique: ussd_active_sessions Type: Jauge Description: Déclarée comme une jauge pour le nombre de sessions USSD actives.

Avertissement: Cette jauge est déclarée mais jamais mise à jour dans la version actuelle — elle lit toujours 0. Ne comptez pas dessus pour les comptes de session en direct. Suivez l'activité de session via ussd_requests_total à la place.

Métrique: map_request_duration_milliseconds Type: Histogramme Description: Durée des opérations d'envoi USSD en millisecondes Étiquettes:

  • operation"ussd_send" pour les requêtes push sortantes

Exemples de requêtes Prometheus

# Taux de requêtes USSD par direction (entrant vs sortant)
rate(ussd_requests_total[5m])

# Latence USSD sortante (p95)
histogram_quantile(0.95, rate(map_request_duration_milliseconds_bucket{operation="ussd_send"}[5m]))

Serveur de rappel d'exemple

Python (Flask)

from flask import Flask, request, jsonify

app = Flask(__name__)
sessions = {}

@app.route('/ussd', methods=['POST'])
def ussd():
data = request.json
session_id = data['session_id']
text = data['text']
turn = data['turn']

if data['type'] == 'initiation':
sessions[session_id] = {'state': 'main_menu'}
return jsonify({
'action': 'continue',
'text': 'Bienvenue!\n1. Vérifier le solde\n2. Acheter du crédit\n3. Transférer'
})

state = sessions.get(session_id, {}).get('state')

if state == 'main_menu':
if text == '1':
del sessions[session_id]
return jsonify({
'action': 'end',
'text': 'Votre solde est de 5,00 $'
})
elif text == '2':
sessions[session_id]['state'] = 'buy_airtime'
return jsonify({
'action': 'continue',
'text': 'Entrez le montant :'
})
else:
del sessions[session_id]
return jsonify({
'action': 'end',
'text': 'Option invalide. Au revoir.'
})

elif state == 'buy_airtime':
del sessions[session_id]
return jsonify({
'action': 'end',
'text': f'Vous avez acheté {text} $ de crédit. Merci!'
})

return jsonify({'action': 'end', 'text': 'Session expirée.'})

if __name__ == '__main__':
app.run(host='0.0.0.0', port=9000)

Node.js (Express)

const express = require('express');
const app = express();
app.use(express.json());

const sessions = new Map();

app.post('/ussd', (req, res) => {
const { session_id, text, type } = req.body;

if (type === 'initiation') {
sessions.set(session_id, { state: 'main_menu' });
return res.json({
action: 'continue',
text: 'Bienvenue!\n1. Vérifier le solde\n2. Acheter du crédit'
});
}

const session = sessions.get(session_id);
if (!session) {
return res.json({ action: 'end', text: 'Session expirée.' });
}

if (session.state === 'main_menu' && text === '1') {
sessions.delete(session_id);
return res.json({ action: 'end', text: 'Votre solde est de 5,00 $' });
}

sessions.delete(session_id);
return res.json({ action: 'end', text: 'Au revoir.' });
});

app.listen(9000, () => console.log('Rappel USSD sur le port 9000'));

Dépannage

Le numéro USSD renvoie "Service non disponible"

Symptômes: L'abonné compose un code court et obtient immédiatement une erreur réseau.

Causes possibles:

  • ussd_gateway_enabled est false
  • Aucune route ne correspond au code court composé
  • La connexion M3UA est hors service

Résolution:

  1. Vérifiez ussd_gateway_enabled: true dans la configuration
  2. Vérifiez qu'un modèle de route correspond au code court (n'oubliez pas d'inclure * comme solution de repli)
  3. Vérifiez l'état du pair M3UA dans l'interface Web (page des pairs)

Le rappel ne reçoit pas de requêtes

Symptômes: Les journaux de la passerelle montrent un début USSD mais votre application ne reçoit jamais le POST HTTP.

Causes possibles:

  • L'URL de rappel est inaccessible depuis l'hôte OmniSS7
  • Pare-feu bloquant HTTP sortant depuis OmniSS7
  • Application de rappel non en cours d'exécution

Résolution:

  1. Testez la connectivité : curl -v http://your-app:9000/ussd depuis l'hôte OmniSS7
  2. Vérifiez les règles de pare-feu pour HTTP sortant
  3. Vérifiez que votre application de rappel écoute sur le port configuré

Les sessions expirent prématurément

Symptômes: Les sessions multi-tours se terminent avec "systemFailure" avant que l'abonné puisse répondre.

Causes possibles:

  • turn_timeout_ms est trop court pour votre base d'abonnés
  • http_timeout_ms est trop court pour le temps de traitement de votre application
  • Latence réseau entre OmniSS7 et votre serveur de rappel

Résolution:

  1. Augmentez turn_timeout_ms (30 secondes par défaut devraient suffire pour la plupart des cas)
  2. Augmentez http_timeout_ms si votre application a besoin de plus de temps de traitement
  3. Déployez le serveur de rappel près d'OmniSS7 pour réduire la latence

Erreurs d'encodage GSM 7 bits

Symptômes: Erreurs gsm7_encode_failed dans les journaux, ou une réponse 500 de /api/ussd/send dont le champ error contient {:gsm7_encode_failed, ...}.

Causes possibles:

  • Le texte contient des caractères en dehors de l'alphabet par défaut GSM 7 bits (par exemple, emoji, caractères CJK)

Résolution:

  • Limitez le texte USSD à l'ensemble de caractères de base GSM : lettres ASCII, chiffres, ponctuation courante, et quelques caractères grecs/nordiques
  • Voir 3GPP TS 23.038 Section 6.2.1 pour le tableau complet des caractères

Documentation connexe