Full and Partial Refunds
Overview
Refunding transactions is a crucial aspect of managing a successful merchant operation. CrissCross supports both full and partial refunds, allowing merchants to process refunds directly through the same payment rails used for the original transactions.
How CrissCross Executes Refunds
You always call the same POST /payment/{transactionId}/refund endpoint, but under the hood CrissCross executes a refund in one of two ways depending on what the originating payment provider supports:
- Provider refund. When the originating provider exposes a refund API, CrissCross calls it directly. This is the preferred path because it reverses the original movement on the rail it came in on.
- Payout refund. When the originating provider does not support refunds, CrissCross automatically converts the refund into a payout back to the original destination (the payer’s mobile wallet, bank account, etc.). This is particularly common for mobile money, where many providers expose payouts but not refund APIs, and is also used for bank payment methods when we hold the full destination account details.
Three practical consequences:
- No time limit on payout refunds. Provider refund windows (typically 90–180 days, sometimes shorter) only apply when CrissCross takes the provider-refund path. Payout refunds can be issued whenever the destination is still valid.
- Unified accounting. Both types are tracked as refunds against the original transaction (they appear in
refundTransactionIdsand contribute tototalRefundedValueon the session). You cannot over-refund a customer regardless of which mechanism executes, because the same balance is enforced for both. - Same webhook events on both paths. A refund only ever emits
refund.*events — plustransaction.refundedon the original payment once it is refunded in full. The one exception is an automatic late-payment refund: the original signals throughtransaction.auto_refundedinstead and never firestransaction.refunded. A payout refund never emitspayout.*events; those are reserved for payouts you create through the Payouts API. See Webhook Events for the event-by-event behaviour.
The mechanism CrissCross chose for any given refund is visible on the returned refund transaction; consumers do not need to differentiate between the two paths to issue or track refunds correctly.
Refund Requirements
- Available Balances: To process refunds, merchants must maintain sufficient balances with their payment processors in the relevant markets. Payout refunds additionally require sufficient payout balance for the destination currency.
- Transaction Eligibility: Refunds can only be issued against transactions that have been successfully captured and settled. Provider-refund windows apply only on the provider-refund path; payout refunds are not subject to a CrissCross-imposed time limit.
Creating a Refund
Refunds are created against the original payment, identified by its transactionId in the path: POST /payment/{transactionId}/refund. The request body takes:
refundValue(optional) — an object withminorAmount(the refund amount in minor units, e.g. cents) andcurrency(ISO 4217). Omit it entirely for a full refund; supply it for a partial refund. When provided, the currency must match the original payment. This mirrors thedestinationValueshape used on payouts.reason(optional) — free-form note recorded for reporting and dispute defense.
A refund is itself a transaction in CrissCross, so the response is a standard transaction record — the same shape returned by the Retrieve Payment Status endpoint. Note in particular:
transactionId— the refund’s own transaction identifier. Use this to poll the refund’s status or correlate webhook events.status— the refund starts in a non-terminal (pending) state and transitions asynchronously (see below).
Refunds Are Asynchronous
Refund creation returns the refund transaction in a non-terminal (pending) state. The refund is then processed against the underlying rail and transitions to exactly one terminal state — COMPLETED, FAILED, ERRORED, or CANCELLED — each mapping one-to-one to a refund.* webhook event. There are two ways to observe the transition:
- Poll
GET /payment/{transactionId}using the refund’s returnedtransactionId— a refund is a transaction, so it is retrieved through the standard payment-status endpoint. - Subscribe to the
refund.completed,refund.failed,refund.errored, orrefund.cancelledwebhook events.
When the original payment becomes fully refunded, a transaction.refunded event is also delivered against the original payment’s transactionId.
Refund Webhook Payload
Each refund webhook is delivered as the standard CrissCross envelope { eventType, timestamp, payload }, where payload is the refund transaction record — the same object you get from GET /payment/{transactionId}. For example, a refund.completed event:
The payload.status is COMPLETED, FAILED, ERRORED, or CANCELLED, matching the event. See the Webhooks reference for the full event schemas.
Full Refund
A full refund returns the entire remaining refundable balance. Omit refundValue altogether — an empty body (or just a reason) is enough:
Partial Refund
Partial refunds return only a portion of the payment amount, useful for partial returns or post-purchase discounts. Pass the partial amount in refundValue:
Checking Refundability
Before issuing a refund you can check whether a payment is refundable and for how much with GET /payment/{transactionId}/refundability. It returns refundable, the remaining refundableAmount (in minor units), an optional refundableUntil deadline, and a machine-readable notRefundableReason when the payment cannot be refunded — ideal for rendering a refund affordance in your UI.
When refundable is false, notRefundableReason is one of: NOT_REFUNDABLE_REASON_NOT_A_PAYMENT, NOT_REFUNDABLE_REASON_NOT_COMPLETED, NOT_REFUNDABLE_REASON_WINDOW_EXPIRED, NOT_REFUNDABLE_REASON_ALREADY_REFUNDED, NOT_REFUNDABLE_REASON_REFUND_IN_FLIGHT, NOT_REFUNDABLE_REASON_INSUFFICIENT_BALANCE, or NOT_REFUNDABLE_REASON_UNSUPPORTED.
Retrieving a Refund
A refund is a transaction, so retrieve it with GET /payment/{transactionId} using the transactionId returned from creation. The response is a standard transaction record, with status reflecting the latest refund state.
Where Refunds Surface on Other Objects
Refunds are linked back from the records they affect so you don’t have to query refunds separately to understand the state of a session or transaction:
- On the parent transaction:
refundTransactionIdsis a list of thetransactionIdof every refund processed against that transaction. Pass any of them directly toGET /payment/{transactionId}. - On the session:
totalRefundedValueis a{ minorAmount, currency }object reflecting the cumulative amount refunded across all refunds on the session. Use this to check remaining refundable balance before issuing further partial refunds.
Refund Notifications
When refunds are processed, CrissCross can trigger notifications for:
- Successful Refunds: Confirmations sent to both merchants and customers.
- Balance Alerts: Notifications for top-ups when balances with processors are insufficient to cover the refund.
Best Practices
- Maintain Adequate Balances: Regularly monitor and top up your balances with payment processors to ensure smooth refund transactions.
- Communicate with Customers: Keep customers informed about the status of their refunds to maintain trust and satisfaction.
- Monitor Refund Trends: Analyze refund patterns to identify potential issues with products or services early on.
Conclusion
Efficiently managing refunds is essential for maintaining customer satisfaction and operational compliance. CrissCross provides robust tools to handle full and partial refunds, ensuring merchants can manage their financial transactions effectively.