Documentation J+SERVICES Guides Référence API

Contrat API — Équipements du client, accès à distance

Pour l’équipe application mobile. Comment permettre à un client d’ajouter ses points d’accès, antennes, CPE, caméras et switches, puis d’ouvrir leur interface web depuis n’importe où — à travers le tunnel, sans ouvrir un seul port public.

Base : https://live.jmoai.net/api/v1/clients/topology


1. Ce qui existe déjà, et qu’il ne faut pas refaire

Tout le chemin réseau est construit, déployé et prouvé de bout en bout :

brique état
balayage du LAN, tri du bruit livré
réservation d’un port privé par équipement livré
règle dst-nat posée sur le routeur livré (posée par le backend)
relais 443 authentifié, isolation par locataire livré
vérification et plan de réparation livré
épinglage du bail DHCP en statique livré
sonde de surveillance posée sur le routeur livré (posée par le backend)
notification push à la chute et au retour livré

Ce qui manquait n’était pas du réseau : c’était l’entrée. Au 23/08, un seul équipement était déclaré sur tout le parc en un mois. Depuis, le backend pose la règle lui-même — il ne reste plus qu’un écran à brancher.


2. Le problème que l’utilisateur doit résoudre — et l’outil qui le résout

Un routeur hotspot voit des centaines d’adresses. Sur un routeur réel du parc, le balayage brut remonte 493 adresses. Le client, lui, ne veut ajouter qu’un point d’accès.

Ne pas lui montrer la liste brute. Le tri est déjà fait côté backend :

GET /api/v1/clients/topology/scan?router_serial=HH40AF7YVNP
{
  "discovered": [
    {
      "lan_ip": "10.100.4.1",
      "interface": "ether1-TP-LinkGiga",
      "mac": "3C:6A:D2:7C:11:18",
      "hostname": "TendaAP",
      "already_added": false
    }
  ],
  "hotspot_clients_hidden": 492
}

493 → 1. Sur ce routeur, l’équipement à ajouter est le seul de la liste.

Comment le tri fonctionne, pour que l’écran sache quoi dire

Le backend croise quatre lectures du routeur — table ARP, baux DHCP, adresses du routeur, serveurs hotspot — puis écarte :

Ce qui reste est enrichi du nom d’hôte DHCP (hostname) et de la MAC. C’est ce nom qui permet à l’utilisateur de reconnaître son matériel sans connaître les adresses IP — affichez-le en premier, l’adresse en second.

La marque, l’application n’a rien à envoyer

Chaque ligne du balayage porte déjà :

{
  "lan_ip": "10.100.4.1",
  "mac": "3C:6A:D2:7C:11:18",
  "vendor": "TP-Link",
  "vendor_logo": "tp-link",
  "vendor_reason": null
}

vendor est déduit de la MAC — ses trois premiers octets sont attribués au constructeur par l’IEEE. Demander la marque à l’utilisateur reviendrait à lui demander de savoir : il se tromperait, ou laisserait vide. vendor_logo est l’identifiant stable auquel accrocher le SVG (tp-link, ubiquiti, cudy, mikrotik, hikvision…) — séparé du nom affiché, pour que renommer l’un ne casse pas l’autre.

Le même champ est présent sur chaque équipement de la topologie (§5).

⚠️ vendor: null arrive souvent, et ce n’est pas un défaut

vendor_reason ce que ça veut dire
MAC_ALEATOIRE MAC tirée au hasard par le téléphone — aucune marque n’existe
CONSTRUCTEUR_INCONNU préfixe hors de notre table
MAC_ABSENTE pas de MAC relevée

Les téléphones modernes changent de MAC par réseau pour ne pas être suivis. Sur le parc mesuré, plusieurs préfixes sont dans ce cas. Leur chercher une marque produirait une correspondance fausse, puisque le préfixe n’appartient à personne.

Afficher une icône générique. Jamais un logo approchant : un mauvais logo fait douter de tout le reste de l’écran.

