Collections
A collection charges money from a customer's mobile wallet to your merchant balance — a pay-in. The customer approves on their phone (USSD prompt or app push), so collections complete asynchronously.
Create a collection
POST /v1/collectionsRequires Authorization and Idempotency-Key.
Request
| Field | Type | Required | Notes |
|---|---|---|---|
amount | integer | yes | Minor units. XAF has no minor unit, so 5000 = 5 000 francs. Must be positive. |
currency | string | yes | ISO 4217. XAF at launch. |
msisdn | string | yes | Customer's number, international format, digits only, no +. 8–15 digits. |
description | string | yes | Shown in your dashboard and reports. |
externalId | string | no | Your own reference (order id, invoice number). Stored and returned; not required to be unique. |
curl -X POST https://api-dev.moni-wave.com/v1/collections \
-H "Authorization: Bearer sk_test_…" \
-H "Idempotency-Key: 3f9c1e2a-7b4d-4a1f-9c8e-2d6b5a0f3e11" \
-H "Content-Type: application/json" \
-d '{
"amount": 5000,
"currency": "XAF",
"msisdn": "237690000001",
"description": "Order #1042",
"externalId": "order-1042"
}'Response — 201 Created
{
"id": "col_019fff54dbf37614b3420ade5ab24ae1",
"status": "accepted",
"amount": 5000,
"currency": "XAF",
"msisdn": "237690000001",
"description": "Order #1042",
"externalId": "order-1042",
"failureReasonCode": null,
"createdAt": "2026-08-14T08:12:59.243Z"
}201 means accepted, not paid. The customer has not approved yet. Wait for completed before treating the payment as received — poll, or use webhooks.
Collection ids are prefixed col_.
Fee and net
fee is what MoniWave charged; net is what actually reached your balance. Both are null until the collection is completed, because money moves only on the terminal event — a fee on a pending charge would be a forecast, not a charge.
{ "amount": 10000, "currency": "XAF", "fee": 200, "net": 9800, "status": "completed" }They are read from the ledger entry that was actually posted, not recalculated from the current rate, so a past collection always reports what it really cost — even after pricing changes. Reconcile against net, never against amount minus an assumed percentage.
Retrieve a collection
GET /v1/collections/{id}Returns the same shape with the current status. This is the authoritative source of truth for whether the payment succeeded (transaction-lifecycle.md).
curl https://api-dev.moni-wave.com/v1/collections/col_019fff… \
-H "Authorization: Bearer sk_test_…"Returns 404 if the id does not exist or belongs to another merchant — the two are deliberately indistinguishable.
Statuses
| Status | What to do |
|---|---|
accepted | Wait. Not yet with the operator. |
submitted | Wait. Customer is being prompted. |
completed | Money received. Safe to fulfil the order. |
failed | Not paid, and will not be. Check failureReasonCode. |
expired | Customer never responded. Not paid. |
On failed, failureReasonCode carries a stable machine-readable reason — the same value the webhook payload carries, so polling never knows less than the callback. It is null in every other status. Codes: errors.md. New codes may be added over time, so treat unknown values as a generic failure rather than an error.
Errors
| Code | HTTP | |
|---|---|---|
invalid_amount | 400 | Not a positive integer in minor units. |
invalid_currency | 400 | Unsupported currency. |
invalid_msisdn | 400 | Wrong format — digits only, no +, 8–15 characters. |
description_required | 400 | Missing or blank. |
Plus the shared authentication and idempotency codes. Validation returns all problems at once in details.
Fees
Fees are deducted from the collected amount and credited to your balance net. You are charged only for successful collections — failed and expired transactions cost nothing.
Testing
Use the magic MSISDNs to force any outcome deterministically in the sandbox: immediate success, delayed success, decline, insufficient funds, no response, or rejection at submission.