Documentation J+SERVICES Guides Référence API

Contrat API — Pro Hub (Hub des techniciens)

Version 1.0 — 2026-08-21. Ce document décrit ce qui est réellement déployé, et signale les endroits où l’implémentation s’écarte du contrat initial, avec la raison.

Qui consomme quoi. Le Pro Hub — annuaire, progression, quota, supervision, portefeuille — est servi à l’application web app.mikhmoai.com. L’application mobile reste sur le parcours client et n’embarque pas ces écrans, pour ne pas s’alourdir. Même API, deux surfaces.

Un client navigateur est soumis à un contrôle d’Origin au WebSocket dont une application native est dispensée : voir la note d’intégration web.


1. Annuaire des professionnels

GET /api/v1/support/professionals

Public, sans authentification. Ne renvoie que les experts actifs ET certifiés — le filtre est appliqué côté serveur et n’est pas contournable : figurer dans cette liste est une recommandation implicite de notre part.

Paramètres

Nom Valeurs Effet
status online, offline, busy Toute autre valeur → 400 INVALID_STATUS
country Code ISO 2 lettres (BJ, CI…) Insensible à la casse
tags Liste séparée par des virgules Correspondance si au moins un tag est commun
page Entier ≥ 1 Défaut 1
limit Entier 1–100 Défaut 20

Réponse 200

{
  "success": true,
  "data": [
    {
      "id": "7b1ed0ba-…",
      "name": "Juste Moailte",
      "avatar": null,
      "rating": 5,
      "isOnline": false,
      "status_detail": "OFFLINE",
      "country": "BJ",
      "ticketsResolved": 0,
      "tags": ["MikroTik", "VPN"],
      "price": null,
      "currency": "XOF",
      "languages": ["fr"]
    }
  ],
  "meta": { "total": 1, "page": 1, "limit": 20 }
}

price: null signifie « sur devis », pas « gratuit ». L’app doit afficher une mention, jamais 0. Plusieurs experts ont répondu « Variable » au formulaire de candidature : inventer un nombre à leur place l’afficherait comme un engagement de leur part.

status_detail vaut OFFLINE dès que isOnline est faux, quel que soit l’état stocké. Un expert hors ligne n’est ni disponible ni occupé : il est absent. Renvoyer AVAILABLE ferait cliquer le client sur quelqu’un qui ne répondra pas.

GET /api/v1/support/professionals/:id

Même forme, complétée de bio, tier_level, votes et reviews (les 10 derniers avis). 404 PROFESSIONAL_NOT_FOUND si l’expert n’existe pas ou n’est pas actif.


2. Présence

POST /api/v1/support/presence · authentifié

Battement de cœur du technicien. Corps : { "online": true } (défaut) ou { "online": false } pour une sortie propre.

À envoyer au moins toutes les 60 secondes. Sans nouvelle pendant 90 s, la session expire d’elle-même : un technicien qui tue son application ne reste pas « en ligne » indéfiniment.

Réponse : { "success": true, "data": { "isOnline": true, "status_detail": "AVAILABLE" } }

Événements temps réel

Publiés via Centrifugo, sur le canal support:presence :

{
  "type": "PRO_STATUS_CHANGED",
  "payload": {
    "expert_id": "…",
    "isOnline": true,
    "status_detail": "BUSY",
    "updated_at": "2026-08-21T18:00:00Z"
  }
}
{ "type": "TICKET_CLAIMED", "payload": { "ticket_id": "…", "expert_id": "…", "updated_at": "…" } }

Et sur support:missions :

{
  "type": "NEW_BOUNTY_TICKET",
  "payload": {
    "ticket_id": "…",
    "subject": "…",
    "category": "…",
    "bounty_amount": 15000,
    "currency": "XOF",
    "is_urgent": true,
    "created_at": "…"
  }
}

status_detail est calculé, jamais déclaré. Il passe à BUSY quand le technicien porte au moins un ticket urgent non résolu, et revient à AVAILABLE à la clôture — après relecture de tous ses tickets ouverts, car il peut lui en rester un autre.