already_added: true signale un équipement déjà enregistré : le griser plutôt que de laisser l’utilisateur créer un doublon.

?all=true rend la liste brute. À réserver au diagnostic — ce n’est pas un écran client.


3. Ajouter un équipement

POST /api/v1/clients/topology/devices
Authorization: Bearer <jeton du client>

{
  "router_serial": "HH40AF7YVNP",
  "name": "AP Salle 2",
  "device_type": "AP",
  "lan_ip": "10.100.4.1",
  "service_protocol": "http",
  "service_port": 80,
  "mac_address": "3C:6A:D2:7C:11:18"
}

router_serial suffit — l’application n’a pas besoin de connaître l’allocation_id. Le backend résout le numéro de série vers son propriétaire actuel et refuse si le registre et l’allocation divergent. Un numéro inconnu et le numéro d’un tiers rendent la même erreur 404 ROUTER_NOT_OWNED : la réponse ne dit jamais si le matériel existe ailleurs.

Envoyez la mac_address du balayage. Elle sert à retrouver l’équipement le jour où son adresse change, et c’est elle qui rend la réparation automatique possible.

La réponse, dans le cas normal

{
  "id": "b6347768-…",
  "status": "ACTIVE",
  "access_endpoint": {
    "dstnat_port": 34001,
    "nat_managed_by": "backend",
    "proxy_path": "/api/v1/clients/topology/access/b6347768-…"
  },
  "nat_rule": {
    "_": "rendu quand même : c'est le plan de secours si le routeur était injoignable",
    "mikrotik_commands": [
      "/ip dhcp-server lease make-static [find where address=\"10.100.4.1\"]",
      "/ip firewall nat remove [find comment=TOPO_b6347768]",
      "/ip firewall nat add chain=dstnat dst-address=10.255.0.15 protocol=tcp dst-port=34001 action=dst-nat to-addresses=10.100.4.1 to-ports=80 comment=TOPO_b6347768"
    ],
    "remove_command": "/ip firewall nat remove [find comment=TOPO_b6347768]"
  }
}

Depuis le 23/08, le backend met TOUT en service lui-même quand il joint le routeur :

  1. la règle d’accès à distance sur le routeur ;
  2. la sonde de surveillance sur le port de service de l’équipement ;
  3. un push au client — ✅ <nom> ajouté · Accès à distance et surveillance en service.

L’utilisateur nomme son équipement, et il apparaît. L’application n’a rien à exécuter, rien à enchaîner, et aucun appel supplémentaire pour activer la surveillance — c’était jusqu’ici un appel séparé que personne n’effectuait.

La réponse le dit sans ambiguïté :

status nat_managed_by ce qui s’est passé ce que l’application fait
ACTIVE backend la règle est sur le routeur rien, afficher « Ouvrir »
PENDING app routeur injoignable à cet instant exécuter nat_rule.mikrotik_commands si elle est sur le LAN, sinon laisser la vérification reprendre plus tard

Un routeur hors ligne ne fait pas échouer l’ajout : l’équipement est enregistré, la place est réservée, et la saisie de l’utilisateur n’est pas perdue pour une raison qui n’a rien à voir avec lui.

pushNat: false dans le corps force l’ancien comportement — au cas où l’application préfère poser depuis le LAN. Ce n’est plus nécessaire pour que la fonctionnalité marche.

⚠️ La première commande n’est pas optionnelle

make-static épingle le bail DHCP de l’équipement. Sans elle, un point d’accès en adresse dynamique change d’IP au premier redémarrage, la règle dst-nat pointe dans le vide, et l’accès casse sans aucun message. La commande est idempotente et sans effet sur un équipement déjà en adresse fixe.


4. Confirmer que l’accès existe vraiment

POST /api/v1/clients/topology/devices/:id/verify

À appeler seulement si l’application a posé la règle elle-même (status: "PENDING"). C’est ce qui fait alors passer l’équipement en ACTIVE. Utile aussi comme diagnostic à tout moment : c’est le seul appel qui va vraiment LIRE le routeur.

