Refunds
Refund any completed payment, in full or in part. Zirzir asks the provider to send the money back where the provider supports it, and otherwise walks you through a manual refund. Either way, the ledger keeps your balance correct.
How a refund works
Section titled “How a refund works”- Checks. The payment must be
completed, and refunds can never add up to more than it. Concurrent requests are checked together, so two refunds cannot both take the last 30.00. - Hold. The amount is reserved on your
merchant_availablebalance (a hold). If the balance does not cover it, for example because the money was already paid out, the refund fails before any provider is contacted. - Provider or manual.
- Provider refunds (Chapa today): zirzir calls the provider’s refund API.
- Manual refunds (every other provider, or when you pass
"manual": true): the refund waits inrequires_actionwhile you refund the customer in the provider’s dashboard, then you confirm it.
- Settle. When the refund succeeds, the hold is captured: your available balance goes down and
provider_clearing(what the provider owes you) goes down by the same amount. When it fails or is cancelled, the hold is released.
Statuses
Section titled “Statuses”| Status | Meaning |
|---|---|
pending | Being set up (the hold is not placed or the request not sent yet). Retried automatically. |
processing | The provider accepted the refund; zirzir checks it every minute until it completes. After 48 hours without a final answer it moves to requires_action. |
requires_action | You need to act: refund (or verify) in the provider’s dashboard, then confirm or cancel. |
succeeded | Done. The money left your balance. |
failed | The provider refused, or your balance could not cover it. Nothing moved. |
cancelled | You cancelled it. Nothing moved. |
Zirzir never guesses whether money moved. If a request reached the provider but the answer was lost (a timeout, or a reply that does not clearly refuse the refund), the refund goes to requires_action with an explanation instead of being retried. A retry could refund twice, and providers such as Chapa refuse a repeated reference, which would look like a failure even though the first attempt worked. The same happens when a refund cannot be tracked, the provider’s credentials are missing, or the provider could not be reached after 5 attempts (nothing was sent in that case).
Every status change is atomic: a refund being sent to the provider cannot be cancelled at the same moment, and the held funds always end up captured (succeeded) or released (failed, cancelled), even if zirzir restarts in between.
All endpoints use your API key and only see payments in that key’s environment.
Create a refund
Section titled “Create a refund”curl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"amount": "250.00", "reason": "Returned half the order", "idempotency_key": "refund-order-42"}' \ http://localhost:8080/api/v1/payments/$PAYMENT/refunds$PAYMENTis the zirzir payment id or your own reference. A reference shared by several payments is rejected; use the id.- Amount:
amount(an exact decimal string such as"250.00") oramount_minor(25000). Omit both to refund everything not refunded yet. More decimals than the currency has ("1.005"ETB,"10.5"UGX) is an error, never rounded. idempotency_keyis required. Retrying with the same key returns the same refund with"replayed": true; the same key with a different amount is a409."manual": trueskips the provider API even if it supports refunds.
Finish a manual refund
Section titled “Finish a manual refund”# You refunded the customer in the provider's dashboardcurl -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \ -d '{"provider_refund_id": "TB-REF-9"}' \ http://localhost:8080/api/v1/refunds/$REFUND/confirm
# The refund did not happen and will not: release the fundscurl -X POST -H "X-API-Key: $KEY" http://localhost:8080/api/v1/refunds/$REFUND/cancel- Confirm works for
requires_actionandprocessingrefunds (you saw it complete before zirzir did). - Cancel works for
pendingandrequires_actionrefunds. If zirzir already sent the refund to the provider, cancelling requires{"verified_not_refunded": true}: confirm in the provider’s dashboard that the customer was not refunded first, or you may refund them twice. - A refund in
processingcannot be cancelled; the provider has it.
Look up refunds
Section titled “Look up refunds”# A payment's refunds, plus how much is refunded and still refundablecurl -H "X-API-Key: $KEY" http://localhost:8080/api/v1/payments/$PAYMENT/refunds
# All refunds, optionally filteredcurl -H "X-API-Key: $KEY" "http://localhost:8080/api/v1/refunds?status=requires_action"
curl -H "X-API-Key: $KEY" http://localhost:8080/api/v1/refunds/$REFUNDWebhooks
Section titled “Webhooks”When ZIRZIR_WEBHOOK_SECRET is set, zirzir sends signed events to your merchant webhook_url:
| Event | When |
|---|---|
refund.requires_action | You need to refund or verify in the provider’s dashboard |
refund.succeeded | The refund completed |
refund.failed | The provider refused it, or the balance could not cover it |
refund.cancelled | You cancelled it |
The data field is the refund object returned by the API. See Webhooks for verifying signatures.
Provider support
Section titled “Provider support”| Provider | Refund API | Notes |
|---|---|---|
| Chapa | Yes | Full and partial. Chapa deducts the amount from your Chapa balance; Chapa fees are not refunded. |
| Telebirr, CBE Birr, SantimPay, Arifpay, LakiPay | Manual | No public refund API. Refund in the provider’s portal, then confirm. |
| Plugins | Optional | A plugin that declares the refund capability (and verify_refund to track pending refunds) is used automatically. |
Split payments: the refund is taken from your own balance. Recover a vendor’s share separately with a manual transfer from their subaccount if needed.