API Prépayés
Vends du prépayé depuis ton site
Recharges télécom, eSIM et cartes cadeaux, achetées par API au prix coûtant débité du wallet Monniz Business. La livraison est effectuée par Monniz.
Mode test
La clé test simule tout le parcours : aucun débit, aucun appel réel, mêmes réponses.
Tarification
Chaque offre expose cost_price_xof, le prix coûtant débité de ton wallet à l’achat.
Webhooks signés
Chaque livraison ou échec est poussé sur ton serveur, signé HMAC, avec retries.
Démarrage rapide
- 1
Génère ta clé test
Dans Paramètres → Développeur, génère la clé
mnz_sk_test_…. Elle n'est affichée qu'une seule fois. - 2
Explore le catalogue
bashcurl "https://api.monniz.shop/v1/prepaid/catalog/topups?country=BJ" \ -H "Authorization: Bearer mnz_sk_test_..."
- 3
Fais un premier achat simulé
La clé test ne débite rien et n'appelle aucun fournisseur :
bashcurl -X POST "https://api.monniz.shop/v1/prepaid/purchase/topup" \ -H "Authorization: Bearer mnz_sk_test_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8f4e2a10-77b3-4c1e-9b1a-2d6c58c01a42" \ -d '{ "offer_id": "TOPUP-MTN-BJ-1000", "recipient_phone": "+2290167819813" }'La réponse contient une transaction
COMPLETEDavec une livraison factice. - 4
Passe en réel
Une fois ton business vérifié (KYB), la clé
mnz_sk_live_…se génère au même endroit — même API, même contrat.
Le détail de chaque endpoint est dans la référence interactive
https://api.monniz.shop/docs.postman.Authentification & modes
Toutes les requêtes portent ta clé en en-tête Authorization: Bearer <clé>. Le préfixe de la clé détermine le mode :
mnz_sk_test_…TestAchats simulés : aucun débit, aucune livraison réelle, réponses au même format (livemode: false). Disponible dès l'inscription.
mnz_sk_live_…LiveAchats réels débités de ton wallet Business. Nécessite un KYB approuvé.
Les données test et live sont étanches : une clé test ne voit que les transactions test, et inversement. Tu peux restreindre les adresses IP autorisées (exactes ou CIDR) et révoquer tes clés à tout moment — effet immédiat.
La clé est un secret côté serveur
Catalogue & prix
- GET
/v1/prepaid/catalog/countriesPays disponibles par type - GET
/v1/prepaid/catalog/brandsMarques (logo, couleur) - GET
/v1/prepaid/catalog/categoriesSous-types valides du filtre sub_type - GET
/v1/prepaid/catalog/{topups|esims|vouchers}Offres — filtres country, brand, sub_type, search, pagination - GET
/v1/prepaid/catalog/offers/{offer_id}Détail d'une offre
Chaque offre porte un bloc pricing : cost_price_xof est le prix coûtant, débité du wallet à l'achat.
{
"offer_id": "TOPUP-MTN-BJ-1000",
"name": "Recharge MTN 1 000 FCFA",
"pricing": {
"price_type": "FIXED",
"cost_price_xof": 1039
}
}Les offres price_type: "RANGE" (montant libre) exposent des bornes value_min/value_max et un pricing à la borne minimale (at_min_value) ; le montant choisi se passe dans value à l'achat.
Achat
- POST
/v1/prepaid/purchase/previewDevis sans engagement (prix au moment T) - POST
/v1/prepaid/purchase/{topup|esim|voucher}Achat — Idempotency-Key OBLIGATOIRE
Idempotency-Key obligatoire
Idempotency-Key (un UUID unique par intention d'achat) est obligatoire : sans lui, la requête est refusée (428). Rejouer une requête avec la même clé renvoie la réponse d'origine, sans second débit.Corps de la requête selon le type :
| Champ | Type | Détail |
|---|---|---|
offer_id | requis | Identifiant de l'offre du catalogue |
recipient_phone | requis (topup) | Format international strict, ex. +2290167819813 — Bénin et Côte d'Ivoire gardent le 0 après l'indicatif |
recipient_email | optionnel | Email du bénéficiaire (vouchers, eSIM) |
iccid | optionnel (eSIM) | Recharge d'une eSIM existante |
fields | selon l'offre (voucher) | Objet {nom: valeur} — champs requis listés dans required_fields de l'offre |
value | requis (RANGE) | Montant choisi, dans la devise de remise |
locked_price | recommandé | Le cost_price_xof montré à ton client — achat refusé si le prix réel a dérivé de plus de 5 % |
Réponse (achat accepté) :
{
"success": true,
"data": {
"transaction": {
"reference": "MZB_PRE_20260818_8QCF6OCA",
"livemode": true,
"status": "PROCESSING",
"amount": 1039,
"delivery_details": null,
"created_at": "2026-08-18T11:42:05+00:00"
}
}
}status: "PROCESSING" signifie que la livraison est en cours, amount est le montant débité de ton wallet, et delivery_details reste null jusqu'à la livraison.
Cycle de vie & suivi
PROCESSING : payé, livraison en cours (quelques secondes à quelques minutes). COMPLETED : livré — delivery_details contient la confirmation (n° de confirmation, ePIN, activation eSIM…). FAILED / CANCELLED : échec de livraison → remboursement automatique de ton wallet, état final REFUNDED.
- GET
/v1/prepaid/transactionsHistorique — filtres product_type, status, date_from, date_to - GET
/v1/prepaid/transactions/{reference}Détail + statut d'une transaction - GET
/v1/prepaid/transactions/{reference}/qrcodeQR code eSIM (PNG) — une fois COMPLETED - GET
/v1/prepaid/balanceSolde de ton wallet Business
La livraison n'est confirmée que par COMPLETED
COMPLETED (webhook ou GET /transactions/{reference}) ; la réponse de l'achat ne la confirme pas.Webhooks
L'URL HTTPS et les événements se configurent dans l'onglet Développeur. Chaque transition est POSTée sur ton serveur : transaction.completed, transaction.failed, transaction.refunded — 8 tentatives avec backoff (10 s → 24 h, ~2 jours) tant que la réponse n'est pas 2xx. GET /transactions/{reference} rend le même statut.
L'en-tête Monniz-Signature porte le timestamp et la signature v1 = HMAC-SHA256 de "{t}.{corps brut}" avec ton secret whsec_… :
POST https://ton-site.com/webhooks/monniz
X-Monniz-Event: transaction.completed
Monniz-Signature: t=1787053351,v1=3f5a1c9b2e847d0a6c1f4b8e5d2a7c90e1b6f3a8d4c7092e5b1f8a3d6c40e7b2
{
"id": "evt_01J8XKQ2M4N7P9R2S5T8V1W4X8",
"type": "transaction.completed",
"livemode": true,
"created": "2026-08-18T11:42:31+00:00",
"data": {
"reference": "MZB_PRE_20260818_8QCF6OCA",
"status": "COMPLETED",
"product_type": "topup",
"amount": 1039,
"delivery_details": { "confirmation": { "confirmationNumber": "4058712396" } }
}
}Signature
whsec_…, affiché une seule fois à la configuration du webhook.// Monniz-Signature: t={timestamp},v1={hmac}
[$tPart, $vPart] = explode(',', $_SERVER['HTTP_MONNIZ_SIGNATURE'] ?? ',');
$t = substr($tPart, 2);
$given = substr($vPart, 3);
$expected = hash_hmac('sha256', $t . '.' . $rawBody, $webhookSecret);
// Timestamp signé = anti-rejeu : refuse un événement trop vieux.
if (abs(time() - (int) $t) > 300 || ! hash_equals($expected, $given)) {
http_response_code(401); exit;
}// Express — utiliser le corps BRUT, pas le JSON parsé
const crypto = require('crypto')
const header = req.get('Monniz-Signature') || '' // t=...,v1=...
const t = (header.match(/t=(\d+)/) || [])[1] || '0'
const given = (header.match(/v1=([a-f0-9]{64})/) || [])[1] || ''
const expected = crypto.createHmac('sha256', WEBHOOK_SECRET).update(t + '.' + rawBody).digest('hex')
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300 // anti-rejeu
if (!fresh || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given.padEnd(expected.length)))) {
return res.status(401).end()
}Une réponse 2xx marque l'événement comme livré. Un même événement peut être livré plusieurs fois.
Erreurs
Toutes les erreurs suivent le même format :
{ "success": false, "message": "...", "errors": { "code": "..." } }| HTTP | Code | Quand |
|---|---|---|
| 401 | API_KEY_MISSING / API_KEY_INVALID | Clé absente, inconnue ou révoquée |
| 403 | PREPAID_DISABLED | Service momentanément indisponible (maintenance) |
| 403 | KYB_REQUIRED / ACCOUNT_SUSPENDED | Mode live sans KYB approuvé, ou compte suspendu |
| 403 | IP_NOT_ALLOWED | IP hors de ta whitelist |
| 429 | RATE_LIMIT_EXCEEDED | Limite par minute atteinte |
| 428 | IDEMPOTENCY_KEY_REQUIRED | En-tête Idempotency-Key manquant sur l'achat |
| 422 | UNPROCESSABLE | Validation : numéro mal formé, champ voucher manquant, prix dérivé (> 5 %), solde insuffisant… |
| 404 | NOT_FOUND | Offre ou transaction introuvable (ou pas à toi) |
Limites & sécurité
Rate limit
60 requêtes/minute par défaut — la limite exacte figure dans ton onglet Développeur.
Solde
Les achats live sont débités de ton wallet Business. Un solde insuffisant refuse l'achat (422).
Remboursements
Tout échec de livraison recrédite le wallet automatiquement, à l'identique du débit et sans frais.
Pagination
page / per_page (max 100) sur les listes, avec meta.total dans la réponse.
Aller plus loin
Prêt à vendre du prépayé ?
Crée ton compte Monniz Business, génère ta clé test et fais ton premier achat simulé en cinq minutes.