{
  "ok": true,
  "reason": null,
  "checks": {
    "router_reachable": true,
    "nat_rule_present": true,
    "nat_rule_enabled": true,
    "nat_rule_matches": true,
    "lease_static": true,
    "mac_matches_ip": true
  },
  "repair": { "repairable": false }
}

⚠️ ACTIVE veut dire « le routeur porte la règle », pas « c’est enregistré »

Un équipement créé est PENDING : la place est réservée, le routeur n’en sait rien encore. Seule la vérification, qui va lire la règle sur le routeur, promeut en ACTIVE.

Constaté le 23/08 avant correction : deux équipements ACTIVE en base, zéro règle sur le routeur. L’écran aurait montré deux accès prêts qui échouaient au clic.

N’affichez un bouton « Ouvrir » que sur un équipement ACTIVE.

Réparer en un appel

POST /api/v1/clients/topology/devices/:deviceId/repair

Un seul appel : le backend diagnostique, pose ce qui manque sur le routeur, puis revérifie. « J’ai lancé la réparation » n’est pas « c’est réparé ».

{
  "repaired": true,
  "reason_before": "NAT_RULE_MISSING",
  "reason_after": null,
  "status": "ACTIVE",
  "verify": { "ok": true, "…": "…" }
}

Trois réponses possibles, et l’écran doit les distinguer :

réponse sens ce que l’écran affiche
already_healthy: true rien à réparer « tout va bien » — ne pas proposer le bouton
repaired: true c’est réparé, et revérifié « accès rétabli »
needs_human: true la correction demande une décision le champ note, tel quel

⚠️ needs_human n’est pas un échec

Le cas type est DEVICE_IP_CHANGED : l’équipement ne vit plus à l’adresse enregistrée. Le backend pourrait poser une règle vers la nouvelle adresse — mais rien ne prouve que la machine trouvée là est celle que l’utilisateur avait choisie. Réparer, ici, ce serait ouvrir un accès vers un appareil que personne n’a désigné.

La note est écrite pour être affichée sans reformulation. Exemple réel :

L’équipement répond maintenant sur 10.100.90.125. Faites confirmer la nouvelle adresse à l’utilisateur, puis ré-enregistrez-le : rejouer les commandes d’origine recréerait la redirection vers l’ancienne adresse.

L’appel rend 200 dans les trois cas. Ce n’est pas une erreur d’appel, c’est un diagnostic — et l’écran a de quoi expliquer plutôt que d’afficher « échec ».

Les motifs, et ce que l’écran doit en faire

reason sens action proposée
NAT_RULE_MISSING la commande n’a pas été exécutée appeler /repair
NAT_RULE_DISABLED règle présente mais désactivée appeler /repair
NAT_RULE_MISMATCH la règle vise une autre adresse ou un autre port appeler /repair
DEVICE_IP_CHANGED l’équipement vit à une autre adresse afficher note, puis ré-enregistrer
DEVICE_NOT_SEEN plus vu dans la table ARP équipement éteint ou débranché
ROUTER_UNREACHABLE tunnel tombé ne rien conclure sur l’équipement
ROUTER_CHANGED_OWNER · ROUTER_REALLOCATED le routeur a changé de main revalidation nécessaire

repair.repairable: false n’est pas un échec : c’est le cas où la correction demande une décision humaine. Un changement d’adresse en fait partie — le backend ne réécrit pas tout seul une donnée que l’utilisateur a saisie.

⚠️ Un routeur injoignable ne rétrograde rien. Une coupure de trois minutes ferait sinon basculer toute une installation en erreur. « Je n’ai pas pu regarder » n’est pas « c’est cassé ». N’affichez pas d’alarme sur ROUTER_UNREACHABLE.


5. Où l’utilisateur les retrouve

Depuis le 23/08, les équipements apparaissent dans la topologie RADIUS, sous leur routeur. Un seul appel suffit à peindre l’écran complet :

