API BluePay

Encaissez par MTN Mobile Money et Orange Money, reversez instantanément, répartissez entre bénéficiaires. Une API REST, des montants en FCFA entiers, des réponses JSON.

URL de base : https://<votre-domaine-bluepay>. Toutes les requêtes passent en HTTPS. Créez votre compte dans l'espace marchand : une clé de test est fournie immédiatement.

Authentification

Chaque requête porte votre clé secrète dans l'en-tête Authorization. Les clés sk_test_… agissent en mode test, les clés sk_live_… en production (disponibles après vérification de votre compte). Une clé peut être restreinte à des adresses IP.

curl https://VOTRE-DOMAINE-BLUEPAY/v1/balance \
  -H "Authorization: Bearer sk_test_VotreCleSecrete"

Ne placez jamais une clé secrète dans une application mobile ou du code exécuté dans le navigateur. Créez les paiements depuis votre serveur.

Mode test

Le mode test simule les opérateurs : aucun argent réel ne circule et les soldes de test sont séparés de la production.

Conventions et erreurs

Paiements

POST/v1/payments

Avec channel et phone, BluePay envoie directement la demande au téléphone du client, qui valide avec son code Mobile Money. Sans eux, vous obtenez une page de paiement hébergée (voir plus bas).

curl https://VOTRE-DOMAINE-BLUEPAY/v1/payments \
  -H "Authorization: Bearer sk_test_…" -H "Idempotency-Key: commande-1042" \
  -H "Content-Type: application/json" \
  -d '{ "amount": 15000, "channel": "MTN", "phone": "677123456",
        "reference": "CMD-1042", "description": "Commande 1042",
        "metadata": { "client": "8812" } }'
{ "id": "pay_7hQ2…", "object": "payment", "livemode": false, "status": "pending",
  "amount": 15000, "currency": "XAF", "fee": 375, "fee_payer": "merchant",
  "amount_charged": 15000, "net": 14625, "channel": "MTN", "phone": "+237677123456",
  "reference": "CMD-1042", "checkout_url": null, "failure": null, … }

Statuts : open (page de paiement en attente du client), pending (demande envoyée), succeeded, failed, expired. Le solde est crédité dès succeeded. Option fee_payer: "customer" : les frais s'ajoutent au montant payé par le client.

GET/v1/payments/:id
GET/v1/payments?status=succeeded&reference=CMD-1042&limit=25

Fiez-vous aux webhooks ou à cette lecture pour connaître l'état final, jamais au seul retour du client sur votre site.

Page de paiement hébergée

POST/v1/checkout/sessions
{ "amount": 20000, "description": "Abonnement annuel", "fee_payer": "customer",
  "return_url": "https://votre-site.cm/merci" }
→ { "id": "pay_…", "status": "open", "checkout_url": "https://…/pay/pay_…?k=…" }

Redirigez le client vers checkout_url. Il choisit MTN ou Orange, saisit son numéro et valide. À la fin, il revient sur return_url?payment=pay_…&status=succeeded. La page expire après 30 minutes.

POST/v1/payment_links
{ "title": "Cotisation 2027", "amount": 25000 }      // montant fixe
{ "title": "Don", "amount": null, "min_amount": 500 }  // montant libre
→ { "id": "lnk_…", "url": "https://…/l/k3m9x2p7qa", "uses": 0, … }

À partager par WhatsApp, SMS ou QR code. Aussi disponible sans code depuis l'espace marchand.

Remboursements

POST/v1/payments/:id/refunds
{ "amount": 2000, "reason": "Article manquant" }   // sans montant : remboursement total

Le client est remboursé sur son numéro Mobile Money ; le montant est débité de votre solde. Événement payment.refunded à la réception.

Répartition entre bénéficiaires

Les plateformes créent des sous-comptes (vendeurs, chauffeurs, agents) puis répartissent chaque paiement automatiquement.

POST/v1/accounts
{ "name": "Boutique Kamga", "phone": "677555444" }  → { "id": "4b1f…", … }
POST /v1/payments
{ "amount": 10000, "channel": "ORANGE", "phone": "699111222",
  "split": [ { "merchant": "4b1f…", "amount": 8000 } ] }

Le sous-compte reçoit 8 000 FCFA ; vous recevez le reste après frais.

GET/v1/accounts
renvoie les sous-comptes et leurs soldes.

Solde

GET/v1/balance
{ "object": "balance", "livemode": true, "currency": "XAF", "available": 1254300, "payouts_in_progress": 0 }

Reversements

POST/v1/payouts
{ "amount": 50000, "channel": "MTN", "phone": "677333222",
  "beneficiary_name": "Fournisseur Ngo", "reference": "FAC-889" }
→ { "id": "po_…", "status": "processing", "fee": 500, … }

Les fonds sont réservés immédiatement ; si l'opérateur refuse, le montant et les frais sont recrédités automatiquement (payout.failed). Un plafond quotidien s'applique selon votre profil.

Paiements de masse

POST/v1/payout_batches
{ "label": "Salaires novembre", "approve": true,
  "items": [ { "amount": 85000, "channel": "MTN", "phone": "677100001", "name": "Awa" },
             { "amount": 92000, "channel": "ORANGE", "phone": "699100002", "name": "Paul" } ] }

Si la double validation est activée sur votre compte, le lot reste en brouillon et doit être approuvé dans l'espace marchand par un autre membre.

Webhooks

POST/v1/webhook_endpoints
{ "url": "https://votre-site.cm/bluepay/webhook", "events": ["payment.succeeded", "payout.failed"] }
→ { "id": "we_…", "secret": "whsec_…" }   // secret affiché une seule fois

Événements : payment.succeeded, payment.failed, payment.expired, payment.refunded, payout.succeeded, payout.failed, payout_batch.approved. Répondez 2xx en moins de 10 secondes ; sinon BluePay réessaie pendant 72 heures. Chaque événement peut être renvoyé depuis l'espace marchand.

{ "id": "evt_…", "type": "payment.succeeded", "livemode": true, "created": "…", "data": { "id": "pay_…", "status": "succeeded", … } }

Vérifier la signature

L'en-tête BluePay-Signature vaut t=<horodatage>,v1=<signature>, où la signature est le HMAC-SHA256, avec votre secret, de t + "." + corps brut. Refusez les messages de plus de 5 minutes.

Node.js

import crypto from 'node:crypto';
function verify(secret, rawBody, header) {
  const p = Object.fromEntries(header.split(',').map((x) => x.split('=')));
  if (Math.abs(Date.now() / 1000 - Number(p.t)) > 300) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${p.t}.${rawBody}`).digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1));
}

PHP

function bluepay_verify(string $secret, string $raw, string $header): bool {
  parse_str(str_replace(',', '&', $header), $p);
  if (abs(time() - (int)$p['t']) > 300) return false;
  $expected = hash_hmac('sha256', $p['t'] . '.' . $raw, $secret);
  return hash_equals($expected, $p['v1']);
}

BluePay · Blue Energy Holding · Douala