TeronaPay

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"
}
FieldTypeNotes
iduuidRefund id
payment_iduuidThe payment being refunded
amountnumberRefund slice amount, decimal major units
currencystringMust match payment currency
statusenumpending / succeeded / failed
failure_reasonstringSet on failed only

POST /v1/payments/{id}/refunds

Issue a refund.

Request body — all fields optional:

FieldTypeNotes
amountnumberSlice in decimal major units. Omit (or pass 0) to refund the remaining unrefunded gross
currencystringMust 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): 201 with a Refund in status succeeded or failed.
  • M-Pesa (async): 201 with a Refund in status pending. The final state arrives via payment.refunded or payment.refund_failed webhook.

Errors

  • 404 not_found — payment doesn't exist
  • 422 precondition_failed — payment isn't in succeeded state, or already fully refunded
  • 400 invalid_requestamount > remaining, mismatched currency, or amount <= 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.

API: Refunds · TeronaPay docs