Developer documentation
The BaobaPay API for collecting payments across the CEMAC (XAF) and WAEMU (XOF) zones — mobile money, card and payment links. Your customer's payment is routed to your merchant account.
Introduction
The API is organised around REST resources, with JSON responses and standard HTTP codes. All requests go over HTTPS.
https://api.baobapay.com/api/v1 // production
https://api.sandbox.baobapay.com/api/v1 // sandboxTwo environments: sandbox (keys sk_sb_…, no real money) and production (keys sk_live_…, activated when the contract is signed).
Quickstart
Take your first mobile money payment in the sandbox, in three steps.
1. Create a mobile money pay-in
curl https://api.sandbox.baobapay.com/api/v1/payins/mobile-money \
-H "Authorization: Bearer sk_sb_9f2a…" \
-H "Idempotency-Key: 8f14e45f" \
-H "Content-Type: application/json" \
-d '{
"reference": "CMD-10412",
"amount": { "currency": "XAF", "value": 24500 },
"operator": "MTN",
"phone": "+237671224188"
}'2. Read the response
{
"id": "pi_10482",
"status": "PENDING",
"amount": { "currency": "XAF", "value": 24500 }
}3. Receive the confirmation
When the customer confirms on their phone, BaobaPay sends payin.succeeded to your webhook endpoint — see Webhooks below.
{
"type": "payin.succeeded",
"data": { "id": "pi_10482", "status": "SUCCEEDED" }
}Authentication
Every request is authenticated with a secret key passed in the Authorization header, in the Bearer format. Never expose a sk_live_ key on the client side.
Authorization: Bearer sk_sb_9f2a3b7c1d…| Prefix | Environment |
|---|---|
| sk_sb_… | Sandbox — active immediately |
| sk_live_… | Production — after the contract is signed |
401 unauthorized. Manage and regenerate your keys in the dashboard → Developers.Idempotency
All POST requests that create movements accept an Idempotency-Key header. Replaying the same key returns the original response without creating a duplicate — essential for network retries and mobile money retries.
Idempotency-Key: 8f14e45f-ceea-467d-9c1b-2a…The key is retained for 24 h. Use a unique UUID per payment intent.
Amounts & currencies
Amounts are represented by a Money object — a currency and an integer value. XAF and XOF are currencies without minor units: the value is expressed in whole francs (no cents).
{ "currency": "XAF", "value": 24500 } // 24,500 CFA francsFund model
Understanding where the money sits at any moment is the prerequisite to any integration.
Wallet topology
Two wallets are created when a platform opens on a third-party vertical — marketplace, freelance, crowdfunding, pots, travel, B2B. In direct sale (e-commerce), only the main wallet exists.
| Wallet | Holds | Who debits it |
|---|---|---|
| WLT-MAIN | Your commissions | You, to your settlement accounts |
| WLT-BENEF | Funds belonging to your third parties | You, to the third party's account only |
| WLT-SM-xxxx | A verified third party's balance | Payout to their own account |
wallet.created rather than calling POST /wallets.Segregation
A withdrawal from WLT-MAIN can only go to a settlement account of the platform. A payout from a third-party wallet can only go to that third party's account. The API refuses every cross combination — a marketplace owner cannot drain their sellers' wallet to their own account.
Withdrawal lifecycle
POST /withdrawals → status: PENDING_APPROVAL
↓ (internal approval)
withdrawal.approved → the funds have not left yet
↓ (execution by the operator)
withdrawal.paid → the wallet balance is decremented here
or
withdrawal.failed → the balance stays intact, the request can be replayedpaid. A payout approved then failed at the operator leaves the funds in place.Cash collections
Cash declarations are merchant data: no wallet is credited, no flow can be reconciled in bank. They are kept and exportable, but do not enter the balance computation.
Errors
The API uses conventional HTTP codes and returns a structured error object.
{
"type": "invalid_request",
"code": "insufficient_kyc",
"message": "The merchant account must be verified to collect in production."
}| Code | Meaning |
|---|---|
| 400 / 422 | Invalid request or business rule not met |
| 401 | Missing or invalid API key |
| 402 | Payment declined (operator unavailable, cap…) |
| 403 | Action not allowed (frozen flows, KYC) |
| 409 | Conflict — idempotency key replayed with a different body |
| 429 | Too many requests (rate limit) |
E-commerce plugins & modules
Get paid without writing code: official modules maintained by BaobaPay. Each module installs the hosted checkout (mobile money, card, link) and the automatic reconciliation of orders via webhooks.
Common setup: paste the public API key + the webhook URL generated by the module. The order status changes automatically to « paid » on payin.succeeded. Sandbox supported in every module.
Endpoints
The collection resources of the aggregator API.
| Method | Endpoint | Usage |
|---|---|---|
| POST | /payins/mobile-money | Collect by mobile money (Orange, MTN, Express Union…) |
| POST | /payins/card | Collect by card (with 3-DS/SCA) |
| POST | /payins/recurring | Recurring payment (on file / mandate) |
| GET | /payins/{payInId} | Retrieve a collection — status, method, credited wallet |
| POST | /payins/recurring/registrations | Create a recurring payment mandate |
| POST | /preauthorizations | Card pre-auth / deposit (deferred capture)soon |
| POST | /payment-links | Create a hosted payment link / QR |
| POST | /refunds | Refund a collection |
| GET | /transactions | Unified collection log (PAYIN / REFUND types) |
| GET | /reports | Reconciliation reports and exports |
| POST | /developers/webhooks | Manage webhook endpoints |
| GET | /developers/api-keys | List API keys — metadata only, never the value |
| Holders & verification | ||
| POST | /users | Create a holder (seller, beneficiary, payer) |
| GET | /users | List holders |
| GET | /users/{userId} | Retrieve a holder and their KYC level |
| POST | /users/{userId}/kyc/documents | Submit a KYC/KYB document |
| Wallets | ||
| GET | /wallets | List wallets — main, beneficiaries, individual |
| POST | /wallets | Create a wallet (automation; creation normally happens at KYC validation) |
| GET | /wallets/{walletId} | Read a wallet — total, committed and available balance |
| GET | /wallets/{walletId}/transactions | A wallet's movements — cursor pagination |
| Fund exit | ||
| GET | /settlement-accounts | List the merchant's settlement accounts |
| POST | /settlement-accounts | Declare a settlement account — step-up authentication required |
| PATCH | /settlement-accounts/{settlementAccountId} | Update an account — step-up authentication required |
| POST | /settlement-accounts/{settlementAccountId}/verify | Re-run the holder verification |
| GET | /withdrawals | List withdrawal requests |
| POST | /withdrawals | Request a withdrawal to a verified settlement account |
| DELETE | /withdrawals/{withdrawalId} | Cancel a request still awaiting approval |
| Cash, disputes & context | ||
| GET | /cash-declarations | List declared cash collections |
| POST | /cash-declarations | Declare a cash collection — no wallet credited |
| POST | /cash-declarations/{id}/refund | Declare a cash refund |
| GET | /disputes | List disputes |
| GET | /disputes/{disputeId} | Retrieve a dispute and its evidence |
| GET | /operator-enrollments | List operator enrollments |
| GET | /operator-enrollments/{enrollmentId} | Read an enrollment |
| GET | /me/platforms | Platforms the user can access |
| POST | /session/active-platform | Switch the session context — step-up authentication on first opening |
| GET | /platform | Platform configuration — vertical, features, zone |
Webhooks
BaobaPay notifies your server of asynchronous events (payment confirmation, KYC change, dispute…). Each delivery is signed and replayed on failure (backoff up to 24 h).
Verify the signature
Every request carries a BaobaPay-Signature header — an HMAC-SHA256 of the raw body, computed with your endpoint secret. Compare it before processing the event.
const sig = req.headers['baobapay-signature'];
const expected = crypto
.createHmac('sha256', endpointSecret)
.update(rawBody)
.digest('hex');
if (sig !== expected) return res.status(400).end();
// valid signature → process the eventEvent catalogue
| Event | Triggered when… |
|---|---|
| EVENT payin.succeeded | a collection is confirmed |
| EVENT payin.failed | a collection fails (decline, timeout, balance) |
| EVENT refund.done | a refund is executed |
| EVENT kyc.updated | a customer's verification status changes |
| EVENT kyb.updated | a company's KYB status changes |
| EVENT dispute.opened | a dispute / chargeback is opened |
| EVENT dispute.closed | a dispute is closed (outcome known) |
| EVENT user.blocked | compliance suspends a holder — they can no longer collect or be paid |
| EVENT user.unblocked | a holder's suspension is lifted |
| EVENT user.closed | a holder is closed (data kept under legal retention) |
| Wallets | |
| EVENT wallet.created | a wallet is created — automatically at a holder's KYC validation |
| EVENT wallet.balance_updated | a wallet's balance changes — collection, payout, withdrawal |
| EVENT wallet.frozen | a wallet is frozen by compliance |
| Withdrawals | |
| EVENT withdrawal.approved | a withdrawal request is approved — the funds have not left yet |
| EVENT withdrawal.paid | the withdrawal is confirmed by the operator — the balance is decremented at that moment |
| EVENT withdrawal.rejected | the request is refused before execution |
| EVENT withdrawal.failed | the operator could not execute the payout — invalid number, closed account |
| Settlement accounts | |
| EVENT settlement_account.verified | a settlement account is verified — it becomes usable after the safety delay |
| EVENT settlement_account.rejected | a settlement account is refused — holder not matching the KYB |
| Cash, operators & terms | |
| EVENT cash.declared | a cash collection is declared — no wallet is credited |
| EVENT cash.refund_declared | a cash refund is declared |
| EVENT operator_enrollment.updated | an operator enrollment's status changes |
| EVENT terms.published | new terms and conditions are published |
withdrawal.paid can precede withdrawal.approved. Treat each event as an independent fact, and rely on the body's timestamp rather than the order of receipt. Deliveries are retried: your endpoint must be idempotent on event.id.{
"type": "payin.succeeded",
"created": 1752931320,
"data": {
"id": "pi_10482",
"amount": { "currency": "XAF", "value": 24500 },
"operator": "MTN"
}
}Sandbox & testing
The sandbox reproduces the entire API without real money. Trigger successes and failures deterministically.
Test cards
| Number | Result |
|---|---|
| 4970 1000 0000 0055 | Successful payment (Visa) |
| 4000 0000 0000 0002 | Card declined |
| 4000 0000 0000 9995 | Insufficient balance |
Test mobile money
+237 6 71 22 41 88→ successful confirmation+237 6 00 00 00 00→ timed out (not confirmed)