Contrat API — Points de vente TiketMOMO (une boutique, plusieurs routeurs)
Public : développeurs de l’app mobile et des portails captifs. Toutes les valeurs ci-dessous sont fictives.
1. Le problème réglé
Une boutique (store_slug) identifie le vendeur, pas l’équipement. Un vendeur peut vendre
sur plusieurs routeurs, et le nom du serveur hotspot (nasid) n’est pas unique : deux routeurs
nommés « WIFI-ZONE » et « WIFI ZONE », ou le même « hotspot1 » chez plusieurs vendeurs.
Conséquences sans point de vente : un ticket tiré du stock du mauvais routeur (code invalide pour l’acheteur), des ventes impossibles à séparer par routeur.
Un point de vente = un routeur = un créneau VPN. Son identifiant, point_id, est stable tant
que le routeur garde son créneau. C’est lui qui porte le stock de tickets de ce routeur.
2. Lister les points de vente
GET /api/v1/clients/store/points — jeton client.
{
"success": true,
"data": {
"currency": "XOF",
"points": [
{
"point_id": "11111111-1111-4111-8111-111111111111",
"slot": 42,
"nas_id": "WIFI-ZONE",
"store_slug": "ma-boutique",
"router": { "identity": "Salle A", "serial": "ABC123XYZ", "model": "RB951Ui-2HnD" },
"tunnel": {
"status": "CONNECTED",
"transport": "SSTP",
"last_seen_at": "2026-09-14T10:00:00Z"
},
"purchase_link": "https://tpay.mikhmoai.com/api/v1/store/buy-ticketmomo?store=ma-boutique&point=11111111-1111-4111-8111-111111111111&nasid=$(server-name)&mac=$(mac)&ip=$(ip)&link-status=$(link-status-esc)",
"stock": { "total": 180, "by_profile": { "100F-5H": 30, "250F-1d": 30 } },
"sales": {
"today": { "revenue": 450, "count": 3 },
"this_week": { "revenue": 1200, "count": 8 },
"this_month": { "revenue": 5400, "count": 36 }
}
}
],
"unattributed_sales": {
"today": { "revenue": 0, "count": 0 },
"this_week": { "revenue": 0, "count": 0 },
"this_month": { "revenue": 700, "count": 5 }
}
}
}
| Champ | Sens |
|---|---|
points[] |
Un élément par routeur ACTIF du vendeur, trié par créneau. |
store_slug |
Boutique qui vend pour ce routeur : celle de sa licence, sinon la boutique principale (la plus ancienne). Deux routeurs peuvent partager la même boutique. |
purchase_link |
Lien d’achat prêt à poser dans le portail de CE routeur. Les $(…) sont des variables MikroTik : ne pas les encoder ni les remplacer. |
stock.by_profile |
Tickets disponibles dans le stock de CE routeur, par profil hotspot. |
sales |
Ventes payées attribuées à CE routeur. Fenêtres en heure d’Afrique de l’Ouest (UTC+1), semaine à partir du lundi. |
unattributed_sales |
Ventes du mois qu’aucune preuve ne rattache à un routeur actif (routeur retiré, vente ancienne). Afficher à part, ne pas les répartir. |
3. Tableau de bord d’un point de vente
GET /api/v1/clients/store/points/:pointId — jeton client.
Même objet qu’un élément de points[], plus currency et recent_sales (50 dernières ventes du
mois pour ce routeur, les plus récentes d’abord) :
"recent_sales": [
{ "id": "TM_0000000000", "amount": 250, "currency": "XOF", "profile": "250F-1d", "created_at": "2026-09-14T09:47:18Z" }
]
| Code HTTP | error.code |
Quand |
|---|---|---|
| 400 | POINT_ID_INVALID |
pointId n’est pas un UUID. |
| 404 | POINT_NOT_FOUND |
Routeur inactif, inexistant, ou appartenant à un autre client (on ne confirme pas son existence). |
4. Le lien d’achat : point
Base : https://tpay.mikhmoai.com/api/v1/store/buy-ticketmomo — la racine
https://tpay.mikhmoai.com/buy-ticketmomo répond 404.
GET /api/v1/store/buy-ticketmomo?nasid=$(server-name)&amount=100¤cy=cfa&profile_name=100F-5H&mac=$(mac)&ip=$(ip)&link-status=$(link-status-esc)&store=<slug>&point=<point_id>[&pub_key=pk_live_…]
Si amount ou profile_name manque et que store, client_id ou le suffixe -TM<n> de nasid permet d’identifier le vendeur, ce même lien affiche son catalogue de forfaits livrables. Chaque forfait ouvre ensuite le lien complet d’achat, avec les paramètres réseau effectivement reçus. Les macros MikroTik encore sous la forme $(...) ne sont jamais recopiées ; dans ce cas, la page invite l’acheteur à rouvrir le lien depuis le WiFi du vendeur.
Qui est le vendeur ? (ordre de priorité)
store(slug de boutique) ;- sinon
client_id; - sinon
pub_key(vendeur retrouvé par sa clé publique FedaPay enregistrée) ; - sinon
nasidseul, en dernier recours (nom non unique : à éviter).
store et client_id peuvent coexister : store l’emporte.
Révision 2026-09-16 — gabarit MikroTik : chaque portail généré pour une allocation ajoute désormais
point=<allocation_id>au lien d’achat et transmetnasid=$(server-name). Il ne transmet jamais$(identity), qui désigne le routeur et non le serveur Hotspot. Ainsi, même si un NAS est renommé, la vente reste épinglée au bon point de vente.
Les anciens portails qui envoient encore $(identity) restent pris en charge uniquement à
l’intérieur du vendeur déjà résolu et seulement si cette identité désigne une allocation unique.
Une identité partagée reste refusée : le serveur ne normalise jamais les espaces, tirets ou
suffixes pour « deviner » un routeur.
Quel routeur ? point
point= lepoint_idexact (UUID) renvoyé parGET /api/v1/clients/store/points. Pas de préfixe ni de format inventé (point_a1b2c3d4ne désigne rien et sera ignoré).- Avec un
pointvalide, le ticket est tiré du stock de CE routeur et la vente lui est attribuée ;nasidn’est plus utilisé pour choisir le routeur. pointdésigne un routeur à l’intérieur du vendeur déjà résolu. Unpointqui n’appartient pas à ce vendeur est ignoré (la vente continue, sans attribution) : il ne fait jamais changer de vendeur.- Sans
point, le serveur déduit le routeur denasid, et refuse de servir le stock d’un autre routeur quand il ne peut pas trancher (le ticket est alors créé directement sur le routeur). - Alias historiques acceptés :
equipementId,allocationId.
pub_key — qui encaisse
- Présente et valide (
pk_live_…/pk_sandbox_…) : le paiement est ouvert sur le compte FedaPay du marchand, sans frais plateforme. La livraison du ticket dépend alors du webhook FedaPay du marchand configuré chez nous : sans lui, le client paie et ne reçoit rien. - Absente ou mal formée : repli sur le compte TiketMOMO de la plateforme (frais standard), et le marchand est prévenu que sa clé est invalide.
pub_keyne remplace passtore/client_idpour identifier le vendeur quand ceux-ci sont présents.
Montant, durée
- Le prix est celui du profil dans la boutique : un
amountdifférent est corrigé par le serveur (anti-fraude). Le lien peut donc porteramount, mais il ne décide pas. timelimitest enregistré à titre indicatif ; la durée réelle est celle du profil hotspot du routeur.datalimitn’est pas lu.currency=cfaest accepté.
5. Webhooks et transactions
À l’ouverture du paiement, l’allocation retenue est écrite dans
transactions.metadata.canonical_allocation_id : c’est le point_id, et il suit la transaction
jusqu’au webhook de paiement puis à la livraison du ticket.
Attribution d’une vente à un point, par force de preuve :
- le stock d’où vient le ticket livré (
vouchers.site_id = tiketmomo_<point_id>) ; transactions.allocation_id;transactions.metadata.canonical_allocation_id.
Jamais nas_id.
6. Pousser des tickets depuis l’app
POST /api/v1/vouchers/sync range les tickets dans le stock du routeur désigné par
metadata.allocationId (= point_id), metadata.routerSerial ou metadata.nasId.
- Envoyer
metadata.allocationId: c’est la seule valeur sans ambiguïté. - Plusieurs routeurs actifs et aucun identifiant →
400 ROUTER_ID_REQUIRED. - Identité ambiguë →
400 ROUTER_IDENTITY_AMBIGUOUS; matériel d’un autre client →404.
7. À ne pas faire
- Identifier un routeur par
nas_idou par le nom de la boutique. - Réutiliser le
purchase_linkd’un routeur dans le portail d’un autre : les tickets seraient tirés du mauvais stock. - Additionner
unattributed_salesà un routeur.
8. Identité NAS gérée
Le service NasIdentityAllocationService attribue à chaque allocation routeur un NAS stable
au format TiketMOMO TM-1, TM-2, TM-3, etc. L’allocation est réservée dans nas_identity_registry avant toute
écriture distante ; le même identifiant est réutilisé lors des reprises et n’est jamais
réattribué à une autre allocation.
L’écriture sur MikroTik est idempotente et vérifiée : REST sur RouterOS 7, SSH borné sur
RouterOS 6. Le service n’écrit que si un seul serveur Hotspot est identifié ; plusieurs
serveurs, routeur non joignable ou conflit de réservation donnent un état d’échec explicite.
Le portail doit ensuite envoyer nasid=$(server-name) et point=<point_id>.
Quatre gardes empêchent les doublons : clé primaire sur nas_id, unicité de l’allocation,
unicité du compteur séquentiel et relecture idempotente avant insertion (avec gestion des
courses concurrentes). Une collision est refusée et ne change jamais de routeur.