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: nullsignifie « sur devis », pas « gratuit ». L’app doit afficher une mention, jamais0. Plusieurs experts ont répondu « Variable » au formulaire de candidature : inventer un nombre à leur place l’afficherait comme un engagement de leur part.
status_detailvautOFFLINEdès queisOnlineest faux, quel que soit l’état stocké. Un expert hors ligne n’est ni disponible ni occupé : il est absent. RenvoyerAVAILABLEferait 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 |
nulln’est pas0. 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.0et100 %: 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, pas0. 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 :
POST /api/v1/support/wallet/tickets— crée le ticket et bloque la somme en séquestre avant de l’ouvrir. Un ticket rémunéré ne peut pas être créé par une autre route : celle-ci est la seule qui garantit que l’argent existe.POST /api/v1/support/tickets/:id/claim—200si assigné,409si un autre technicien a été plus rapide. Verrou atomique en base, pas de verrou applicatif.POST /api/v1/support/tickets/:id/review— recalcule la note moyenne de l’expert.
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.