Documentation J+SERVICES Guides Référence API

Contrat API — Portefeuille de services & tickets rémunérés

Version 1 — 10/08/2026. Base : https://live.jmoai.net Toutes les routes exigent l’en-tête Authorization: Bearer <jeton>. Montants en XOF, entiers ou décimaux à 2 chiffres. Toutes les réponses suivent la forme { success: boolean, data?: …, error?: { code, message } }.


Le principe en une phrase

Le client recharge un portefeuille, puis dépense ce solde pour poster des missions rémunérées. L’argent d’une mission est bloqué en séquestre dès la publication : l’expert qui la prend sait que la somme est là, et le client ne peut pas la dépenser ailleurs entre-temps.

recharger ──► solde disponible ──► poster une mission ──► séquestre
                                                            ├─ mission terminée ─► expert payé
                                                            └─ mission annulée  ─► client remboursé

1. Consulter le portefeuille

GET /api/v1/support/wallet/me?limit=20
{
  "success": true,
  "data": {
    "balance": 12500.0,
    "currency": "XOF",
    "status": "ACTIVE",
    "entries": [
      {
        "id": "…",
        "direction": "DEBIT",
        "amount": -6000,
        "currency": "XOF",
        "reason": "TICKET_ESCROW",
        "reference_type": "TICKET",
        "reference_id": "…",
        "created_at": "2026-08-10T09:12:44Z"
      },
      {
        "id": "…",
        "direction": "CREDIT",
        "amount": 10000,
        "currency": "XOF",
        "reason": "TOPUP",
        "reference_type": "PAYMENT",
        "reference_id": "MTX-…",
        "created_at": "2026-08-10T09:01:02Z"
      }
    ]
  }
}

amount est signé du point de vue du client : négatif quand il paie, positif quand il reçoit. Affichable directement, sans recalcul.

reason possibles : TOPUP, TICKET_ESCROW, TICKET_RELEASE, TICKET_REFUND, PLATFORM_FEE, WITHDRAWAL, ADMIN_CREDIT, ADMIN_DEBIT.

Un client qui n’a jamais rechargé n’a pas encore de compte : la réponse renvoie balance: 0 et entries: []. Ce n’est pas une erreur, il ne faut pas afficher d’échec.


2. Recharger le solde

POST /api/v1/support/wallet/topup
{ "amount": 10000, "provider": "fedapay", "return_url": "mikhmoai://wallet/success" }

provider : fedapay (défaut) ou geniuspay. Bornes : 500 à 2 000 000 XOF (WALLET_TOPUP_MIN_XOF / _MAX_XOF).

{
  "success": true,
  "data": {
    "checkout_url": "https://…",
    "internal_tx_id": "WLT_1786…_a1b2c3",
    "provider": "fedapay",
    "amount": 10000,
    "currency": "XOF"
  }
}

Le solde n’est PAS crédité à cet instant. Cette route ouvre seulement un paiement. Le crédit intervient quand le prestataire confirme, par webhook. Un client qui abandonne le paiement n’a rien reçu — c’est voulu.

Parcours attendu côté app

  1. Appeler /wallet/topup, ouvrir checkout_url.
  2. Au retour sur return_url, ne pas supposer que c’est payé.
  3. Rafraîchir /wallet/me : si le solde n’a pas bougé, réessayer après quelques secondes (le webhook arrive en général en 2 à 5 s). Trois tentatives espacées suffisent.
  4. Le montant crédité est celui que le prestataire confirme, jamais celui envoyé dans la requête. Inutile de le recalculer côté app.

⚠️ Envoyer l’en-tête Authorization sur /wallet/topup. Si le jeton manque, la requête est rejetée en 401 : il n’existe aucun chemin « invité » pour un rechargement. C’est délibéré — un rechargement anonyme créditerait un portefeuille sans propriétaire.


3. Poster une mission rémunérée

POST /api/v1/support/wallet/tickets
{
  "subject": "Configuration hotspot MikroTik hAP ax3",
  "description": "Portail captif + limitation de débit par profil.",
  "category": "MIKROTIK",
  "price_amount": 6000,
  "is_urgent": true
}

