Documentation J+SERVICES Guides Référence API

Contrat API — Panier et commande (solde J+SERVICES)

Version 1 — 22/08/2026. Base : https://live.jmoai.net Authorization: Bearer <jeton client> obligatoire : on débite un portefeuille, il faut donc savoir celui de qui. Le client vient du jeton, jamais du corps de la requête.

Répond à la note « Panier & Facturation » de l’équipe frontend. Les noms de champs demandés (status, error_code, order, invoice_ref…) sont respectés : l’écran est déjà écrit, le faire changer pour satisfaire une convention n’apporterait rien à l’utilisateur.


1. La règle qui décide de tout : qui vend ?

Le mode de paiement n’est pas un choix de l’application, c’est une propriété du vendeur :

Article vendu par Paiement Pourquoi
J+SERVICES solde J+SERVICES (ce contrat) nous vendons, donc nous encaissons
un partenaire son agrégateur (action_behavior) l’argent doit tomber chez LUI

Un article de partenaire mis au panier est refusé avec VENDEUR_EXTERNE. Ce n’est pas une limite technique : encaisser sa vente sur notre solde ferait de nous son débiteur — séquestre, reversement, trésorerie à surveiller. C’est un métier, pas une ligne de code.

Côté interface : n’affichez le bouton « Ajouter au panier » que sur les articles dont action_behavior.mode vaut in_app_store et dont le vendeur est J+SERVICES. Les autres gardent « Acheter chez le vendeur ».


2. Vérifier le panier — avant de débiter

POST /api/v1/store/orders/verifier
{ "currency": "XOF",
  "items": [ { "product_id": "hw-hap-ax4", "quantity": 2 } ] }
{
  "status": "success",
  "panier": {
    "lignes": [
      {
        "designation": "MikroTik hAP ax4",
        "quantite": 2,
        "prix_unitaire": 85000,
        "total": 170000,
        "product_id": "hw-hap-ax4"
      }
    ],
    "total": 170000,
    "currency": "XOF",
    "payment_method": "J_SERVICES_BALANCE"
  }
}

Appelez-la à l’ouverture du tiroir. Elle ne débite rien et rend le vrai total : une surprise au moment du débit est la pire des surprises.


3. Payer

POST /api/v1/store/orders/checkout
{ "currency": "XOF",
  "items": [ { "product_id": "hw-hap-ax4", "quantity": 2, "unit_price": 85000 } ],
  "reference": "ord_optionnel" }
{
  "status": "success",
  "order": {
    "order_id": "ord_1396345dc835",
    "invoice_ref": "FAC-2026-000002-AE4A",
    "total_paid": 170000,
    "currency": "XOF",
    "payment_method": "J_SERVICES_BALANCE",
    "transaction_date": "2026-08-22T21:05:00Z",
    "invoice_pdf_url": "https://live.jmoai.net/api/v1/marketplace/factures/FAC-2026-000002-AE4A?t=…",
    "invoice_pdf_expire_le": "2026-12-20T…",
    "solde_restant": 30000
  }
}

Quatre points qui diffèrent de la note initiale — et pourquoi

unit_price est ignoré. Le serveur facture le prix du catalogue. Un panier ouvert depuis vingt minutes, une page en cache ou un appel forgé reviennent au même : faire confiance au client sur le prix, c’est le laisser fixer nos prix. Si l’écart existe, la commande est refusée avec PRIX_MODIFIE et le nouveau total — à l’acheteur de confirmer. Débiter un montant différent de celui affiché à l’écran est la définition de « prix affiché ≠ prix facturé ».

invoice_ref a la forme FAC-<année>-<séquence>-<vendeur>, pas INV-784512. C’est la même série continue que toutes nos factures : elle doit rester justifiable devant un contrôle.

invoice_pdf_url est un lien SIGNÉ et daté, pas /invoices/INV-784512.pdf. Les numéros de facture sont séquentiels, donc devinables : une URL non signée laisserait n’importe qui parcourir les factures des autres. Le lien vaut pour une facture et une seule, et il expire — mais il n’exige aucune session, donc le bouton « Télécharger » marche même hors connexion à l’app.

solde_restant est rendu en plus : c’est la question suivante de l’utilisateur, autant y répondre tout de suite.

Réponse en cas d’échec

{
  "status": "error",
  "error_code": "INSUFFICIENT_FUNDS",
  "message": "Solde insuffisant pour effectuer cet achat.",
  "details": { "solde": 50000, "requis": 170000, "manquant": 120000, "currency": "XOF" }
}
HTTP error_code Ce que l’écran doit faire
402 INSUFFICIENT_FUNDS proposer de recharger — details.manquant donne le montant exact
409 PRIX_MODIFIE afficher le nouveau total (details.total_actuel) et redemander confirmation
409 OUT_OF_STOCK retirer la ligne, details.product_id la désigne
409 VENDEUR_EXTERNE cet article s’achète chez le vendeur, pas au panier
404 PRODUCT_NOT_FOUND l’article n’existe plus — rafraîchir le catalogue
401 session expirée

4. Ce que le serveur garantit


5. Stock

GET /api/v1/marketplace/hardware renvoie désormais inStock (booléen) à côté de stockStatus — le badge « en rupture » n’a plus à interpréter quatre valeurs.

stock_quantity n’est pas renvoyé : nous ne tenons pas de compteur d’unités sur ce catalogue. Annoncer « 45 en stock » sans savoir le décrémenter à la vente afficherait un chiffre faux au deuxième acheteur — pire que pas de chiffre du tout. Le jour où les quantités seront tenues (boutiques partenaires), le champ apparaîtra.

stockStatus inStock
in_stock, low_stock, preorder true
out_of_stock false