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: trueest 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
- Un clic dupliqué n’est jamais facturé deux fois. Même visiteur, même produit, même tranche de 30 minutes → un seul événement facturable. C’est une contrainte de base de données, pas une vérification applicative.
- Une campagne à budget épuisé cesse d’être servie, donc d’être facturée.
- Le montant vient de la campagne, jamais de la requête : rien de ce que l’app envoie ne peut influencer la facturation.
- L’IP n’est jamais stockée. Seule une empreinte salée et tronquée sert à dédupliquer.
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.