Refunds
One call: POST /v1/refunds with a payment id. You never care which provider processed the original, which rail it used, or how money physically travels back — Finscale resolves all of it from the payment.
Refund the payment, never the provider
A refund references pay_8Q2mX4nT1cVb — not prov_eu_acq_01. That distinction is the whole model:
- Finscale looks up which provider settled the payment (even if it took failover to get there) and issues the refund through that provider's rail.
- If the original rail can't carry refunds — most bank redirects can't — Finscale switches to a credit transfer automatically.
- Provider migrations don't strand refunds. A payment processed a year ago on a provider no longer in your routing set refunds exactly the same way.
Refunding our example order — €15.00 of the €49.00 back to Anna:
curl https://api.finscale.dev/v1/refunds \
-H "Authorization: Bearer sk_test_51FinscaleDemo…" \
-H "Idempotency-Key: idem_rf_ord_9f21_0716_1" \
-H "Content-Type: application/json" \
-d '{
"payment": "pay_8Q2mX4nT1cVb",
"amount": 1500,
"reason": "requested_by_customer"
}'
Idempotency-Key is money out the door twice. Key every refund POST — the pattern idem_rf_<reference>_<n> makes keys unique per refund, not per payment. See Idempotency.
The Refund object
{
"id": "rf_3Vp7dK2wQx",
"object": "refund",
"payment": "pay_8Q2mX4nT1cVb",
"amount": 1500,
"currency": "EUR",
"status": "pending",
"reason": "requested_by_customer",
"created_at": "2026-07-16T14:02:09Z",
"livemode": false
}
| Field | Type | Notes |
|---|---|---|
id | string | Prefix rf_. |
payment | string | The payment being refunded. The only routing input you provide. |
amount | integer | Minor units. Omit to refund the full remaining balance. |
currency | string | Always the payment's currency — refunds can't convert. |
status | string | pending → succeeded | failed. See lifecycle. |
reason | string | requested_by_customer, duplicate, fraudulent, or other. Optional; feeds reporting. |
Refund lifecycle
Refunds are asynchronous. Every refund starts pending while Finscale submits it on the appropriate rail, then resolves:
| Status | Meaning | Webhook |
|---|---|---|
pending | Accepted and submitted to the rail. The payment's amount_refunded is already reserved. | — |
succeeded | Confirmed by the rail. Appears as a negative line on your next settlement report. | refund.succeeded |
failed | The rail rejected it — closed account, expired card. The amount is restored to the payment's refundable balance. | refund.failed |
How long pending lasts is a rail property, not a Finscale one: seconds for PIX, a business day or two for credit transfers, and for cards the refund is usually accepted in seconds but takes 5–10 business days to reach the customer's statement. Set expectations in your customer comms accordingly.
Partial & multiple refunds
Refund any amount up to the un-refunded remainder, as many times as needed. For the €49.00 example: refund €15.00, later refund the remaining €34.00 —
{
"id": "pay_8Q2mX4nT1cVb",
"object": "payment",
"amount": 4900,
"currency": "EUR",
"status": "succeeded",
"amount_refunded": 4900,
"partially_refunded": false,
"refunded": true,
"reference": "ord_9f21_0716"
}
- The refund total can never exceed the payment's
amount— an over-refund fails with aninvalid_request_errorbefore any money moves. - While
0 < amount_refunded < amount, the payment readspartially_refunded: true. At full refund it flips torefunded: true. Status stayssucceededthroughout — see Refund state. - List a payment's refunds with
GET /v1/refunds?payment=pay_8Q2mX4nT1cVb. - A
pendingrefund already counts against the refundable balance, so concurrent partials can't oversubscribe the payment.
Local-method caveats
Cards and most wallets refund natively — the refund travels back the same rail as a linked transaction. Push-based local methods don't have a "refund message" on the rail, so Finscale substitutes one. Same API call, different mechanics underneath:
| Methods | Refund path | What to expect |
|---|---|---|
card, apple_pay, google_pay |
Native card refund | Linked to the original on the customer's statement. 5–10 business days to appear. |
pix, blik, swish, mb_way, twint, satispay, mobilepay, vipps, trustly |
Native rail refund | Fast — instant to next-day. Shows as a refund in the customer's app. |
ideal, bancontact, eps, p24, multibanco, sepa_credit |
SEPA credit transfer to the originating account | Arrives in 1–2 business days as a separate incoming credit, not a reversal of the original debit. Finscale captured the customer's IBAN at payment time — nothing extra to collect. |
mpesa |
Reversal, else B2C transfer | In-window refunds reverse the original; later ones arrive as a new incoming transfer. |
sepa_debit, ach |
Credit back to the debited account | 1–2 business days (SEPA) / 2–4 (ACH). Independent of the customer's own return rights on the debit. |
usdc |
New transfer to the sending address | On-chain within minutes. Goes to the address that paid — warn customers paying from exchange-hosted wallets. |
Refunds and their fees are itemized as negative lines on your settlement report, referencing both the refund and the original payment id — see Settlement.