Refunds
A refund returns money from your balance to the customer who paid, linked to the original collection.
A refund is its own transaction with its own lifecycle — it never changes the original collection's status. A fully refunded collection stays completed; the refund appears alongside it. This keeps history honest: you can always see that a payment happened and that it was returned.
Create a refund
POST /v1/refundsRequires Authorization and Idempotency-Key.
Request
| Field | Type | Required | Notes |
|---|---|---|---|
collectionId | string | yes | The col_… id being refunded. |
amount | integer | no | Minor units. Omit for a full refund of everything still refundable. |
description | string | no | Defaults to Refund of col_…. |
# Full refund
curl -X POST https://api-dev.moni-wave.com/v1/refunds \
-H "Authorization: Bearer sk_test_…" \
-H "Idempotency-Key: b7d3f9a1-2c8e-4f5b-a0d6-3e9c1b4a7f28" \
-H "Content-Type: application/json" \
-d '{ "collectionId": "col_019fff54dbf37614b3420ade5ab24ae1" }'
# Partial refund
-d '{ "collectionId": "col_019fff…", "amount": 2000, "description": "Damaged item" }'Response — 201 Created
{
"id": "ref_019fff8b3d295cf0b1824ae9d3f6c521",
"status": "accepted",
"collectionId": "col_019fff54dbf37614b3420ade5ab24ae1",
"amount": 5000,
"currency": "XAF",
"msisdn": "237690000001",
"description": "Refund of col_019fff54dbf37614b3420ade5ab24ae1",
"failureReasonCode": null,
"createdAt": "2026-08-14T10:02:44.771Z"
}Refund ids are prefixed ref_. The currency and recipient MSISDN are inherited from the original collection — you cannot refund to a different number.
Retrieve a refund
GET /v1/refunds/{id}What can be refunded
The original collection must be completed. A collection that is still pending, failed, or expired never took money, so there is nothing to return — attempting it returns not_refundable.
Partial refunds and the remaining balance
You can refund a collection in several parts, as long as the total never exceeds the original amount.
Important: the remaining refundable amount counts refunds in any non-final-failure status — including ones still accepted or submitted. An in-flight refund holds its amount.
Worked example on a 10 000 XAF collection:
| Step | Request | Result | Remaining |
|---|---|---|---|
| 1 | Refund 6 000 | 201 — accepted | 4 000 |
| 2 | Refund 5 000 | 400 refund_exceeds_refundable | 4 000 |
| 3 | Refund 4 000 | 201 — accepted | 0 |
| 4 | Refund anything | 400 refund_exceeds_refundable | 0 |
At step 2 the first refund had not completed yet — but its 6 000 was already reserved. Without that rule, two concurrent refunds could each pass a naive check and together return more than was ever collected.
If a refund ends up failed or expired, its amount is released back into the refundable remainder automatically.
The error message states the actual remainder, so you can correct and retry:
{
"error": {
"code": "refund_exceeds_refundable",
"message": "Refundable remainder is 4000 XAF (6000 already reserved by other refunds).",
"details": []
}
}Fees
Refunds carry no fee, but the fee on the original collection is not returned — it was earned on the original transaction. Refunding a 10 000 XAF collection that cost 200 XAF in fees debits 10 000 XAF from your balance; the 200 XAF stays spent.
So on a completed refund, fee is 0 and net equals amount:
{ "amount": 10000, "currency": "XAF", "fee": 0, "net": 10000, "status": "completed" }Both are null until the refund is completed. Note the asymmetry this makes visible: the collection credited you net 9 800 and the refund debits net 10 000, so a collect-then-fully-refund cycle leaves you 200 XAF down.
Statuses
Same lifecycle as any transaction: accepted → submitted → completed / failed / expired. completed means the customer has the money back. On failed, failureReasonCode carries the stable reason (errors.md).
Errors
| Code | HTTP | Meaning |
|---|---|---|
invalid_collection_id | 400 | Not a valid transaction id. |
not_a_collection | 400 | The id is a payout or refund, not a collection. |
not_refundable | 400 | The collection is not completed. |
refund_exceeds_refundable | 400 | Amount exceeds the remainder (message states it). |
invalid_amount | 400 | Not positive, or nothing left to refund. |
| — | 404 | Collection does not exist, or belongs to another merchant. |