Documentation J+SERVICES Guides Référence API

Contrat API — Marketplace matériel sponsorisé

Version 1 — 10/08/2026. Base : https://live.jmoai.net Authorization: Bearer <jeton> optionnel : la vitrine s’affiche sans session, mais quand le jeton est présent le ciblage et la mesure sont plus précis.


1. Récupérer les produits

GET /api/v1/marketplace/hardware?context_tag=high_cpu&country=SN&router_model=RB750Gr3
Paramètre Optionnel Valeurs
context_tag oui high_cpu, heavy_hotspot, wifi_expansion, high_bandwidth
country oui code ISO 2 lettres (SN, CI, FR…) — cible les distributeurs locaux
router_model oui modèle détecté (RB750Gr3, CCR1009…)
limit oui 20 par défaut, 50 maximum
{
  "success": true,
  "data": {
    "campaign_id": "camp_mikrotik_2026_q3",
    "sponsor_name": "EuroWireless Distribution",
    "updated_at": "2026-08-10T00:00:00Z",
    "items": [
      {
        "id": "rb5009-ug",
        "title": "MikroTik RB5009UG+S+IN",
        "category": "Router",
        "model": "RB5009",
        "description": "…",
        "features": ["Jusqu'à 300 utilisateurs", "WiFi 6 haut débit"],
        "specs": { "cpu": "…", "ram": "1 GB DDR4", "ports": "…", "throughput": "10 Gbps+" },
        "price": 219,
        "currency": "EUR",
        "originalPrice": 249,
        "discountPercentage": 12,
        "imageUrl": "https://…",
        "partnerName": "Distributeur Officiel MikroTik",
        "partnerBadge": "Distributeur Certifié",
        "clickUrl": "/api/v1/marketplace/go/rb5009-ug",
        "stockStatus": "in_stock",
        "targetAudience": ["high_cpu", "heavy_hotspot"],
        "featured": true,
        "action_behavior": { "mode": "external_browser", "internal_route": null }
      }
    ]
  }
}

features — les arguments de la fiche

Deux phrases courtes, éditoriales, faites pour être lues d’un coup d’œil : « Jusqu’à 300 utilisateurs », « WiFi 6 haut débit ». Affichez-en deux au maximum ; le tableau peut en contenir plus, le reste est du détail qui n’a pas sa place sur une carte.

Elles vivent avec l’article, pas dans l’application : changer un argument ne doit pas demander une publication sur les magasins. Le tableau peut être vide — affichez alors la description, n’inventez rien.

specs reste ce qu’il était : le détail technique, pour la page produit.

action_behavior — ce que l’app FAIT au clic

L’application ne décide plus rien. Le serveur dit, pour chaque fiche, où elle s’ouvre :

{
  "mode": "in_app_store",
  "internal_route": "/boutique/chez-fatou/hap-ax4",
  "boutique": "chez-fatou",
  "white_label": true,
  "vendeur": "Wifi Plus Bénin"
}
mode Ce que l’app fait
in_app_store ouvre internal_route dans l’app — le client ne sort pas
external_browser ouvre clickUrl dans le navigateur ; le clic est compté par la redirection

white_label dit si la page interne doit porter les couleurs du vendeur plutôt que les nôtres. boutique est l’adresse stable de la boutique : une fois envoyée à un client, elle ne change plus.

Pourquoi c’est le serveur qui tranche

C’est l’offre du vendeur qui décide, et elle change sans prévenir : un partenaire qui prend la boutique passe en in_app_store du jour au lendemain, un contrat qui s’arrête repasse en external_browser. Si la règle vivait dans l’application, chaque changement commercial demanderait une mise à jour sur tous les téléphones — et les anciens téléphones garderaient l’ancienne règle pour toujours.

L’app doit donc obéir au champ, sans jamais deviner d’après le badge, le prix ou le nom du partenaire.

Une fiche jamais morte

Un article dont la caisse du marchand n’est pas vérifiée n’est pas renvoyé du tout — ni en interne (le paiement échouerait), ni en externe s’il n’a pas de lien marchand. Une carte qui ne mène nulle part use la confiance plus vite qu’une section vide. Si un article est là, il s’ouvre.


Trois différences avec la spec initiale, à connaître

price et originalPrice sont des NOMBRES, et currency est un code ISO (EUR), plus un symbole. Le formatage (219 €, 219,00 €, séparateurs) est un choix d’affichage : il revient à l’app, qui connaît la locale de l’utilisateur. Un prix en texte empêchait de trier ou de filtrer par fourchette.

discountPercentage est calculé par le serveur depuis les deux prix. L’app n’a rien à recalculer — et cette valeur ne peut jamais contredire les prix affichés.

