Skip to content

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.

  1. 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.
  2. Hold. The amount is reserved on your merchant_available balance (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.
  3. 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 in requires_action while you refund the customer in the provider’s dashboard, then you confirm it.
  4. 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.
StatusMeaning
pendingBeing set up (the hold is not placed or the request not sent yet). Retried automatically.
processingThe 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_actionYou need to act: refund (or verify) in the provider’s dashboard, then confirm or cancel.
succeededDone. The money left your balance.
failedThe provider refused, or your balance could not cover it. Nothing moved.
cancelledYou 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.

Terminal window
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
  • $PAYMENT is 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") or amount_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_key is required. Retrying with the same key returns the same refund with "replayed": true; the same key with a different amount is a 409.
  • "manual": true skips the provider API even if it supports refunds.
Terminal window
# You refunded the customer in the provider's dashboard
curl -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 funds
curl -X POST -H "X-API-Key: $KEY" http://localhost:8080/api/v1/refunds/$REFUND/cancel
  • Confirm works for requires_action and processing refunds (you saw it complete before zirzir did).
  • Cancel works for pending and requires_action refunds. 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 processing cannot be cancelled; the provider has it.
Terminal window
# A payment's refunds, plus how much is refunded and still refundable
curl -H "X-API-Key: $KEY" http://localhost:8080/api/v1/payments/$PAYMENT/refunds
# All refunds, optionally filtered
curl -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/$REFUND

When ZIRZIR_WEBHOOK_SECRET is set, zirzir sends signed events to your merchant webhook_url:

EventWhen
refund.requires_actionYou need to refund or verify in the provider’s dashboard
refund.succeededThe refund completed
refund.failedThe provider refused it, or the balance could not cover it
refund.cancelledYou cancelled it

The data field is the refund object returned by the API. See Webhooks for verifying signatures.

ProviderRefund APINotes
ChapaYesFull and partial. Chapa deducts the amount from your Chapa balance; Chapa fees are not refunded.
Telebirr, CBE Birr, SantimPay, Arifpay, LakiPayManualNo public refund API. Refund in the provider’s portal, then confirm.
PluginsOptionalA 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.