Ce que votre code appelle
Encaissements, décaissements, devis de frais, soldes, remboursements, webhooks. Authentification signée, montants en XAF entiers, idempotence obligatoire sur toute écriture d'argent.
Commencer par l'authentification →Le paiement connecté, simple et fiable.
B-Cash expose une API REST pour encaisser et décaisser en francs CFA via les opérateurs Mobile Money camerounais, une console pour piloter votre activité, et un HUB qui exécute réellement les opérations. Cette documentation couvre chacun de ces nœuds.
Encaissements, décaissements, devis de frais, soldes, remboursements, webhooks. Authentification signée, montants en XAF entiers, idempotence obligatoire sur toute écriture d'argent.
Commencer par l'authentification →Organisations, rôles, dossier d'activation, projets et clés, journal des livraisons de webhooks, règlements et audit.
Découvrir la console →La couche qui achemine chaque opération vers l'opérateur concerné, en suit l'issue et la reporte dans le registre. Vous observez des statuts, pas des mécanismes.
Ce que cela change pour vous →Extension WooCommerce, applications mobiles via jeton éphémère, ou intégration directe depuis votre propre backend.
Choisir son intégration →| Rôle | Hôte | Usage |
|---|---|---|
| API | api.gba-cm.online | Tous les appels /api/v1/* et /api/dashboard/* |
| Console marchand | dashboard.gba-cm.online | Interface marchand et back-office |
| Documentation | docs.gba-cm.online | Cette page |
Ces trois valeurs sont centralisées dans la constante HOSTS en bas du fichier :
modifiez-les à un seul endroit, tous les exemples de la page se mettent à jour.
+237650000000.null ont le même sens : la valeur n'est pas connue.Cinq nœuds, un seul chemin pour l'argent. Comprendre qui parle à qui évite la moitié des questions de support.
| Nœud | Responsabilité | Qui s'y authentifie |
|---|---|---|
API marchande/api/v1/* |
Enregistre l'intention de paiement, calcule les frais, tient le registre des soldes, garantit l'idempotence, émet les événements. | Vos serveurs (clé HMAC) et vos clients mobiles (jeton éphémère) |
Console/api/dashboard/* |
Lecture et pilotage humain : transactions, soldes, équipe, dossier d'activation, clés, audit. | Vos utilisateurs (JWT de session) |
| Orchestration | Relaie l'opération vers l'opérateur, en suit l'issue et la reporte dans le registre. | Interne — aucune intégration marchande |
| Webhooks | Pousse les changements d'état vers vos systèmes, avec signature et reprise automatique. | B-Cash vers vous (signature HMAC à vérifier) |
| Opérateurs | Débitent ou créditent réellement le compte Mobile Money du client. | Hors périmètre B-Cash |
Votre code ne parle jamais aux opérateurs. Il crée une transaction, puis il attend : soit en interrogeant la transaction, soit — de préférence — en recevant un webhook.
Du compte vide au premier encaissement de test réussi.
Ouvrez un compte sur la console. Un projet sandbox et ses identifiants sont générés immédiatement. Le secret n'est affiché qu'une fois : copiez-le avant de fermer.
# Sonde d'authentification : ne consomme rien, ne crée rien curl https://api.gba-cm.online/api/v1/me \ -H "X-BCash-Key: $BCASH_KEY" \ -H "X-BCash-Timestamp: $(date +%s)" \ -H "X-BCash-Signature: $SIGNATURE"
Le calcul de $SIGNATURE est décrit dans Authentification.
Une réponse 200 confirme que clé, secret et horloge sont corrects.
{
"url": "https://boutique.example.cm/webhooks/bcash",
"events": ["transaction.succeeded", "transaction.failed"]
}
# → 201 { "reference": "whk_…", "secret": "whsec_…" } ← le secret n'apparaît qu'ici
curl -X POST https://api.gba-cm.online/api/v1/collections \ -H "X-BCash-Key: $BCASH_KEY" \ -H "X-BCash-Timestamp: $(date +%s)" \ -H "X-BCash-Signature: $SIGNATURE" \ -H "Idempotency-Key: 8f1c2b4e-3a77-4c11-9f2d-6b0a5e91c7d3" \ -H "Content-Type: application/json" \ -d '{ "amount": 25000, "operator": "MTN", "customer": { "phone": "+237650000000" }, "merchantReference": "CMD-1042", "context": "API_DIRECT" }'
En test, personne ne va taper un code secret : provoquez vous-même le dénouement.
{ "target": "SUCCEEDED" }
# La transaction suit le chemin nominal : écritures comptables,
# webhook transaction.succeeded livré et signé, trace d'audit.
Votre endpoint vient de recevoir un événement signé. Il reste à en vérifier la signature (voir la marche à suivre) et à passer votre commande en « payée ».
Deux environnements, la même API, des clés distinctes qui ne se croisent jamais.
| Sandbox | Production | |
|---|---|---|
| Disponible | Dès l'inscription | Après approbation de votre dossier |
| Argent | Aucun mouvement réel | Débits et crédits réels |
| Issue d'une transaction | Vous la forcez (/sandbox/…/advance) | Le client confirme sur son téléphone |
| Webhooks | Livrés, signés, rejouables | Identiques |
Champ livemode | false | true |
X-BCash-Key. Visible dans la console.Perdu, il ne peut pas être ré-affiché. Générez-en un nouveau : par défaut, l'ancien reste actif pendant votre déploiement, et vous le révoquez une fois la bascule terminée.
La révocation immédiate existe aussi, pour un secret compromis : elle coupe l'ancienne clé à l'instant,
et toute intégration encore en train de l'utiliser reçoit INVALID_SIGNATURE. La console vous
demande de ressaisir le nom du projet avant de l'appliquer.
Trois manières de s'authentifier, pour trois natures d'appelant. Choisir la bonne est la première décision d'architecture d'une intégration.
| Méthode | Pour qui | En-têtes |
|---|---|---|
| Clé HMAC | Vos serveurs, et eux seuls | X-BCash-Key, X-BCash-Timestamp, X-BCash-Signature |
| Jeton éphémère | Application mobile ou page web | Authorization: Bearer bt_… |
| JWT de session | Utilisateurs de la console | Authorization: Bearer … |
Une application mobile est décompilable et une page web est lisible. Toute clé qui y est embarquée est publique le jour de la première installation. Les clients utilisent le jeton éphémère, minté par votre backend.
La chaîne à signer concatène quatre éléments séparés par des sauts de ligne :
METHOD + "\n" + PATH + "\n" + TIMESTAMP + "\n" + sha256_hex(corps) # METHOD : verbe HTTP en majuscules, ex. POST # PATH : chemin seul, ex. /api/v1/collections (sans hôte, sans query) # TIMESTAMP : secondes Unix, identique à l'en-tête X-BCash-Timestamp # corps : le corps JSON exact envoyé ; chaîne vide en GET
$body = json_encode($payload, JSON_UNESCAPED_SLASHES); $timestamp = (string) time(); $toSign = "POST\n/api/v1/collections\n{$timestamp}\n" . hash('sha256', $body); $signature = hash_hmac('sha256', $toSign, $secret); // Envoyez exactement le même $body : re-sérialiser après signature invalide la requête.
const body = JSON.stringify(payload); const ts = Math.floor(Date.now() / 1000).toString(); const digest = crypto.createHash('sha256').update(body).digest('hex'); const signature = crypto .createHmac('sha256', secret) .update(`POST\n/api/v1/collections\n${ts}\n${digest}`) .digest('hex');
Un horodatage trop éloigné de l'heure du serveur fait échouer la signature. Synchronisez vos machines (NTP). Si vos appels échouent d'un coup sans changement de code, vérifiez l'heure système avant tout le reste.
Votre backend échange sa clé HMAC contre un jeton court, qu'il remet au client. Le jeton porte un sous-ensemble strict des scopes de la clé et vit au maximum 900 secondes.
// Requête, signée en HMAC depuis VOTRE serveur { "scopes": ["collections:create", "transactions:read"], "ttl": 600 } // Réponse { "token": "bt_9f2c…", "expiresIn": 600, "scopes": ["collections:create", "transactions:read"] }
Bearer bt_….Le réseau mobile coupe. La règle qui garantit qu'une coupure ne débite jamais deux fois votre client.
Toute route qui déplace de l'argent exige l'en-tête Idempotency-Key, un UUID que
vous générez et que vous conservez le temps de l'opération.
| Situation | Réponse de l'API |
|---|---|
| Première requête avec cette clé | L'opération est exécutée, la réponse est mémorisée |
| Même clé, même corps | La réponse mémorisée est renvoyée — aucune seconde opération |
| Même clé, corps différent | 409 IDEMPOTENCY_CONFLICT |
| En-tête absent | 400 — la requête est refusée |
Rattachez la clé à l'intention métier, pas à la tentative technique. Une commande qu'on paie une fois mérite une clé stable ; si vous en générez une nouvelle à chaque clic, vous autorisez le double paiement que l'idempotence était censée empêcher.
// À la création de la commande, une fois pour toutes : order.paymentIdempotencyKey ??= crypto.randomUUID(); // Toutes les tentatives de paiement de CETTE commande réutilisent cette clé.
Si vous perdez la réponse, ne recréez rien : interrogez.
Ces deux routes existent précisément pour cette situation. La seconde suppose que vous ayez
renseigné merchantReference à la création — faites-le systématiquement.
Le franc CFA n'a pas de sous-unité. Cette simplicité est un piège pour qui vient d'une API en centimes.
amount est un entier strictement positif : 25000 signifie
vingt-cinq mille francs.currency vaut XAF. Le champ existe pour l'avenir ; aujourd'hui il n'a
qu'une valeur.amount (ce qui est demandé),
feeAmount (les frais) et netAmount (ce qui vous reste). Affichez celui
qui correspond à la question posée, pas celui qui est le plus flatteur.Toutes les erreurs partagent la même enveloppe, et chacune porte de quoi être retrouvée par le support.
{
"error": {
"code": "INSUFFICIENT_FUNDS", // code machine stable
"message": "Solde insuffisant.", // texte lisible, susceptible d'évoluer
"correlationId": "c7f3a91b4e2d", // à citer au support
"retryable": false, // rejouer peut-il aider ?
"retryAfter": null, // délai conseillé, en secondes
"details": {} // contexte selon le code
}
}
code, jamais sur message : le texte
peut être reformulé, le code est un contrat.retryable: true — rejouez la même requête avec la
même Idempotency-Key, en respectant retryAfter.retryable: false — rejouer ne changera rien. Corrigez la requête
ou remontez l'erreur à l'utilisateur.correlationId à côté de votre référence de commande.
C'est ce qui transforme un ticket de support en diagnostic de trois minutes.| Statut | Sens | Ce qu'il faut faire |
|---|---|---|
| 400 | Requête malformée | Corriger le corps ou les en-têtes |
| 401 | Signature ou jeton invalide | Vérifier clé, secret, horloge |
| 403 | Scope manquant, ou environnement interdit | Vérifier les scopes du credential |
| 404 | Ressource inexistante dans votre périmètre | Vérifier la référence |
| 409 | Conflit d'état ou d'idempotence | Lire code : l'état actuel interdit l'action |
| 422 | Requête bien formée mais métier invalide | Lire details |
| 429 | Trop de requêtes | Attendre retryAfter |
| 500 | Incident côté B-Cash | Rejouer avec la même clé, puis signaler le correlationId |
| 503 | Service temporairement indisponible | Rejouer avec un délai croissant |
Le catalogue exhaustif des codes machine est publié par l'API elle-même :
GET /api/v1/meta → errors.catalog. Consommez-le plutôt que de recopier
une liste qui vieillira.
Les collections sont paginées côté serveur. Ne rapatriez jamais tout pour filtrer en mémoire.
| Paramètre | Rôle |
|---|---|
page | Numéro de page, à partir de 1 |
itemsPerPage | Taille de page ; plafonnée côté serveur |
status | Filtre sur un statut de la liste stable |
merchantReference | Votre propre référence |
La réponse porte le total ; ne le recalculez pas à partir du nombre d'éléments reçus.
La liste est dynamique : elle porte l'état de service de chaque opérateur.
| État | Sens | Comportement attendu de votre interface |
|---|---|---|
| ACTIVE | Service nominal | Proposer l'opérateur |
| DEGRADED | Lenteurs ou taux d'échec anormal | Proposer, en signalant un délai possible |
| DOWN | Indisponible | Masquer ou griser, sans redéploiement |
Le champ operator est obligatoire à la création. Si vous figez « MTN et Orange » dans
votre code, une panne opérateur devient une mise en production chez vous. En lisant cette route,
elle devient un simple changement d'affichage.
Deux questions différentes se cachent derrière « combien ça coûte ». L'API vous demande laquelle vous posez.
amountBasis | Ce que représente amount | Qui absorbe les frais |
|---|---|---|
GROSS (défaut) | Le montant débité au client | Vous. merchantNetAmount = amount − frais |
ORDER_NET | Le net que vous voulez encaisser | Le client. customerPayableAmount = amount + frais, calculé exactement |
// Requête { "type": "COLLECTION", "context": "WOOCOMMERCE", "amount": 25000, "amountBasis": "ORDER_NET", "operator": "MTN" } // Réponse { "reference": "fq_7Q4K…", "orderAmount": 25000, "customerFeeAmount": 500, "customerPayableAmount": 25500, // ← à utiliser comme "amount" à la création "merchantNetAmount": 25000, "feePayer": "CUSTOMER", "ruleVersion": "2026-07-01", "expiresAt": "2026-07-27T10:15:00Z" // 900 s }
Pour garantir au client le prix affiché, passez feeQuoteId à la création — et
amount doit alors valoir exactement customerPayableAmount.
Toute autre valeur est refusée : c'est la protection contre l'écran qui affiche un prix et l'API
qui en débite un autre.
ruleVersion identifie la grille appliquée : conservez-la avec votre commande, elle
explique un écart six mois plus tard.La route à utiliser dans neuf cas sur dix : elle fixe le type et résout votre wallet destinataire toute seule.
collections:create| Champ | Requis | Description |
|---|---|---|
amount | oui | Entier XAF. Avec feeQuoteId, doit égaler customerPayableAmount. |
operator | oui | Code issu de GET /v1/operators. |
customer.phone | oui | Format international, ex. +237650000000. |
customer.displayName | non | Nom affiché côté suivi. |
merchantReference | non | Votre référence — mettez-la, elle vous sauvera un jour. |
feeQuoteId | non | Devis à consommer. |
context | non | Origine de l'appel : WOOCOMMERCE, MOBILE_APP, API_DIRECT… |
metadata | non | Objet plat, 32 clés max, 4 096 octets max. Préfixe bcash_ réservé. |
callbackUrl | — | Déprécié. Accepté, mais jamais utilisé pour livrer. |
Elles apparaissent dans la console et dans le corps des webhooks. N'y mettez ni pièce d'identité, ni mot de passe, ni donnée bancaire : c'est un champ de contexte, pas un coffre.
{
"reference": "TRX7QK4M2",
"type": "COLLECTION",
"status": "PENDING_CONFIRMATION",
"amount": 25500,
"feeAmount": 500,
"netAmount": 25000,
"merchantReference": "CMD-1042",
"nextAction": {
"type": "AWAIT_MOBILE_CONFIRMATION",
"displayMessage": "Confirmez le paiement sur votre téléphone.",
"expiresAt": "2026-07-27T10:05:00Z"
},
"createdAt": "2026-07-27T10:00:00Z"
}
nextActionCe bloc vous dit quoi afficher pendant l'attente, sans que vous ayez à inventer un texte par opérateur :
AWAIT_MOBILE_CONFIRMATION — le client doit valider sur son téléphone. Affichez
displayMessage et un compte à rebours vers expiresAt.AWAIT_PROCESSING — rien à faire pour le client, l'exécution est en cours.expiresAt fait autorité. Un écran qui abandonne avant cette date affiche un échec
pour une transaction qui va réussir — et votre client paiera deux fois.
Envoyer de l'argent depuis votre solde vers un numéro Mobile Money.
payouts:create{
"amount": 50000, // ce que REÇOIT le bénéficiaire
"operator": "ORANGE",
"recipient": { "phone": "+237690000000", "name": "Fournisseur ALBA" },
"merchantReference": "FACT-2026-118"
}
amount
amount est le montant qui arrive chez le bénéficiaire ; les frais s'ajoutent au débit.
Demandez un devis avant si vous devez afficher le coût total à un utilisateur.
Un décaissement échoue si le solde disponible est insuffisant : le montant nécessaire est réservé dès la création, puis libéré en cas d'échec.
Douze statuts, quatre terminaux. Une transaction n'est jamais « peut-être payée ».
| Statut | Sens | Terminal |
|---|---|---|
| CREATED | La transaction est enregistrée, rien n'est engagé. | non |
| FEE_QUOTED | Les frais sont fixés pour cette transaction. | non |
| HELD | Les fonds nécessaires sont réservés sur le wallet source. | non |
| QUEUED | En file d'exécution. Attend un worker ou un terminal. | non |
| PROCESSING | Exécution en cours chez l'opérateur. Plus annulable. | non |
| PENDING_CONFIRMATION | Le client doit saisir son code secret. | non |
| SUCCEEDED | L'argent a bougé. Livrez la commande. | oui |
| FAILED | Refus, solde insuffisant, mauvais code… Aucun mouvement. | oui |
| CANCELLED | Annulée avant exécution. Fonds réservés libérés. | oui |
| EXPIRED | Le client n'a pas confirmé à temps. | oui |
| REVERSED | Intégralement remboursée. | oui, après SUCCEEDED |
| PENDING_RECONCILIATION | Issue indéterminée : ni succès, ni échec. En cours de rapprochement. | non |
PENDING_RECONCILIATION comme un échec
Ce statut signifie exactement : « l'argent a peut-être bougé, nous vérifions ». Ni livraison, ni remboursement, ni nouvelle tentative tant que la transaction n'a pas basculé vers un statut terminal. Le traiter comme un échec, c'est risquer de livrer deux fois ou de rembourser un paiement qui n'a jamais eu lieu. La résolution est décrite dans Quand l'issue n'est pas confirmée.
Deux stratégies, et l'une est nettement meilleure :
Possible uniquement avant exécution : CREATED, FEE_QUOTED,
HELD, QUEUED. Une transaction en PROCESSING ne s'annule pas —
l'argent est peut-être déjà en mouvement. L'appel libère les fonds réservés et reste idempotent :
annuler une transaction déjà annulée renvoie 200.
refunds:createCOLLECTION et MERCHANT_PAYMENT au statut SUCCEEDED.REFUND,
reliée par parentReference.amount, le restant dû est intégralement remboursé ; sinon le remboursement
est partiel et peut être répété.REVERSED et l'événement transaction.reversed est émis.POST /api/v1/transactions/TRX7QK4M2/refund
Idempotency-Key: 3d9a1c77-2f40-4b8e-9c15-77e0a2b4d611
{ "amount": 5000, "reason": "Article manquant" }
Un solde n'est pas un nombre unique : trois compartiments cohabitent.
| Compartiment | Ce qu'il contient |
|---|---|
available | Utilisable immédiatement : décaissement, règlement. |
reserved | Immobilisé par des opérations en cours. Ni disponible, ni perdu. |
pending | Encaissements dont l'issue n'est pas encore consolidée. |
Le wallet marchand est créé automatiquement au premier encaissement. Un wallet peut être gelé par la conformité : la consultation reste possible, les mouvements sont bloqués. Le gel d'un wallet et la suspension d'une organisation sont deux mécanismes distincts.
Vous n'attendez pas l'API : c'est elle qui vous appelle quand l'état change.
{
"id": "evt_9K2mQ7…", // stable entre les tentatives → clé de déduplication
"type": "transaction.succeeded",
"apiVersion": "2026-07-20",
"createdAt": "2026-07-27T10:02:13Z",
"livemode": true, // false en sandbox
"data": {
"object": { /* la transaction complète */ },
"previousAttributes": { "status": "PENDING_CONFIRMATION" }
}
}
id. Le même evt_… peut arriver deux fois ;
votre traitement doit être sans effet la seconde fois.transaction.succeeded peut précéder un
transaction.pending_confirmation. Fiez-vous au statut porté par l'objet, pas à l'ordre
d'arrivée.La seule étape non négociable de l'intégration.
| En-tête | Contenu |
|---|---|
X-BCash-Timestamp | Secondes Unix de l'émission |
X-BCash-Signature | HMAC_SHA256(secret, timestamp + "\n" + corps_brut) |
X-BCash-Signature-Previous | Présent 24 h après une rotation : signature avec l'ancien secret |
// Le corps BRUT est indispensable : un JSON re-sérialisé ne redonne pas les mêmes octets. app.post('/webhooks/bcash', express.raw({ type: 'application/json' }), (req, res) => { const ts = req.get('X-BCash-Timestamp'); const raw = req.body.toString('utf8'); // Fenêtre anti-rejeu : on refuse un événement trop ancien. if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.sendStatus(400); const expected = crypto.createHmac('sha256', process.env.BCASH_WEBHOOK_SECRET) .update(`${ts}\n${raw}`).digest('hex'); const candidates = [req.get('X-BCash-Signature'), req.get('X-BCash-Signature-Previous')].filter(Boolean); const valid = candidates.some(sig => sig.length === expected.length && crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))); // comparaison à temps constant if (!valid) return res.sendStatus(401); res.sendStatus(200); // on accuse réception AVANT de traiter queue.push(JSON.parse(raw)); // traitement asynchrone, dédupliqué sur event.id });
$raw = file_get_contents('php://input'); $ts = $_SERVER['HTTP_X_BCASH_TIMESTAMP'] ?? ''; if (abs(time() - (int) $ts) > 300) { http_response_code(400); exit; } $expected = hash_hmac('sha256', "{$ts}\n{$raw}", $secret); $ok = hash_equals($expected, $_SERVER['HTTP_X_BCASH_SIGNATURE'] ?? '') || hash_equals($expected, $_SERVER['HTTP_X_BCASH_SIGNATURE_PREVIOUS'] ?? ''); if (!$ok) { http_response_code(401); exit; }
Utiliser le corps déjà parsé au lieu des octets bruts ; comparer les signatures avec ==
au lieu d'une comparaison à temps constant ; ignorer X-BCash-Signature-Previous et casser
ses livraisons pendant les 24 heures d'une rotation.
webhooks:writewebhook.testLa référence d'un endpoint a la forme whk_… et son secret whsec_….
Le secret n'est affiché qu'à la création et à la rotation ; l'objet ne renvoie ensuite que
secretLast4.
Le nouveau secret signe immédiatement dans X-BCash-Signature, tandis que l'ancien
co-signe dans X-BCash-Signature-Previous pendant 24 heures. Si votre
vérification accepte les deux en-têtes, la rotation ne fait perdre aucune livraison. La date de fin
de chevauchement est lisible dans previousSecretValidUntil.
Le champ events vide signifie « tous ». Restreindre est plus sain : votre endpoint
ne reçoit que ce qu'il sait traiter, et les événements ajoutés plus tard ne le surprennent pas.
| Situation | Comportement |
|---|---|
| Vous répondez 2xx | Livraison réussie, terminée. |
| Vous répondez autre chose, ou ne répondez pas | Nouvelles tentatives espacées, à intervalles croissants. |
| Échecs répétés | L'endpoint bascule en FAILING ; la console vous alerte. |
| Endpoint durablement injoignable | Peut être désactivé ; réactivez-le après correction. |
La console conserve chaque tentative : code HTTP, délai de réponse, corps renvoyé. Une livraison peut être rejouée manuellement.
La relivraison renvoie une notification, jamais un mouvement d'argent. En revanche, si
votre traitement n'est pas dédupliqué sur id, vous pouvez expédier deux fois la même
commande. La déduplication est votre responsabilité.
Les paramètres exacts (nombre de tentatives, intervalles, fenêtre de tolérance) sont publiés par
l'API : GET /api/v1/meta → webhookDelivery. Lisez-les plutôt que de les
supposer ; ils peuvent être ajustés sans changement de version majeure.
Le même code, les mêmes réponses, les mêmes webhooks — sans un franc en jeu.
Cette route force l'issue d'une transaction non terminale, en empruntant le chemin nominal : écritures comptables, webhooks signés, trace d'audit. Ce n'est pas une simulation de façade — c'est la vraie mécanique, déclenchée à la main.
{ "target": "SUCCEEDED" } // ou "FAILED", "EXPIRED"
Appelée avec un credential de production, elle répond 403. La liste
à jour des scénarios est publiée dans GET /api/v1/meta → sandbox.
Ce qu'il faut avoir essayé avant de demander l'accès production. Le chemin heureux ne prouve presque rien.
| Scénario | Comment le provoquer | Ce que votre système doit faire |
|---|---|---|
| Paiement réussi | advance → SUCCEEDED | Marquer payé une seule fois, livrer |
| Paiement refusé | advance → FAILED | Message clair, commande réouvrable |
| Client qui ne confirme pas | advance → EXPIRED | Libérer le panier, proposer de réessayer |
| Webhook reçu deux fois | Rejouer la livraison depuis la console | Traitement sans effet la seconde fois |
| Webhook hors ordre | Rejouer un événement ancien | Se fier au statut, pas à l'ordre d'arrivée |
| Coupure après l'appel | Interrompre la requête, puis rejouer la même clé | Une seule transaction créée |
| Signature invalide | Poster sur votre endpoint sans signature | Rejet en 401 |
| Rotation de secret | Faire tourner le secret de l'endpoint | Aucune livraison perdue pendant 24 h |
| Opérateur indisponible | Lire un opérateur DOWN | Le masquer dans le sélecteur |
| Remboursement partiel puis total | Deux appels /refund | Passage à REVERSED géré |
Les clés sandbox restent actives après le passage en production : votre environnement de recette continue de fonctionner exactement comme avant.
La règle est appliquée côté serveur, pas seulement masquée dans l'interface. Une tentative de
création renvoie ORG_NOT_APPROVED. De même, une organisation suspendue ne peut plus
créer de credential tant que la suspension court.
Une seule console pour les marchands, les développeurs, le support et l'administration : le menu et les droits diffèrent, pas l'application.
Recherche par référence, statut, période ; détail complet avec le fil des événements et le correlation ID à citer au support.
Un projet par intégration, ses credentials par environnement, ses endpoints de notification et leur santé sur sept jours.
Invitations, rôles, progression d'activation, historique des décisions de conformité.
Demande de versement vers le numéro Mobile Money validé dans votre dossier, avec suivi jusqu'à réception.
La console s'authentifie en JWT de session et consomme les routes
/api/dashboard/*. Elle n'utilise jamais la signature HMAC : celle-ci reste réservée
aux échanges de serveur à serveur.
Une organisation est l'entité qui encaisse. Son statut détermine ce qui est permis.
| Statut | Sens | Sandbox | Production |
|---|---|---|---|
| Brouillon | Créée, dossier non soumis | oui | non |
| Profil incomplet | Informations obligatoires manquantes | oui | non |
| Prêt à soumettre | Dossier complet côté marchand | oui | non |
| En vérification | Examiné par la conformité ; dossier figé | oui | non |
| Action requise | Complément demandé, motif affiché | oui | non |
| Production active | Approuvée | oui | oui |
| Suspendue | Opérations de production bloquées | lecture | non |
| Refusée | Demande rejetée, motif consultable | oui | non |
| Clôturée | Relation terminée | non | non |
Profil incomplet et Prêt à soumettre ne sont pas stockés : ils sont déduits en temps réel de ce qui manque réellement. Un statut recalculable et stocké finit toujours par mentir sur l'état du dossier.
{
"organizationStatus": "READY_TO_SUBMIT",
"completionPercent": 83,
"steps": [
{ "code": "EMAIL_VERIFIED", "status": "DONE" },
{ "code": "SANDBOX_PROJECT", "status": "DONE" },
{ "code": "FIRST_SANDBOX_TX", "status": "DONE" },
{ "code": "ORGANIZATION_PROFILE", "status": "DONE" },
{ "code": "PHONE_VERIFIED", "status": "DONE" },
{ "code": "APPLICATION_FILE", "status": "IN_PROGRESS" }
],
"nextAction": "SUBMIT_APPLICATION_FILE",
"statusReason": null
}
La liste dépend de votre forme juridique et s'affiche dans votre dossier. Le socle commun : pièce d'identité du représentant (recto et verso) et preuve de titularité du numéro Mobile Money de règlement. Les sociétés y ajoutent l'extrait RCCM et l'attestation NIU.
Un utilisateur peut appartenir à plusieurs organisations, avec un rôle différent dans chacune.
| Rôle | Peut | Ne peut pas |
|---|---|---|
| Propriétaire | Tout, y compris soumettre le dossier et transférer la propriété | — |
| Administrateur | Gérer l'équipe, les projets, les clés de production, le profil | Soumettre le dossier, transférer la propriété |
| Développeur | Projets, clés sandbox, endpoints, rotation, tests | Créer la première clé de production, gérer l'équipe |
| Finance | Transactions, soldes, règlements, initiation d'opérations | Toucher aux clés ou à l'équipe |
| Support | Consulter transactions et livraisons | Toute écriture |
| Lecture seule | Consulter | Toute écriture |
La bascule émet un nouveau jeton dont le périmètre est l'organisation choisie. Un jeton porte un seul périmètre : il n'existe pas d'en-tête permettant de viser une autre organisation au fil des requêtes.
Un projet représente une intégration : une boutique, une application, un backend.
Les mêmes données que l'interface, si vous souhaitez construire vos propres tableaux de bord.
/api/dashboard/operations crée la transaction et la met en file (QUEUED) ;
c'est un worker qui l'exécute et poste les écritures. Une transaction qui reste indéfiniment en
QUEUED signale un worker à l'arrêt, pas un problème de contrat.
Aucun secret, aucune signature, aucun numéro complet n'apparaît pour un rôle non autorisé. La consultation d'une pièce justificative passe par un lien signé de courte durée et laisse elle-même une trace d'audit.
Ce que devient une transaction entre votre appel d'API et le compte Mobile Money de votre client.
Une fois la transaction enregistrée, B-Cash la confie à sa couche d'orchestration. C'est elle qui relaie l'opération vers l'opérateur concerné, en suit l'issue et la reporte dans le registre. Votre intégration n'a rien à en connaître : elle observe des statuts, pas des mécanismes.
GET /v1/operators : lisez-le plutôt que de supposer que
tout est toujours joignable.Il arrive qu'une opération ne puisse pas être conclue immédiatement : l'opérateur ne répond pas de manière exploitable, ou la confirmation n'arrive pas dans la fenêtre attendue. La transaction passe alors en PENDING_RECONCILIATION et les fonds concernés restent réservés, le temps que le rapprochement tranche.
Le dénouement est automatique : la transaction rejoint un statut terminal et l'événement correspondant vous est livré. Aucune action ne vous est demandée — et c'est précisément le point important.
Sur transaction.pending_reconciliation : n'expédiez pas, ne remboursez pas, ne
relancez pas. Ce statut ne signifie pas « échec », il signifie « pas encore tranché ». Informez
le client que son paiement est en cours de vérification et attendez l'événement terminal.
Une nouvelle tentative pendant ce délai est le moyen le plus sûr de le débiter deux fois.
Ces situations sont rares et se résolvent généralement en quelques minutes. Si une transaction reste dans cet état au-delà de ce qui vous paraît raisonnable, écrivez au support avec sa référence : le rapprochement peut être accéléré manuellement.
Le chemin le plus court : votre boutique encaisse sans que vous écriviez de code.
| Événement B-Cash | Effet attendu sur la commande |
|---|---|
transaction.succeeded | Commande payée, traitement lancé |
transaction.failed | Commande en échec, panier réouvrable |
transaction.expired | Commande annulée, stock libéré |
transaction.pending_reconciliation | Commande en attente — ne rien expédier |
transaction.reversed | Commande remboursée |
Le client peut fermer son téléphone avant la redirection, ou la rouvrir plus tard. Seul le webhook fait foi. La page de retour affiche un état d'attente ; c'est l'événement qui déclenche la livraison.
Le context transmis par l'extension vaut WOOCOMMERCE et le numéro de
commande est envoyé dans merchantReference : vous retrouvez ainsi n'importe quelle
transaction depuis votre back-office WooCommerce.
Le schéma est simple, à condition de ne jamais inverser les rôles : c'est votre backend qui détient le secret, jamais l'application.
App mobile Votre backend API B-Cash
│ │ │
│ « je veux payer » │ │
│────────────────────>│ │
│ │ POST /v1/auth/token │ (signé HMAC)
│ │───────────────────────>│
│ │<── bt_… (TTL 600 s) ───│
│<── jeton éphémère ──│ │
│ │
│ POST /v1/collections (Bearer bt_…) │
│─────────────────────────────────────────────>│
│<── nextAction : AWAIT_MOBILE_CONFIRMATION ───│
│ │
│ Écran d'attente jusqu'à expiresAt │
│ │<── webhook signé ──────│
│<── votre push / │ │
│ votre polling ───│ │
nextAction.displayMessage plutôt qu'un texte figé : il est adapté à
l'opérateur choisi.expiresAt : compte à rebours visible, et surtout aucune conclusion avant
cette échéance.Embarquer la clé HMAC dans l'application, même « obfusquée ». Une application se décompile en quelques minutes, et une clé extraite permet d'encaisser en votre nom jusqu'à sa révocation.
Intégration directe : vous contrôlez tout, vous portez donc tout.
| Point | Vérification |
|---|---|
| Secrets | Hors du dépôt, en variables d'environnement ou coffre. Jamais dans un fichier versionné. |
| Horloge | NTP actif sur tous les serveurs appelants. |
| Idempotence | Clé rattachée à la commande, persistée avec elle. |
| Webhooks | Signature vérifiée en temps constant, corps brut, deux en-têtes acceptés. |
| Déduplication | Table des evt_… traités, contrainte d'unicité. |
| Journalisation | correlationId et reference stockés avec la commande. |
| Filet de sécurité | Tâche périodique qui interroge les transactions non terminales anciennes. |
| Statuts | PENDING_RECONCILIATION traité comme une attente, pas comme un échec. |
| Opérateurs | Liste lue depuis l'API, pas codée en dur. |
| Rotation | Procédure écrite et testée au moins une fois en sandbox. |
async function handleEvent(event) { // 1. Déduplication : la contrainte d'unicité fait le travail if (await events.exists(event.id)) return; await events.insert(event.id); const tx = event.data.object; const order = await orders.findByReference(tx.merchantReference); if (!order) return; // événement d'un autre système // 2. On se fie au STATUT porté par l'objet, jamais à l'ordre d'arrivée switch (tx.status) { case 'SUCCEEDED': return order.markPaid(tx); case 'FAILED': case 'EXPIRED': return order.markUnpaid(tx); case 'REVERSED': return order.markRefunded(tx); case 'PENDING_RECONCILIATION': return order.markUnderReview(tx); // on attend default: return; // état transitoire } }
CREATED, FEE_QUOTED, HELD, QUEUED,
PROCESSING, PENDING_CONFIRMATION, SUCCEEDED,
FAILED, CANCELLED, REVERSED, EXPIRED,
PENDING_RECONCILIATION. Détail en Cycle de vie.
| Type | Sens |
|---|---|
COLLECTION | Encaissement Mobile Money vers votre wallet |
MERCHANT_PAYOUT | Versement de votre wallet vers un numéro |
MERCHANT_PAYMENT | Paiement réglé depuis un wallet client |
WALLET_TOPUP | Alimentation d'un wallet |
CASH_OUT | Retrait depuis un wallet |
REFUND | Remboursement, lié par parentReference |
POST /v1/transactions accepte tous les types, mais préférez
/v1/collections et /v1/payouts : elles fixent le type et résolvent
le wallet à votre place, ce qui élimine une classe entière d'erreurs.
| Événement | Émis quand | Action typique |
|---|---|---|
transaction.created | La transaction est enregistrée | Journaliser |
transaction.pending_confirmation | Le client doit confirmer | Afficher l'attente |
transaction.succeeded | L'argent a bougé | Livrer |
transaction.failed | Échec définitif | Rouvrir le panier |
transaction.expired | Pas de confirmation à temps | Libérer le stock |
transaction.reversed | Remboursement intégral atteint | Marquer remboursé |
transaction.pending_reconciliation | Issue indéterminée | Attendre |
webhook.test | Vous déclenchez un test | Vérifier la chaîne |
La liste exhaustive et à jour est publiée dans GET /api/v1/meta. Un événement inconnu
doit être ignoré sans erreur : de nouveaux types peuvent apparaître en 1.x.
| Scope | Autorise |
|---|---|
collections:create | Créer un encaissement |
payouts:create | Créer un décaissement |
refunds:create | Rembourser |
transactions:read | Lire les transactions |
wallets:read | Lire les soldes |
fees:quote | Demander un devis |
webhooks:write | Gérer les endpoints |
Un jeton éphémère ne peut porter qu'un sous-ensemble strict des scopes de la clé qui l'a émis. Accordez à chaque credential le minimum nécessaire : une clé de boutique n'a aucune raison de pouvoir décaisser.
L'API est en version 1.3. En 1.x, ces garanties tiennent :
callbackUrl, ignoré au profit des endpoints enregistrés.Conséquence pratique : votre désérialisation doit être tolérante. Un client qui plante sur un champ inconnu cassera à la prochaine évolution mineure.
Le champ apiVersion des webhooks (ex. 2026-07-20) date le format de
l'enveloppe ; conservez-le dans vos journaux, il explique les différences entre deux événements
anciens et récents.
/v1/meta mérite d'être lu au moins une fois : il publie les énumérations, la liste
exhaustive des événements, la spécification de signature, les règles d'idempotence et de devis,
ainsi que le catalogue des codes d'erreur. Tout ce que cette page décrit y est disponible sous
forme exploitable par votre code.
Trois éléments suffisent presque toujours à trancher un incident :
correlationId de la réponse en erreur.reference de la transaction, ou votre merchantReference.Écrivez à support@gba-cm.online. N'envoyez jamais un secret ni une capture contenant une clé : si vous pensez qu'un secret a fuité, faites-le tourner d'abord, signalez ensuite.
Elle couvre les nœuds existants à la version 1.3 de l'API. Les guides pas-à-pas de l'extension WooCommerce et du SDK mobile seront enrichis avec le contenu réel de chaque paquet.