GET /api/v1/radius/topology
{
  "primary": {
    "nasname": "10.255.0.15",
    "shortname": "J+Pro Ax3",
    "model": "hAP ax^3",
    "status": "ACTIVE",
    "tunnel_status": "CONNECTED",
    "web_access": {
      "webfig": "http://vpn.mikhmoai.com:17012",
      "winbox": "vpn.mikhmoai.com:19012",
      "api": "vpn.mikhmoai.com:22012",
      "ssh": "vpn.mikhmoai.com:23012",
      "reachable": true
    },
    "devices": [
      {
        "id": "c58f77aa-…",
        "name": "Ubiquiti Lite AP 120",
        "type": "AP",
        "lan_ip": "10.100.88.16",
        "mac_address": "24:5A:4C:B2:AB:53",
        "vendor": "Ubiquiti",
        "vendor_logo": "ubiquiti",
        "protocol": "https",
        "port": 443,
        "status": "ACTIVE",
        "pending_reason": null,
        "online": true,
        "online_changed_at": "2026-08-23T21:38:42Z",
        "monitored": true,
        "access_path": "/api/v1/clients/topology/access/c58f77aa-…",
        "access_session_path": "/api/v1/clients/topology/devices/c58f77aa-…/access-session"
      }
    ]
  },
  "members": [],
  "warnings": [],
  "capabilities": {
    "can_add_devices": true,
    "reason": null,
    "enforced": false,
    "max_devices": 10,
    "devices_used": 2,
    "devices_remaining": 8
  }
}

capabilities — l’accès aux équipements est un abonnement à part

L’accès et la surveillance des équipements du LAN forment un abonnement mensuel vendu séparément (suivi_pro). Il ne vient ni avec la licence RADIUS, ni avec le VPN.

champ à quoi il sert
can_add_devices le client a-t-il l’abonnement ? null = droits illisibles
reason SUIVI_PRO_REQUIRED quand il ne l’a pas
enforced la restriction est-elle RÉELLEMENT appliquée aujourd’hui ?
max_devices plafond du palier souscrit
devices_used équipements déjà déclarés
devices_remaining ce qu’il reste avant le refus

Lire les deux champs, jamais un seul. Tant que enforced vaut false, l’appel d’ajout passe quand même — la restriction est en observation. Griser le bouton sur le seul can_add_devices cacherait une fonction qui marche.

enforced: false                          → bouton actif
enforced: true  + can_add_devices: false → bouton grisé + proposer l'abonnement
can_add_devices: null                    → bouton actif (on ne prive pas sur un doute)

Un refus au clic ne vend rien. Un bouton grisé qui explique pourquoi devient une proposition.

Les deux paliers

palier prix équipements
Essentiel 3 100 XOF / mois 10
Étendu 10 000 XOF / mois 30

Afficher le compteur en permanence, pas seulement le blocage. « 7 sur 10 » prévient ; « limite atteinte » ne fait que constater. C’est devices_used / max_devices.

Au-delà du plafond, l’ajout rend :

402 Payment Required
{ "code": "DEVICE_QUOTA_REACHED",
  "error": "Votre abonnement couvre 10 équipements et vous en avez 10. Passez au palier supérieur pour en ajouter davantage.",
  "details": { "max_devices": 10, "devices_used": 10 } }

402 et non 403 : ce n’est pas un droit manquant, c’est un palier atteint — deux situations qui n’appellent pas le même écran. Le message porte les deux nombres et la sortie ; l’afficher tel quel suffit.

web_access concerne le routeur lui-même : ces quatre services existent nativement sur tout MikroTik, les ports sont déjà alloués et servis. reachable suit l’état du tunnel — grisez le bouton quand il est faux plutôt que d’ouvrir une page morte.

⚠️ Le schéma de l’URL WebFig n’est pas toujours https

Le port public est un flux TCP brut, sans terminaison TLS côté serveur. Le schéma est déjà calculé par le backend à partir du port réel du routeur (80 → http, 8081 → https). Ouvrez l’URL telle qu’elle est renvoyée. La forcer en https produit une page noire — le bug a déjà été vécu côté Desktop.