affiliateUrl n’est plus renvoyée. Elle est remplacée par clickUrl — voir ci-dessous.

Aucune campagne en cours

{ "success": true, "data": { "campaign_id": null, "items": [] } }

Ce n’est pas une erreur. Il faut simplement masquer la section, sans message d’échec. C’est l’état normal quand aucune campagne n’est active, que le budget est épuisé, ou qu’aucune ne cible le pays du client.


2. Ouvrir un produit — clickUrl

GET /api/v1/marketplace/go/:slug        →  302 vers le site du marchand

L’app ouvre simplement clickUrl (préfixée du domaine), dans un navigateur externe ou une WebView. Le serveur enregistre le clic puis redirige. Il n’y a rien d’autre à appeler : pas de POST de tracking à envoyer en parallèle.

GET https://live.jmoai.net/api/v1/marketplace/go/rb5009-ug?context_tag=high_cpu
→ 302 Location: https://distributeur.com/p/rb5009?ref=mv7&cid=camp_…

Paramètres optionnels : context_tag, country, client_os.

Pourquoi ce changement

La spec prévoyait de renvoyer affiliateUrl puis de poster un événement tracking/click. Ce clic était alors auto-déclaré par l’application — et un clic déclenche une facture à l’annonceur.

Concrètement, cela rendait possible : un bug de re-render facturant des milliers de clics ; un utilisateur rejouant l’appel ; un concurrent vidant le budget d’un annonceur en quelques minutes. Dans les trois cas, nous présentons une facture que l’annonceur refuse — à juste titre — et nous n’avons rien à lui opposer.

Avec la redirection, le clic est compté au moment où le serveur redirige réellement. C’est la pratique standard des régies, et la seule mesure défendable.

Réponse 404 si le produit n’existe plus ou si sa campagne est terminée : ouvrir un lien générique ou masquer l’élément.


3. Impressions (facturation au mille)

POST /api/v1/marketplace/tracking/impression
{ "campaign_id": "camp_mikrotik_2026_q3",
  "displayed_item_ids": ["rb5009-ug", "cap-ax"] }

À appeler une seule fois par affichage réel de la section, pas à chaque render. Réponse : { "success": true, "recorded": true }.


4. POST /tracking/click — conservé, mais déconseillé

L’ancien endpoint fonctionne toujours, avec les mêmes protections :

POST /api/v1/marketplace/tracking/click
{ "item_id": "rb5009-ug", "context_tag": "high_cpu", "client_os": "android" }

Il reste disponible pour ne pas casser une intégration en cours, mais clickUrl est le chemin à implémenter : il mesure mieux et ne demande aucun appel supplémentaire.

recorded: true est renvoyé même pour un clic dédupliqué. Du point de vue de l’app, l’événement est pris en compte ; qu’il soit facturé ou non ne la regarde pas.


5. Ce que le serveur garantit

L’app n’a donc aucune logique anti-fraude à implémenter. Elle affiche, elle ouvre clickUrl, c’est tout.


6. Ciblage — ce que le serveur fait du context_tag

context_tag Diagnostic côté app Produits remontés
high_cpu CPU routeur > 80 % durablement routeurs supérieurs (RB5009, CCR2004, CCR2116)
heavy_hotspot plus de 80 clients hotspot actifs routeurs haute capacité + points d’accès Wi-Fi 6
wifi_expansion signal faible, besoin d’un AP cAP ax, cAP ac, wAP ax, Groove
high_bandwidth trafic WAN > 500 Mbps switches SFP+ 10G (CRS310, CRS326)
(absent) meilleures ventes

Le classement est décidé côté serveur : correspondance avec le context_tag, puis avec le router_model, puis mise en avant et priorité éditoriale. L’app affiche la liste dans l’ordre reçu, sans retrier.


7. Erreurs

HTTP Quand Que faire
200 avec items: [] aucune campagne servable masquer la section
404 sur /go/:slug produit ou campagne terminée masquer l’élément
429 quota atteint ne pas réessayer en boucle
500 erreur serveur masquer la section, réessayer plus tard

Rappel : le quota global de l’API est de 100 requêtes / 15 min par IP, partagé par toutes les routes. La vitrine ne doit pas être rafraîchie en boucle — un appel à l’ouverture de l’écran suffit largement, le catalogue ne change pas en quelques secondes.


8. Administration (hors app)

GET /admin/api/marketplace/campaigns/:code/stats     # session admin requise

Renvoie clics totaux, clics facturés, clics non facturés (dédupliqués ou hors budget), impressions, budget consommé et restant. C’est le rapport à présenter à l’annonceur.