Refunds and reversals
Two different ways to give a customer their money back. They look the same from outside; the rules and constraints differ significantly.
Reversals and refunds are the two ways to return money to a customer after a successful transaction. They look the same from outside — the customer gets their money back — but the rules, eligibility, and constraints are different.
This page covers when to use which, the API for each, and the typical operational patterns.
For the rules in detail, see Refunds and reversals — business rules.
Reversal vs refund — pick the right one
| Reversal | Refund | |
|---|---|---|
| When | Same-day, before the transaction settles | Any time after settlement (within the issuer's window) |
| Amount | Full only — you cannot reverse part of a transaction | Full or partial — refund any amount up to the original |
| Card types | All — debit, credit, cheque | Credit and cheque only — debit cards cannot be refunded (PASA rules) |
| Customer experience | Looks like the transaction never happened | Two entries: original charge, then a refund credit |
| Settlement impact | Net-zero; nothing settles | Original settles, refund settles separately |
| Use it when | Order cancelled before fulfilment, customer disputed at checkout, your handler returned non-200 | Order shipped then returned, partial returns, post-shipment customer queries |
When in doubt: reverse if you can, refund if you must. Reversals are cleaner — no settlement impact, no bookkeeping entries to reconcile, and they work on all card types.
Reverse a transaction
curl -X DELETE "https://qa.scantopay.io/pluto/purchase/{transactionId}" \
-u "$USERNAME:$PASSWORD"Path parameter:
transactionId— the Scan to Pay transaction ID from the original webhook payload
Reversals are full-amount only. No body required.
Outcome
If the transaction is reversible, the platform schedules a reversal and you'll receive a webhook with status: REVERSED (TxState: END_REVERSED). If the reversal fails, the webhook shows status: REVERSED with the underlying reason in bankResponse, and the original transaction state becomes END_REVERSAL_FAILED.
Reversal eligibility is enforced by the issuing bank, not Scan to Pay. The platform will attempt any reversal you request; whether it succeeds depends on whether the bank has cleared the transaction yet.
Reversal-on-webhook-failure
Reversals also happen automatically if your webhook handler fails to acknowledge a successful payment within 45 seconds. See Webhooks — How it works. You don't need to manually reverse in that case — the platform's safety net does it for you.
Refund a transaction
Two endpoints depending on whether you want the response inline or via webhook only.
Refund — async (webhook only)
curl -X POST "https://qa.scantopay.io/pluto/purchase/refund/{transactionId}" \
-u "$USERNAME:$PASSWORD" \
-H 'Content-Type: application/json' \
-d '{
"amount": 50.00
}'The platform queues the refund and responds quickly. The actual outcome arrives on your webhook with status: REFUNDED and referenceTransactionId pointing back to the original transaction.
Refund — sync (response includes outcome)
curl -X POST "https://qa.scantopay.io/pluto/purchase/doRefund/{transactionId}" \
-u "$USERNAME:$PASSWORD" \
-H 'Content-Type: application/json' \
-d '{
"amount": 50.00
}'This endpoint blocks until the refund is processed and returns the outcome in the response body. Use it when you need to update your UI immediately, e.g. on a back-office refund screen.
Path and body
| Param | Where | Detail |
|---|---|---|
transactionId | Path | The original transaction's Scan to Pay ID |
amount | Body | The amount to refund. Up to the original transaction amount. Partial refunds allowed for credit/cheque cards. |
Partial refunds
You can refund less than the full amount, and you can refund multiple times against the same original transaction until you've returned the full amount.
# First partial — R50 off an R150 original
POST /pluto/purchase/refund/{transactionId} with {"amount": 50.00}
# Later — another R25
POST /pluto/purchase/refund/{transactionId} with {"amount": 25.00}
# Now you've refunded R75 of R150. R75 remaining can still be refunded.What you'll see in webhooks
Both reversals and refunds trigger webhooks. The status field tells you which:
Webhook status | Meaning |
|---|---|
REVERSED | Reversal succeeded |
REFUNDED | Refund succeeded (full or partial) |
For refunds, referenceTransactionId on the payload points back to the original transaction so you can reconcile. The amount field reflects the amount refunded on this call, not the cumulative refunded amount.
For the full payload structure, see Webhooks.
Errors
| Status / code | Meaning |
|---|---|
444 NOTHING_TO_REVERSE | The transaction can't be reversed (already reversed, already refunded, or outside the bank's window) |
514 UNABLE_TO_REVERSE | The reversal was attempted but failed at the bank |
515 UNABLE_TO_REFUND | The refund was attempted but failed (different card type, already fully refunded, outside acquirer window) |
445 INVALID_CLIENT_CARD | Attempting to refund a debit card — not allowed per PASA |
432 INVALID_AMOUNT | Refund amount exceeds the remaining refundable balance, or is zero/negative |
436 INVALID_TRANSACTION_REF | The transactionId doesn't match any known transaction |
Full error reference: Errors.
What's next
- Detailed business rules and constraints → Refunds and reversals — business rules
- Receive the refund/reversal notification → Webhooks
- Bank decline codes if a refund fails at the issuer → ISO response codes
- Query transaction state to confirm → Querying transactions
- Handle cardholder disputes that need a refund → Customer disputes
Updated 4 days ago