devices[] concerne ce qu’il y a derrière le routeur.

Deux états à ne surtout pas confondre

champ ce qu’il dit
status l’ACCÈS est-il en place ? ACTIVE = la règle est sur le routeur
online l’ÉQUIPEMENT répond-il ? mesuré par la sonde, sur son port

Un équipement peut être ACTIVE et online: false : l’accès est prêt, mais l’appareil est éteint. C’est le cas le plus fréquent, et l’écran doit le dire ainsi — « accès configuré, appareil hors ligne », jamais « erreur ».

⚠️ online: null veut dire « pas surveillé », pas « hors ligne »

Personne n’a encore regardé. À afficher en gris, jamais en rouge. Un point rouge sur un équipement qui fonctionne parfaitement fait perdre confiance dans tous les autres points rouges.

online_changed_at porte l’instant du dernier changement : c’est ce qui permet d’afficher « hors ligne depuis 2 h 15 » sur la fiche, sans attendre une notification.


6. Ouvrir l’interface web d’un équipement

GET /api/v1/clients/topology/access/:deviceId
Authorization: Bearer <jeton du client>

C’est un relais, pas une redirection. La requête traverse : backend → tunnel du routeur → règle dst-nat → équipement. Le locataire est revérifié à chaque appel.

Le bouton « Ouvrir » — deux appels, pas un

access_path exige l’en-tête d’authentification, qu’un navigateur ou une WebView ne portera pas. Pour ouvrir l’équipement, demandez d’abord une URL signée :

POST /api/v1/clients/topology/devices/:deviceId/access-session
{
  "access_url": "https://dev-<id>.topo.jmoai.net/?st=<signature>",
  "access_path": "/api/v1/clients/topology/access/<id>?st=<signature>",
  "ttl_seconds": 300
}

Ouvrez access_url : c’est le sous-domaine, qui sert l’interface complète de l’équipement (les images, le style et les scripts se chargent, ce qui n’est pas le cas d’un accès par chemin). La topologie donne ce chemin dans access_session_path — pas besoin de le composer.

Demandez le lien au CLIC, jamais à l’affichage de la liste. Il vaut 5 minutes. En émettre un par équipement à chaque lecture de la topologie sèmerait des URL signées dans les journaux et les caches pour un seul clic réel.

Pourquoi pas vpn.mikhmoai.com:PORT, comme pour le routeur ?

Les quatre services du routeur (WebFig, Winbox, API, SSH) passent par un flux TCP nginx sur un port public. C’est délibérément l’inverse pour les équipements du client, et pour trois raisons :

Le prix à payer est l’URL signée à durée de vie courte. C’est ce qui permet de garder zéro port public pour le matériel des clients.

N’exposez jamais tunnel_ip:dstnat_port dans l’application. C’est une adresse interne ; la publier contournerait le contrôle d’accès, et quiconque l’aurait vue atteindrait l’équipement.

Aucun port public n’est ouvert pour cette fonctionnalité. C’est le point de conception : un équipement domestique — un AP, une caméra — n’est jamais joignable depuis Internet.


7. Les notifications que l’application recevra

Le client n’a rien à configurer — ni Telegram, ni webhook, ni seuil. La surveillance part avec l’ajout, et les notifications arrivent en push et dans la liste in-app.

data.type quand exemple
EQUIPMENT_ADDED l’équipement vient d’être mis en service ✅ Ubiquiti Lite AP 120 ajouté
EQUIPMENT_MONITOR l’équipement tombe 🔴 Ubiquiti Lite AP 120 hors ligne
EQUIPMENT_MONITOR il est toujours tombé (15 min, 2 h) 🔴 … toujours hors ligne · est OFF depuis 17 min
EQUIPMENT_MONITOR il revient ✅ … de nouveau en ligne après 12 min d'interruption

Chaque message porte device_id : ouvrez la fiche de l’équipement au tap, pas la liste.

