API reference
Generated from the OpenAPI document, so it cannot drift from the deployed routes. Samples are generated from the same document — a new request field appears in all three languages at once.
Machine-readable spec
OpenAPI v1 — import into Postman or Insomnia, or point a code generator at it.
/docs/openapi.jsonPostman
Generated from the same document, so the requests match the routes above. Import the collection and one environment, set apiKey to your sk_test_ key, and run the folders in order — folder 1 creates the collection that folder 3 refunds. Each request captures what the next one needs, so nothing has to be copied by hand.
One environment per base URL above. They ship with apiKey empty and marked secret — a key in a file that gets shared is a leaked key.
Base URLs
| https://api-dev.moni-wave.com | Sandbox (preview) — deterministic simulator, sk_test_ keys |
| https://api-sandbox.moni-wave.com | Sandbox at general availability — not yet serving |
| http://localhost:6003 | Local development |
Every route needs Authorization: Bearer sk_… unless marked Session token. Those three are the hosted-checkout routes: called by the payment page and authorised by the token in the path, never by a key — which is why a secret key must never reach a browser.
Hosted checkout
/v1/checkout-sessionsCreate a checkout session
Creates a payment page and returns its URL. Use this when you do NOT have the customer's number — a payment link in a chat, a "Pay now" button, an invoice email. If you already know the number, call `POST /v1/collections` and skip checkout.
The amount is fixed here, by you. The returned token authorises paying this one bill and nothing else, so a leaked link risks someone paying a bill that was already owed — not minting a new charge.
You learn the outcome the same way as any other payment: the webhook for the resulting collection, or by polling it. The page is not your notification channel.
Requires an `Idempotency-Key` header.
Request body
| amount | integer | string (int64) | required |
| currency | string | required |
| description | string | required |
| externalId | string | optional |
| returnUrl | string | optional |
curl -X POST https://api-dev.moni-wave.com/v1/checkout-sessions \
-H "Authorization: Bearer $MONIWAVE_SECRET_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount": 5000,
"currency": "XAF",
"description": "Order #1042",
"externalId": "order-1042",
"returnUrl": "https://shop.example.cm/orders/1042"
}'/v1/checkout/{token}Session tokenRead a checkout session
Called by the payment page to render itself, and polled to watch the outcome. Authorised by the token in the path — no key, which is why this route is anonymous.
Returns only what the page must display: merchant display name, amount, currency, description, return URL and status. Never the merchant id, never anything about other transactions.
An unknown, malformed or expired token all answer 404 identically. This URL is guessable by anyone on the internet, so it must not reveal whether a session ever existed.
curl -X GET https://api-dev.moni-wave.com/v1/checkout/r4dtalicG0OMSxXfZkd4d7LoVkYSphtS/v1/checkout/{token}/paySession tokenPay a checkout session
Submits the customer's number and starts the charge. Anonymous — the token in the path is the entire authorisation.
Returns **202 Accepted**, not 200: the operator has prompted the customer and they have not approved yet. Poll `GET /v1/checkout/{token}` for the outcome.
One session yields at most one successful payment. Concurrent attempts are serialised, so a double-tap cannot produce two charges — but a FAILED attempt may be retried on the same link, which is what lets a customer who mistyped their number try again.
No `Idempotency-Key`: a browser has no secret and no stable identity, so the session itself is the idempotency scope.
Request body
| msisdn | string | required |
curl -X POST https://api-dev.moni-wave.com/v1/checkout/r4dtalicG0OMSxXfZkd4d7LoVkYSphtS/pay \
-H "Content-Type: application/json" \
-d '{
"msisdn": "237690000001"
}'Collections
/v1/collectionsCreate a collection
Charges a customer's mobile wallet. Returns immediately with status `accepted` — the customer still has to approve on their phone, so this is not a completed payment. Poll the collection or wait for the webhook.
Requires an `Idempotency-Key` header (a UUID you generate). Retrying with the same key replays the original response instead of charging twice.
Request body
| amount | integer | string (int64) | required |
| currency | string | required |
| description | string | required |
| externalId | string | optional |
| msisdn | string | required |
curl -X POST https://api-dev.moni-wave.com/v1/collections \
-H "Authorization: Bearer $MONIWAVE_SECRET_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount": 5000,
"currency": "XAF",
"description": "Order #1042",
"externalId": "order-1042",
"msisdn": "237690000001"
}'/v1/collections/{id}Retrieve a collection
The authoritative status. Webhooks are a convenience; this is the source of truth, and a client that only listens for callbacks will eventually miss one.
Another merchant's collection returns 404, identical to an id that does not exist — a 403 would confirm it exists.
curl -X GET https://api-dev.moni-wave.com/v1/collections/col_019fff54dbf37614b3420ade5ab24ae1 \
-H "Authorization: Bearer $MONIWAVE_SECRET_KEY"Payouts
/v1/payoutsCreate a payout
Sends money to a mobile wallet. Two different balances can refuse it: your own spendable balance, checked here (`payout_exceeds_balance` — the amount plus its fee must fit); and our operator float, checked at submission, which FAILS the payout with `insufficient_float` rather than rejecting the call.
A payout you requested seconds ago already holds its amount, even though your posted balance has not moved — the ledger only posts on completion.
Requires an `Idempotency-Key` header.
Request body
| amount | integer | string (int64) | required |
| currency | string | required |
| description | string | required |
| externalId | string | optional |
| msisdn | string | required |
curl -X POST https://api-dev.moni-wave.com/v1/payouts \
-H "Authorization: Bearer $MONIWAVE_SECRET_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount": 5000,
"currency": "XAF",
"description": "Order #1042",
"externalId": "order-1042",
"msisdn": "237690000001"
}'/v1/payouts/{id}Retrieve a payout
`completed` means the recipient has the money. Another merchant's payout returns 404, identical to an id that does not exist.
curl -X GET https://api-dev.moni-wave.com/v1/payouts/col_019fff54dbf37614b3420ade5ab24ae1 \
-H "Authorization: Bearer $MONIWAVE_SECRET_KEY"Refunds
/v1/refundsRefund a collection
A refund is its own transaction with its own `ref_` id — never a state change on the original collection, which stays `completed` forever.
Only `completed` collections are refundable. Omit `amount` to refund everything still refundable; partial refunds are allowed and accumulate, and a refund still in flight holds its amount, so `refund_exceeds_refundable` can appear even when the arithmetic looks fine.
An unknown collection, or one belonging to another merchant, returns 404.
Request body
| amount | integer | string (int64) | optional |
| collectionId | string | required |
| description | string | optional |
curl -X POST https://api-dev.moni-wave.com/v1/refunds \
-H "Authorization: Bearer $MONIWAVE_SECRET_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount": 5000,
"collectionId": "col_019fff54dbf37614b3420ade5ab24ae1",
"description": "Order #1042"
}'/v1/refunds/{id}Retrieve a refund
Another merchant's refund returns 404, identical to a missing id.
curl -X GET https://api-dev.moni-wave.com/v1/refunds/col_019fff54dbf37614b3420ade5ab24ae1 \
-H "Authorization: Bearer $MONIWAVE_SECRET_KEY"