Contrat API — Forfaits Starlink Priority (quota, lissage, recharges)
Pour les clients basculés de force sur un forfait Priority quantifié après une restriction Starlink.
[!IMPORTANT] J+SERVICES ne vend pas cet abonnement. Starlink le vend au client. Nous avons deux rôles, et deux seulement :
- l’aider à exploiter ce forfait au mieux — étaler le quota pour qu’il tienne le mois ;
- lui dire, chiffres en main, si son mois a été rentable.
Aucun montant de ce contrat n’est encaissé par nous. Le champ
facture_parvaut toujoursSTARLINK, et l’écran doit le répéter : un montant affiché sans cette mention laisse croire que le paiement se fait chez nous.
Base : https://live.jmoai.net/api/v1/radius
1. Ce que le serveur sait, et ce qu’il ne sait pas
[!WARNING] Nous n’avons aucune API vers Starlink. Ni pour lire le quota, ni pour acheter un bloc.
Trois nombres cohabitent, de trois natures différentes. L’application doit les distinguer à l’écran, sinon le gérant croira que nous lisons son compte Starlink :
| Nombre | Nature | D’où il vient |
|---|---|---|
forfait.base_quota_gb |
déclaré | saisi par le gérant |
consommation.cycle_gb |
mesuré par nous | notre relevé sur le WAN du routeur |
consommation.declare_gb |
relevé chez Starlink | saisi par le gérant depuis son compte |
Le champ consommation.source vaut toujours MESURE_JSERVICES. Affichez « estimation » à côté
de la jauge, jamais « consommation Starlink ».
Pourquoi les deux mesures diffèrent : nous comptons ce qui passe par le WAN du MikroTik,
Starlink compte ce qui passe par la parabole — donc aussi le trafic des appareils qui la
partagent sans passer par le routeur. consommation.ecart_gb expose la différence dès que le
gérant a saisi le chiffre de Starlink ; au bout de deux cycles, il connaît son facteur.
2. GET /starlink/priority/{nasId}/qos
{ "is_active": true, "base_quota_gb": 50, "billing_start_day": 9 }
Un NAS jamais configuré renvoie { "is_active": false, "base_quota_gb": null, "billing_start_day": 1 }.
3. PUT /starlink/priority/{nasId}/qos
{ "is_active": true, "base_quota_gb": 50, "billing_start_day": 9 }
{
"success": true,
"message": "Configuration IA mise à jour avec succès",
"is_active": true,
"base_quota_gb": 50,
"billing_start_day": 9
}
base_quota_gbest exigé même quandis_activeest faux : un forfait sans quota ne se pilote pas, et l’accepter produirait une activation impossible au moment où le gérant croira avoir tout réglé.- Désactiver rend la main immédiatement. Le plafond imposé est retiré et le réglage propre du gérant reprend au passage suivant — laisser le dernier débit posé laisserait ses clients bridés sans que rien ne l’explique.
| Code | Sens |
|---|---|
STARLINK_QUOTA_INVALID |
quota absent, nul ou aberrant |
STARLINK_BILLING_DAY_INVALID |
jour de facturation hors 1–31 |
MAC_NAS_NOT_OWNED |
ce NAS n’appartient pas à ce client |
4. GET /starlink/priority/{nasId} — le tableau de bord
C’est cet appel qui alimente la page. Il porte le cycle, la consommation, la projection et l’état du pilotage.
{
"configure": true,
"is_active": true,
"forfait": {
"base_quota_gb": 50,
"recharges_gb": 0,
"quota_total_gb": 50,
"billing_start_day": 9
},
"cycle": {
"debut": "2026-08-09",
"renouvellement": "2026-09-09",
"jours_total": 31,
"jours_restants": 10
},
"consommation": {
"source": "MESURE_JSERVICES",
"cycle_gb": 45,
"jour_gb": 0.4,
"restant_gb": 5,
"pourcentage": 90,
"declare_gb": null,
"ecart_gb": null
},
"projection": {
"rythme_gb_jour": 2.14,
"jours_tenables": 2,
"panne_seche_avant_renouvellement": true
},
"pilotage": {
"debit_impose_mbps": 1,
"dernier_calcul": "2026-08-29T14:07:00Z",
"detail": { "motif": "ALLOCATION_DU_JOUR_ATTEINTE", "ideal_jour_gb": 0.5 },
"moteur": "PRET"
},
"grille_recharge": [{ "gb": 50, "prix_xof": 9400 }, "…"]
}
projection est ce que le gérant vient chercher. « Au rythme actuel, épuisé dans 2 jours »
lui permet d’agir ; « 45 Go sur 50 » ne lui apprend rien qu’il ne voie dans son compte Starlink.
Mettez-la en tête de page, pas la jauge.
rythme_gb_jouretjours_tenablesvalentnulltant qu’un jour entier ne s’est pas écoulé dans le cycle : une projection bâtie sur trois heures dirait n’importe quoi, et serait crue.pilotage.moteurvautAUCUN_PROGRAMME_DE_BRIDAGEquand le routeur n’a pas de programme de maîtrise de consommation. Le lissage ne peut alors rien appliquer — proposez au gérant de créer son programme, sinon la page pilote dans le vide.
4 bis. Reconduction tacite — la règle à afficher au gérant
L’abonnement Starlink est le sien. Nous n’y avons aucun accès : s’il change de forfait ou ajoute des gigaoctets, nous ne l’apprenons que s’il nous le dit.
Sans mise à jour de sa part, le cycle repart à l’identique.
C’est la règle, et elle doit être écrite à l’écran. Le silence est une réponse — encore faut-il que le gérant le sache. Un quota remis à zéro ou à une valeur par défaut au renouvellement le priverait de son réglage sans prévenir, et le lissage briderait aussitôt.
Le tableau de bord porte le bloc :
"reconduction": {
"tacite": true,
"quota_reconduit_gb": 50,
"dernier_changement_le": "2026-08-09T12:00:00Z",
"derniere_reconduction_le": "2026-09-09T07:00:00Z",
"rappel_a_j_moins": 3
}
Les deux rappels
| Quand | Canal | Message |
|---|---|---|
| J-3 avant la fin du cycle | push + courriel | « Votre cycle se termine le 9. Si votre forfait a changé, mettez-le à jour — sans quoi il repartira à l’identique. » |
| au renouvellement | push + courriel | « Nouveau cycle démarré, forfait reconduit à 50 Go. » |
- J-3 et pas J-1 : assez tôt pour qu’il vérifie son compte Starlink et corrige avant le renouvellement, assez tard pour qu’il ait une idée de sa consommation du mois. Le prévenir le premier jour ne servirait à rien, il n’aurait rien à corriger.
- Une fois par cycle et par étape. La clé de dédoublonnage porte la date de début de cycle : le pilotage passe toutes les heures, une clé variable enverrait le rappel vingt-quatre fois par jour, une clé fixe par routeur ne le renverrait jamais au cycle suivant.
- Le push part toujours, le courriel seulement si le client a une adresse. Dans cet ordre :
le quota d’envoi de courriels est la ressource la plus rare du système et il a déjà été épuisé
deux fois cette année. Le courriel est envoyé en priorité
NORMAL— c’est un courrier client, pas un signalement d’exploitation, il ne doit pas se taire quand l’infrastructure fait du bruit. - Un appui sur la notification doit ouvrir l’écran de mise à jour du forfait, pas la page d’accueil : c’est l’action qu’on lui demande.
Le courriel rappelle en clair ce que nous ne pouvons pas faire : « votre abonnement Starlink reste le vôtre, nous ne pouvons ni le lire ni le modifier — nous étalons votre consommation pour éviter la panne sèche ». L’application devrait dire la même chose au même endroit.
5. Comment le lissage décide
Toutes les heures, pour chaque forfait actif :
reste = quota total − consommé sur le cycle
allocation/jour = reste ÷ jours restants
- consommation du jour < 90 % de l’allocation → on ne bride pas (
DANS_LES_CLOUS) ; - au-delà → le reliquat du jour est étalé sur les heures restantes de la journée ;
- quota épuisé → plancher de 1 Mbps, jamais zéro. On ralentit, on ne coupe jamais : un client qui a payé garde un accès, et il lui faut de quoi acheter une recharge.
Trois garde-fous que l’application doit connaître pour ne pas s’étonner :
- Le lissage ne dépasse jamais le plafond du gérant. Il ne fait que restreindre.
- Hystérésis de 20 % : un débit qui bouge de trois pourcents ne provoque aucune réécriture. Le routeur n’est pas retouché toutes les heures.
- Le lissage n’écrit rien directement sur le routeur : il pose un plafond effectif que le moteur de bridage applique à sa réconciliation, donc jusqu’à 5 minutes après le calcul. Un écran qui attend un effet immédiat affichera un décalage normal.
Au renouvellement, les recharges du cycle écoulé ne se reportent pas et le plafond est relâché — sans quoi le client resterait bridé le premier jour d’un cycle tout neuf.
Journal (GET /bandwidth/activity) : QUOTA_SMOOTHED, QUOTA_RELEASED, QUOTA_CYCLE_RESET,
QUOTA_TOPUP_DELIVERED.
6. Déclarer son forfait — POST /starlink/priority/{nasId}/upgrade
Le nom de la route est conservé pour l’application publiée. Ce n’est pas un achat : le gérant déclare le forfait Starlink sur lequel il se trouve.
{ "upgrade_gb": 50 }
{
"bloc_gb": 50,
"base_quota_gb": 50,
"cout_socle_xof": 15000,
"cout_bloc_xof": 9400,
"cout_total_xof": 24400,
"devise": "XOF",
"facture_par": "STARLINK",
"note": "Ce montant est celui que Starlink vous facture. J+SERVICES n’encaisse rien dessus."
}
Le socle (15 000 F) est mensuel et dû quel que soit le volume : c’est l’accès prioritaire
lui-même. Le compter par achat gonflerait son coût et lui ferait croire son mois déficitaire.
upgrade_gb: 0 est un forfait légitime — l’accès sans bloc de données.
Le quota, c’est le bloc, pas le total. Le socle n’apporte aucun gigaoctet ; l’ajouter au quota ferait distribuer des données qui n’existent pas.
GET /starlink/priority/grille
{
"devise": "XOF",
"facture_par": "STARLINK",
"socle_xof": 15000,
"socle_periodicite": "MENSUELLE",
"forfaits": [{ "gb": 50, "socle_xof": 15000, "bloc_xof": 9400, "total_xof": 24400 }, "…"]
}
| Forfait | Socle | Bloc | Total mensuel |
|---|---|---|---|
| 0 Go | 15 000 F | 0 F | 15 000 F |
| 50 Go | 15 000 F | 9 400 F | 24 400 F |
| 100 Go | 15 000 F | 18 800 F | 33 800 F |
| 500 Go | 15 000 F | 47 000 F | 62 000 F |
| 1 To | 15 000 F | 94 000 F | 109 000 F |
N’écrivez aucun montant en dur : cette route est la seule source.
6 bis. La rentabilité — GET /starlink/priority/{nasId}/rentabilite
Le second rôle. Une jauge de quota ne dit pas au gérant s’il gagne sa vie ; « 24 400 F de forfait, 61 000 F encaissés » le dit.
{
"configure": true,
"cycle": { "debut": "2026-08-09", "renouvellement": "2026-09-09", "jours_restants": 10 },
"forfait": { "bloc_gb": 50, "cout_total_xof": 24400, "facture_par": "STARLINK" },
"recettes": {
"source": "VENTES_ENREGISTREES",
"enregistrees_xof": 61000,
"declarees_xof": null,
"avertissement": "Seules les ventes passées par MikhmoAI sont comptées. Saisissez vos ventes en espèces pour un bilan juste."
},
"bilan": {
"cout_xof": 24400,
"recettes_xof": 61000,
"benefice_xof": 36600,
"marge_pct": 60,
"rentable": true,
"cout_par_go": 488,
"recette_par_go": 1220
}
}
Ce que ces chiffres valent
- Les recettes viennent de
sales, pas detransactions. Une transaction marquéeSUCCESSne prouve pas qu’un ticket a été délivré ;salesne reçoit une ligne que lorsqu’une vente a réellement eu lieu. C’est la différence entre un chiffre d’affaires et un espoir. marge_pctest calculée sur les recettes, pas sur le coût — c’est la part de ce qu’il encaisse qui lui reste. Rapportée au coût, elle dépasserait 100 % et ne voudrait plus rien dire.cout_par_gorévèle le forfait surdimensionné. Un gérant qui paie 1 To et n’en consomme que 200 Go paie son gigaoctet cinq fois trop cher — 545 F au lieu de 109 F. C’est le chiffre qui lui fait changer de forfait.- Un mois déficitaire est annoncé tel quel.
benefice_xofpeut être négatif ; affichez-le. Un tableau de bord qui ne sait dire que de bonnes nouvelles ne convainc personne.
bilan à null — et pourquoi ce n’est pas zéro
Nous ne voyons que les ventes passées par MikhmoAI. Un gérant qui encaisse au comptoir, ou dont
le hotspot est piloté depuis les scripts du routeur, apparaîtrait déficitaire alors qu’il
gagne de l’argent — et ce chiffre-là serait cru sur parole. recettes_xof, benefice_xof et
rentable valent donc null, jamais 0. Affichez recettes.avertissement et invitez à
saisir.
Deux populations : RADIUS MikhmoAI et Mikhmon local
Certains gérants vendent via notre RADIUS — leurs ventes arrivent dans sales. D’autres
font tourner Mikhmon en local sur le routeur : leurs ventes n’arrivent jamais chez nous.
Zéro ligne n’est donc pas zéro franc.
Le champ recettes.connu tranche, et recettes.motif dit pourquoi :
connu |
motif |
Sens |
|---|---|---|
true |
null |
nos registres ont vu les ventes du cycle |
true |
AUCUNE_VENTE_CE_CYCLE |
ce routeur vend d’habitude par nous, mais rien ce mois-ci — vrai zéro |
false |
VENTES_HORS_MIKHMOAI |
ce routeur ne nous a jamais envoyé de vente : Mikhmon local, ou comptoir |
false |
REGISTRE_INDISPONIBLE |
lecture en erreur, ne rien conclure |
La distinction se fait sur l’historique du routeur, pas sur le seul cycle : c’est le seul signal disponible sans interroger le routeur.
PUT /starlink/priority/{nasId}/recettes
{ "recettes_xof": 61000, "source": "MIKHMON_LOCAL" }
source vaut MIKHMON_LOCAL (lu par l’application sur le routeur), SAISIE_MANUELLE ou
AUTRE. Le tracer évite qu’on se demande plus tard si un chiffre a été mesuré ou estimé.
Le montant est rattaché au cycle en cours. Celui du mois dernier ne resservira pas : il afficherait un bénéfice qui n’existe plus, et le gérant le croirait.
Les recettes déclarées remplacent les recettes enregistrées, elles ne s’y ajoutent pas :
additionner deux registres compterait deux fois les ventes déjà passées par nous. Quand le
gérant saisit un montant, source passe à DECLAREES_PAR_LE_GERANT — dites-le à l’écran, il
doit savoir quel chiffre il regarde.
7. Ce qu’il reste à brancher
Le rattachement au hub de paiement (transaction_id, webhook, passage à PAYE) et l’écran
d’exécution des recharges ne sont pas dans cette livraison. Tant qu’ils manquent, une
commande reste en EN_ATTENTE_PAIEMENT : c’est volontaire, et c’est visible.