2 bis. Réputation et progression par paliers

Champs ajoutés à la fiche d’un professionnel

Champ Sens
tier_level NOVICE, PRO, EXPERT, INGENIEUR
reputation_score 0-100, calculé — c’est lui qui ordonne la liste
rating Moyenne des étoiles. null = jamais noté, pas « zéro »
success_rate % de missions menées au bout. null = aucune mission prise

null n’est pas 0. Un technicien sans avis n’a pas une mauvaise note : il n’en a aucune. Affiche « Nouveau » ou « Pas encore noté », jamais une note nulle ni des étoiles vides — ce serait le condamner avant qu’il ait commencé. Les colonnes portaient jusqu’au 2026-08-21 des défauts à 5.0 et 100 % : tout profil neuf naissait parfait, ce qui était un mensonge exactement en sens inverse.

La note affichée n’est pas la note qui classe

Le classement se fait sur reputation_score, une moyenne bayésienne : tant que le volume d’avis est faible, le score est tiré vers la moyenne de la plateforme. Un unique avis à 5 étoiles ne fait donc pas passer devant deux cents avis à 4,8. Mesuré : un technicien avec un seul avis à 5 affiche rating: 5 mais ne score que 82.

S’y ajoutent une décroissance dans le temps (demi-vie six mois — le travail récent pèse plus) et un plafonnement par client (le 2ᵉ avis d’un même client compte pour moitié, le 3ᵉ pour un tiers : deux complices ne suffisent pas à fabriquer une réputation).

GET /api/v1/support/progression · authentifié

Palier actuel, palier suivant, et ce qu’il reste à faire :

{
  "current_tier": "PRO",
  "next_tier": "EXPERT",
  "facts": { "free_missions": 10, "total_missions": 10, "rating": 4.71, "success_rate": 100 },
  "requirements": { "free_missions": 20, "total_missions": 60, "rating": 4.5, "success_rate": 90 },
  "remaining": { "free_missions": 10, "total_missions": 50, "rating": 0, "success_rate": 0 }
}

Les paliers

Passage Missions gratuites Missions totales Note Réussite
NOVICE → PRO 10 10 4,2 80 %
PRO → EXPERT 20 60 4,5 90 %
EXPERT → INGENIEUR 50 200 4,8 95 %

Le socle est le nombre de missions gratuites : on ne monte pas en grade en encaissant. C’est une période probatoire assumée, et c’est aussi ce qui permet à un nouveau venu de se construire un dossier.

Un palier se perd aussi. Réévaluation quotidienne dans les deux sens, avec une zone tampon (0,3 point de note, 10 points de réussite) et un minimum de 5 missions : on ne rétrograde pas sur un mauvais jour. Un niveau qu’on ne peut jamais perdre cesse d’être un signal.

Rémunération dégressive — le débutant gagne tout de suite

Niveau Part technicien Commission plateforme
NOVICE 60 % 40 %
PRO 75 % 25 %
EXPERT 85 % 15 %
INGENIEUR 90 % 10 %

Le choix est délibéré : on n’attend pas d’être PRO pour gagner. Un porteur de certification est déjà en poste, son coût d’opportunité est élevé — il prendra une mission par curiosité, jamais deux. La vraie offre vient des débutants, qui ont du temps et tout à gagner. Réserver le revenu aux niveaux hauts assèche la plateforme.

Le dépannage basique est donc ouvert dès NOVICE. Le gating par niveau ne subsiste que là où l’incompétence fait des dégâts : un audit firewall raté ouvre un réseau, un dépannage raté fait perdre une heure.

La part est appliquée au règlement, sans que l’appelant ait à la connaître. revenue_share et next_revenue_share sont exposés dans /support/progression : voir sa rémunération monter est un moteur plus parlant qu’un écusson.

Quota de contribution gratuite — permanent, jamais nul

Niveau Quota, sur 90 jours glissants
NOVICE 1 gratuite pour 1 rémunérée
PRO 1 pour 4
EXPERT 1 pour 6
INGENIEUR 1 pour 10