price_amount est libre : c’est le montant que le client propose. is_urgent remonte la mission dans la file des experts.

201 Created — la mission est ouverte et la somme bloquée :

{
  "success": true,
  "data": {
    "id": "…",
    "status": "OPEN",
    "payment_status": "ESCROWED",
    "price_amount": 6000,
    "is_urgent": true,
    "currency": "XOF"
  }
}

400 — solde insuffisant (le cas à soigner dans l’interface) :

{
  "success": false,
  "error": {
    "code": "SOLDE_INSUFFISANT",
    "message": "Solde insuffisant. Rechargez votre portefeuille.",
    "balance": 2500,
    "required": 6000,
    "missing": 3500
  }
}

missing est fourni pour proposer directement « Recharger 3 500 XOF » sans calcul côté app.

Autres codes : SUJET_REQUIS, PRIX_INVALIDE, UNAUTHENTICATED.

La mission n’est visible des experts qu’après le blocage des fonds. Si le blocage échoue, le ticket est annulé et rien n’est publié — jamais de mission « fantôme » sans argent.

Mission gratuite

Une demande de support ordinaire reste sur POST /support/api/tickets. Cette route crée toujours un ticket gratuit : price_amount et payment_status envoyés dans le corps y sont ignorés. Pour une mission rémunérée, il faut passer par /wallet/tickets.


4. Clôturer une mission

POST /api/v1/support/wallet/tickets/:id/settle     { "expert_id": "…" }   # expert payé
POST /api/v1/support/wallet/tickets/:id/refund     { "raison": "…" }      # client remboursé

Au règlement, la somme séquestrée est répartie entre l’expert et la commission de la plateforme (15 % par défaut, TICKET_PLATFORM_FEE_PCT) :

{ "success": true, "data": { "partExpert": 5100, "commission": 900, "currency": "XOF" } }

Les deux opérations sont idempotentes : rejouer un règlement ne paie pas l’expert deux fois, rejouer un remboursement ne rembourse pas deux fois. L’app peut donc réessayer sans risque après une coupure réseau.

Un ticket qui n’est pas en ESCROWED renvoie { "ignore": true, "raison": "<statut>" } avec un code 200 — ce n’est pas une erreur, c’est une opération déjà faite.


5. États d’une mission

payment_status Sens Ce que l’app affiche
FREE mission gratuite rien de particulier
ESCROWED somme bloquée « Paiement sécurisé »
RELEASED expert payé « Réglée »
REFUNDED client remboursé « Remboursée »

status du ticket : PENDING_PAYMENT (transitoire, jamais visible d’un expert) → OPEN → … → CLOSED / CANCELLED.


6. Erreurs communes

Code HTTP error.code Que faire
401 UNAUTHENTICATED jeton absent ou expiré — reconnecter
400 SOLDE_INSUFFISANT proposer un rechargement de missing
400 MONTANT_HORS_BORNES rappeler les bornes 500 – 2 000 000
400 PRESTATAIRE_INCONNU fedapay ou geniuspay uniquement
429 quota atteint : attendre, ne pas réessayer en boucle
502 CHECKOUT_INDISPONIBLE prestataire injoignable — proposer l’autre

À propos du 429

Les routes qui engagent de l’argent sont limitées par utilisateur (5 actions par minute), pour empêcher le double clic de créer deux paiements. Un 429 ici n’est pas un incident : il faut désactiver le bouton pendant l’appel plutôt que de relancer automatiquement.

⚠️ Attention distincte : le quota global de l’API est de 100 requêtes / 15 min par IP, partagé par TOUTES les routes. Un écran qui sonde une route toutes les 15 secondes consomme à lui seul l’intégralité du budget et fait échouer le reste — y compris ces routes de portefeuille. Espacer les sondages à 60 s minimum et les couper en arrière-plan.


7. Garanties du serveur

Ces points sont assurés côté backend ; l’app n’a rien à réimplémenter.