Refunds API
Refunds are nested under payments. There's no top-level refund list endpoint — refunds live on their parent payment.
The Refund object
{
"id": "8a1c9b25-8f97-4d01-af86-dea590b1e06d",
"payment_id": "75b83cff-488e-4b74-ab26-3e288a3a7f9b",
"amount": 30.00,
"currency": "KES",
"status": "succeeded",
"failure_reason": "",
"created_at": "2026-05-19T10:54:37Z",
"updated_at": "2026-05-19T10:54:37Z"
}| Field | Type | Notes |
|---|---|---|
id | uuid | Refund id |
payment_id | uuid | The payment being refunded |
amount | number | Refund slice amount, decimal major units |
currency | string | Must match payment currency |
status | enum | pending / succeeded / failed |
failure_reason | string | Set on failed only |
POST /v1/payments/{id}/refunds
Issue a refund.
Request body — all fields optional:
| Field | Type | Notes |
|---|---|---|
amount | number | Slice in decimal major units. Omit (or pass 0) to refund the remaining unrefunded gross |
currency | string | Must match payment currency when provided |
Examples
Full remaining refund (no body):
curl -X POST https://api.teronapay.com/v1/payments/$ID/refunds \
-u "$KEY:$SECRET"Partial — KES 30:
curl -X POST https://api.teronapay.com/v1/payments/$ID/refunds \
-u "$KEY:$SECRET" \
-H "Content-Type: application/json" \
-d '{ "amount": 30.00, "currency": "KES" }'Response
- Card (sync):
201with aRefundin statussucceededorfailed. - M-Pesa (async):
201with aRefundin statuspending. The final state arrives viapayment.refundedorpayment.refund_failedwebhook.
Errors
404 not_found— payment doesn't exist422 precondition_failed— payment isn't insucceededstate, or already fully refunded400 invalid_request—amount > remaining, mismatched currency, oramount <= 0
What's NOT supported
- Standalone refund list endpoint. Refunds live on the payment. Use
GET /v1/payments/{id}to retrieve them. - Refunding a refund. Once succeeded, a refund cannot be reversed via this API.
- Cross-channel refunds. A card payment can only be refunded via card; an M-Pesa payment via M-Pesa Reversal.
- Fee refunds. The original platform fee is retained. Contact support for one-off fee reimbursement.
Channel-specific notes
Card refunds are synchronous — the response status determines success. You don't need to wait for a webhook.
M-Pesa Reversal is async via Daraja. Expect 5-30 seconds for the callback. If it never arrives, our refund sweeper marks the refund failed after REFUND_SWEEPER_STUCK_AFTER (default 1h). Documented trade-off: a refund that Daraja secretly succeeded will be reported failed.
See Refunds concept for ledger semantics + idempotency rules.