Si chacun finissait par ne prendre que du rémunéré, la file gratuite ne serait plus servie — et surtout la première marche des débutants disparaîtrait. L’obligation est donc permanente, allégée en montant mais jamais nulle, sur le modèle du pro bono des ordres professionnels.

Le quota ne BLOQUE rien. Bloquer un technicien au moment où il allait gagner, c’est un technicien qui part. Il conditionne le maintien du niveau : qui néglige le gratuit redescend doucement et perd sa commission avantageuse, pas son accès. La notification dit explicitement le motif — une rétrogradation qu’on ne comprend pas est vécue comme arbitraire.

Deux garde-fous : la fenêtre est glissante (sinon on ferait ses missions gratuites une fois pour toutes), et si la file gratuite est vide, l’obligation est suspendue — on ne rétrograde pas quelqu’un pour n’avoir pas fait des missions qui n’existaient pas.

POST /api/v1/support/tickets/:id/supervise · EXPERT et au-dessus

Un senior relit le travail d’un débutant. Corps : { "verdict": "VALIDE" | "A_CORRIGER", "comment": "…" }.

Double usage assumé : c’est la preuve de compétence du junior — bien moins coûteuse à organiser qu’un examen — et la façon dont un EXPERT s’acquitte de son quota gratuit. Le faire répondre à une question de base serait du gâchis ; le faire valider un travail est ce qu’il est seul à pouvoir faire.

403 TIER_TOO_LOW en dessous d’EXPERT, 409 ALREADY_SUPERVISED si la mission l’a déjà été, 400 SELF_SUPERVISION — on ne s’auto-délivre pas un quota.

Contact direct assumé — et sa contrepartie

Le numéro WhatsApp est dans le contrat, avec un lien prêt à cliquer :

"whatsapp": "0166481845",
"whatsapp_url": "https://wa.me/2290166481845"

Dans ce marché, le clic WhatsApp est la norme : forcer l’échange dans un chat interne ferait fuir techniciens comme clients. On n’empêche donc pas le travail hors plateforme — on le rend intenable.

⚠️ Utilise whatsapp_url, pas whatsapp. Deux formats cohabitent en base depuis la renumérotation béninoise de 2023 : l’ancien à 8 chiffres et le nouveau à 10 commençant par 0. Ce zéro fait partie du numéro — le retirer produit un lien qui ne s’ouvre sur personne. La normalisation est faite côté serveur, une fois pour toutes.

La contrepartie : rester listé suppose une activité tracée. Un technicien qui encaisse à côté sans jamais faire remonter de mission n’a rien à montrer, ne tient pas son quota, et sort de la vitrine.

Silence Conséquence
45 jours Avertissement (push ACTIVITY_STATUS_CHANGED, statut WARNED)
90 jours Retrait de l’annuaire (statut DELISTED)
Une mission tracée Retour immédiat dans la liste

Jamais de retrait sans préavis : quelqu’un retiré sans l’avoir vu venir ne revient pas. Et le retrait n’est pas une suppression — compte, historique et avis sont conservés, la fiche reste consultable par lien direct. Une seule mission suffit à réapparaître.

Même garde-fou que pour le quota : si aucune mission n’était disponible, l’absence n’est pas un manquement et rien n’est sanctionné. Sans ce contrôle, une saison creuse viderait l’annuaire.

Enfin, un avis ne peut venir que d’un client ayant réellement engagé une mission avec ce technicien : une clé étrangère vers le ticket et une contrainte d’unicité (ticket, expert) le garantissent en base, pas seulement dans le code.

Comptes internes (fondateur, staff)

Un compte marqué is_internal est exempté des règles de progression : il n’est ni promu ni rétrogradé, il n’a pas de quota de contribution à tenir, et il ne reçoit pas la diffusion des missions rémunérées — le notifier reviendrait à concurrencer les techniciens sur la place qu’on leur ouvre.