Ce que l’application ne doit PAS reconstruire

La durée. Elle est déjà dans le texte, et elle est mesurée, pas estimée. Au retour, le backend connaît l’instant exact de la chute. À la chute, il ne sait pas depuis combien de temps l’appareil est muet — la sonde vient de le constater — et il ne l’invente donc pas. Recalculer une durée côté application produirait un chiffre différent de celui du push, pour le même incident.

La relance. Deux paliers seulement, 15 minutes puis 2 heures, puis plus rien. Ce n’est pas un oubli : un rappel toutes les dix minutes sur un équipement débranché pour l’hiver ferait couper toutes les notifications par le client — y compris celles qui comptent. Ne pas y ajouter de rappel local.

La déduplication. Elle est faite côté serveur, par transition. Deux pushs identiques à une seconde d’intervalle ne remonteront pas.


8. Les autres appels

appel usage
GET /topology/devices la liste du client, avec vpn_health et hardware{anchored, needs_revalidation, reason}
POST /topology/sync déclarer plusieurs équipements en une fois, après un balayage
DELETE /topology/devices/:id retire l’équipement et rend la remove_command à exécuter
POST /topology/devices/:id/monitor surveillance netwatch sur le routeur

hardware.needs_revalidation signale qu’un routeur a changé de propriétaire ou d’allocation depuis l’enregistrement. Un routeur se vend ; les points d’accès, eux, restent sur le site. Un équipement dans cet état ne doit plus offrir d’accès tant que l’utilisateur n’a pas reconfirmé.


9. Le parcours complet, dans l’ordre

  1. GET /topology/scan — l’utilisateur voit une liste courte, pas 493 lignes.
  2. Il choisit un équipement et le nomme (« Ubiquiti Lite AP 120 »).
  3. POST /topology/devices — le backend réserve le port, pose la règle d’accès, pose la sonde, et envoie le push ✅ … ajouté.
  4. Il apparaît sous son routeur dans la topologie, avec status ET online.
  5. Un bouton Ouvrir demande l’URL signée puis l’ouvre.
  6. À la moindre chute, le client reçoit un push. Sans avoir rien configuré.

Pour l’utilisateur, c’est un seul geste : il nomme son équipement, il apparaît.

Les deux étapes de secours, quand le routeur n’était pas joignable à cet instant (status: "PENDING") : exécuter nat_rule.mikrotik_commands, puis appeler POST /topology/devices/:id/verify.

Mesuré de bout en bout le 23/08

balayage            493 adresses → 492 masquées → 1 équipement
ajout               règle posée par le backend, status ACTIVE
règle sur routeur   TOPO_99824df5 chain=dstnat dst-address=10.255.0.15
                    dst-port=34001 to-addresses=10.100.4.1 to-ports=80
accès               HTTP 200 en 0,65 s

10. Les pièges déjà payés

N’affichez jamais la liste brute du balayage. 493 lignes dont 492 passants : l’utilisateur ne trouvera pas son point d’accès, et il conclura que la fonctionnalité ne marche pas.

N’affichez pas « Ouvrir » sur un équipement PENDING. Le clic échouera, et rien à l’écran n’expliquera pourquoi. Affichez le motif et le bouton « Réparer ».

Ne peignez pas online: null en rouge. C’est « pas surveillé », pas « hors ligne ». Un point rouge sur un équipement qui marche fait perdre confiance dans tous les autres.

Ne confondez pas status et online. Un équipement ACTIVE avec online: false n’est pas en erreur : son accès est prêt et l’appareil est éteint. Le dire comme une erreur amène l’utilisateur à « réparer » ce qui n’est pas cassé.

N’oubliez pas verify quand VOUS avez posé la règle. Dans le cas PENDING, sans cet appel l’équipement reste en attente pour toujours, même quand l’accès fonctionne parfaitement. Quand le backend a posé la règle (nat_managed_by: "backend"), l’équipement est déjà ACTIVE — l’appel est inutile.