Contrat API — redirection checkout boutique
POST /api/store/checkout
Cette route valide un plan et retourne l’URL du Hub de paiement officiel. Elle ne crée aucune transaction et ne délivre aucune licence.
Requête
{
"product_id": "uuid-du-plan",
"payment_method": "fedapay",
"coupon_code": "OPTIONNEL",
"referral_code": "OPTIONNEL",
"ref_code": "CODE_REVENDEUR_OPTIONNEL",
"markup_pct": 20
}
Seul product_id est requis ; payment_method est facultatif et ignoré (le moyen de
paiement se choisit sur le Hub). Un code de réduction invalide
renvoie une erreur 404.
ref_code attribue la vente au revendeur et applique le markup configuré pour le plan dans sa boutique.
markup_pct est facultatif et ne peut pas dépasser le taux du plan ni le plafond markup_percentage enregistré par le
revendeur. Il ne remplace pas coupon_code : une remise coupon éventuelle s’applique d’abord,
puis le markup est calculé sur le prix après remise. Le serveur recalcule toujours le montant.
Le catalogue public est GET /api/store/catalog?ref=CODE&markup=20. Pour une boutique revendeur, il ne renvoie que les plans que le vendeur a sélectionnés. Sa réponse conserve
items et ajoute list_price, base_price (prix résolu par le serveur avant markup),
retail_price, markup_amount et markup_percentage à chaque article, ainsi qu’un objet
reseller lorsque le code est reconnu. La consultation peut être anonyme ou authentifiée ;
dans ce dernier cas, la résolution tarifaire utilise le compte du jeton.
Sans markup, chaque plan sélectionné applique son propre taux, plafonné par markup_percentage enregistré sur le profil.
Avec markup, le taux demandé doit être compris entre 0 et le plafond du profil; il est plafonné au markup configuré pour chaque plan. Le champ price
est un alias de retail_price pour les consommateurs qui utilisent déjà ce nom.
Catalogue choisi par le revendeur
Les routes suivantes sont réservées au revendeur approuvé (cookie partenaire ou Bearer) :
GET /api/v1/resellers/store/products
PUT /api/v1/resellers/store/products/{planId}
La lecture renvoie les plans actifs éligibles et leur état selected (false par défaut), avec le markup enregistré par plan. La mise à jour exige { "enabled": true, "markup_percentage": 15 }; enabled: false retire le plan de la boutique publique. Le taux est compris entre 0 et 100, avec deux décimales. Le plan doit être actif et son identifiant doit être un UUID. Il est impossible d’ajouter un produit arbitraire ou un plan inactif. L’enregistrement d’un plan actif relève automatiquement le plafond historique du profil si nécessaire. Le plafond ne peut pas être abaissé sous le markup d’un plan actif (MARKUP_BELOW_PRODUCT, 409).
Lors de la migration initiale, les revendeurs déjà ACTIVE reçoivent une sélection activée à 0 % pour chaque plan licencié RETAIL déjà actif. Cela conserve leur ancien catalogue public. Les revendeurs créés après cette migration choisissent leurs plans; un plan ultérieur n’est pas activé automatiquement. Cette initialisation ne vaut pas approbation partenaire et ne convertit pas resellers.commission_rate en marge de vente.
La boutique publique ne présente que les plans actifs sélectionnés. Le checkout avec ref_code est refusé avec STORE_PRODUCT_NOT_LISTED si le plan n’est pas sélectionné. Le serveur détermine le markup depuis la configuration du revendeur; un markup_pct explicite peut le réduire, jamais l’augmenter. Sans ref_code, le catalogue public non revendeur conserve son comportement existant.
Erreurs de gestion : PRODUCT_PLAN_NOT_FOUND (404), STORE_PRODUCT_NOT_LISTED (404), MARKUP_ABOVE_PRODUCT (400), MARKUP_BELOW_PRODUCT (409), REFERRAL_CODE_CONFLICT (400) si referral_code désigne un revendeur différent de ref_code. Une boutique où aucun plan n’est sélectionné renvoie un catalogue vide; ce n’est pas une erreur.
Réponse catalogue 200
{
"items": [
{
"id": "0d5633f8-f322-4fce-9e8a-bf22e101c701",
"type": "VPN",
"name": "Licence VPN 30 jours",
"price": 1200,
"base_price": 1000,
"list_price": 1000,
"retail_price": 1200,
"markup_amount": 200,
"markup_percentage": 20,
"currency": "XOF"
}
],
"reseller": {
"code": "EXPERT-2026",
"pricing_mode": "PER_PRODUCT",
"markup_ceiling_pct": 20,
"applied_markup_pct": 20
}
}
La réponse conserve reseller.applied_markup_pct pour compatibilité; ce champ indique le taux demandé/profil avant plafonnement par article. Le taux effectivement appliqué est markup_percentage sur chaque article. Les consommateurs doivent privilégier les taux par article.
Les erreurs métier sont RESELLER_CODE_REQUIRED (markup sans référence),
INVALID_MARKUP_PERCENTAGE (taux hors de 0–100), RESELLER_NOT_FOUND (code absent ou
suspendu) et MARKUP_ABOVE_PROFILE (taux demandé supérieur au taux enregistré). Le storefront
doit traiter ces réponses comme un échec de cotation et ne pas afficher les prix publics à la
place des prix revendeur.
Affichage storefront et transfert au Hub
La page publique est /store. Les liens revendeur utilisent
https://store.jmoai.net/store?ref=<promo_code> ; le profil fournit le plafond, et chaque plan sélectionné fournit son taux configuré.
Un lien peut ajouter &markup=<taux> pour appliquer un taux inférieur ou égal au plafond du
profil. Le storefront charge le catalogue public, demande la cotation revendeur à
GET /api/store/catalog, puis affiche retail_price, le prix de base et le taux retournés.
Si la cotation revendeur échoue, il masque les articles concernés au lieu de présenter un prix
non vérifié.
À l’achat, le storefront passe ref_code=<promo_code> à /api/store/checkout. Cette API transmet partnerId=<reseller_id> au Hub et, si le lien avait un paramètre
markup, markupPct=<taux> au Hub de paiement des licences. En l’absence de paramètre, le Hub
résout le taux depuis le profil. Le Hub et le webhook recalculent et vérifient les montants côté
serveur ; le prix affiché par le navigateur ne fait pas autorité.
Réponse 200
{
"checkout_url": "https://live.mikhmoai.com/api/v1/payments/hub?productCode=...&planCode=...",
"final_price": 20880,
"base_price": 17400,
"markup_amount": 3480,
"applied_markup_pct": 20
}
Le client redirige l’acheteur vers checkout_url. Le Hub crée ensuite une
transaction PENDING, démarre le prestataire de paiement et attend son webhook
authentifié. Seule cette confirmation peut entraîner la délivrance d’une
licence.
Garanties et limites
- Une redirection n’est pas une confirmation de paiement.
- Une licence n’est jamais activée par cette route.
- Une offre inactive ou hors catalogue ne peut pas être achetée via le Hub.
- Le montant du checkout est calculé depuis le plan, le coupon validé et le markup autorisé du profil revendeur ; le navigateur ne fixe jamais le montant encaissé.
- Après confirmation du paiement, le webhook crédite la commission habituelle calculée sur le montant avant markup, plus l’intégralité du markup. Le crédit n’est créé qu’après confirmation.
- Le checkout Chario ne prend pas en charge le markup, car le marchand contrôle le prix distant.
Fournisseur MoneyFusion (SaaS)
MoneyFusion peut être proposé dans le Hub de paiement des licences lorsque
MONEYFUSION_HUB_ENABLED=true, MONEYFUSION_API_URL et
MONEYFUSION_WEBHOOK_TOKEN sont configurés. L’URL API vient du tableau de bord
MoneyFusion. Le serveur transmet le montant autoritaire du plan, la référence
locale comme personal_Info.orderId, le return_url et le webhook_url dédié.
Le retour navigateur ne confirme jamais le paiement. Le webhook utilise un
jeton secret dans son chemin, puis le serveur interroge l’API MoneyFusion et
vérifie le token, la référence locale, le statut distant strictement égal à
paid et le montant avant de lancer la livraison de licence. Le montant
confirmé Montant doit égaler le prix attendu, ou Montant + frais doit
l’égaler lorsque MoneyFusion présente les frais séparément. Les notifications
pending, cancelled et les statuts inconnus ne délivrent rien. Les
notifications répétées suivent les gardes d’idempotence existants.
MoneyFusion exige aussi que l’IP publique sortante du serveur soit autorisée dans l’application du compte marchand. La documentation consultée ne décrit pas de signature cryptographique du webhook. L’intégration reste désactivée par défaut ; une transaction réelle de bout en bout doit être qualifiée avant son activation en production.
Déviation historique
Avant le 29 août 2026, la route inscrivait à tort une transaction SUCCESS
avant toute confirmation du prestataire. Ce comportement est supprimé : le Hub
officiel est l’unique parcours de paiement.