Et il peut superviser quel que soit son palier. Ce n’est pas une faveur, c’est ce qui permet au système de démarrer : la supervision exige normalement EXPERT, atteindre EXPERT demande 60 missions, et ces missions ne se font pas sereinement sans encadrement. Sans cette porte, personne n’aurait jamais pu valider personne — un blocage qui ne se serait vu qu’au premier vrai technicien, un mois après le lancement. Les premiers arbitrages sont rendus par la maison, jusqu’à ce qu’un technicien atteigne EXPERT et prenne le relais.

Un compte interne reste visible dans l’annuaire : son palier y est exact, et l’en retirer priverait la vitrine d’une référence réelle. Pour l’en sortir, c’est is_active = false.

À score égal — le cas de deux profils sans activité — c’est le palier qui départage le classement, pas l’ordre d’inscription.

La récompense, c’est l’accès

POST /tickets/:id/claim renvoie 403 TIER_TOO_LOW si le palier ne suffit pas, avec required_tier et current_tier dans l’erreur. Le catalogue actuel :

Mission Palier requis
Dépannage routeur basique PRO
Configuration VPN avancée EXPERT
Audit sécurité firewall INGENIEUR

Une promotion ou une rétrogradation envoie un push TIER_CHANGED (from, to, reason).


3. Notes techniques internes

POST /api/v1/support/tickets/:id/notes · authentifié

Corps : { "content": "…" }. Réponse 201.

GET /api/v1/support/tickets/:id/notes · authentifié

Réservées au technicien assigné, en lecture comme en écriture : 403 NOT_ASSIGNED sinon. La garde vérifie l’assignation, pas seulement l’authentification — c’est tout l’intérêt d’une note privée. Elles pendent au ticket, jamais au canal de discussion : un canal est par nature ce que les deux parties lisent.


4. Missions rémunérées

Inchangé, déjà en place :


5. Trois écarts assumés par rapport au contrat initial

1. Pas de table professionals. support_experts porte déjà les profils, les notes et les compteurs de missions. Créer une seconde table pour la même notion, c’est garantir deux vérités qui divergent au premier oubli de synchronisation. Les colonnes structurées (tags, language_codes, price_amount, country_code, is_online, status_detail, tickets_resolved) ont été ajoutées à côté des colonnes de saisie libre, qui restent le formulaire de candidature.

2. Les canaux portent le préfixe support:, pas support_. Le contrat nommait les canaux support_presence et support_missions. Un canal sans deux-points tombe dans le namespace par défaut de Centrifugo — sans présence, sans historique, sans join/leave. Le namespace configuré s’appelle support, les canaux sont donc support:presence et support:missions.

⚠️ L’application doit s’abonner à ces noms-là. C’est le seul point du contrat où le nom a changé, et il est structurant.

Centrifugo tourne sur VPS2 (chat_centrifugo v5, stack /opt/jservices/chat-engine/), et le backend le joint depuis VPS1 par Tailscale — du serveur à serveur, rien n’est exposé de plus. Vérifié de bout en bout : publication acceptée et retrouvée dans l’historique du canal.

Adresses publiques pour l’application

Usage URL
WebSocket temps réel wss://chat.jmoai.net/connection/websocket
Pièces jointes (URLs pré-signées) https://chat.jmoai.net/expert-hub-media/…

Domaine dédié, séparé de vpn.mikhmoai.com : ce dernier porte WebFig et le plan VPN, et un service grand public n’a rien à y faire. Certificat Let’s Encrypt, renouvellement automatique. Rien d’autre n’est exposé sur ce domaine — ni la console MinIO, ni l’API d’administration de Centrifugo : la racine répond 404.

3. Reprise de données volontairement incomplète. Les tags n’ont été posés que lorsque le mot figure littéralement dans l’expertise saisie ; seul le français a été déduit des langues ; les pays non reconnus restent NULL. Un profil sans tag est simplement absent des filtres — un profil mal étiqueté attire des clients à tort. Les profils existants doivent être complétés à la main pour être pleinement exploitables.