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:

POST /v1/refunds
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 matters most here A retried refund without an 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

201 Created
{
  "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
}
FieldTypeNotes
idstringPrefix rf_.
paymentstringThe payment being refunded. The only routing input you provide.
amountintegerMinor units. Omit to refund the full remaining balance.
currencystringAlways the payment's currency — refunds can't convert.
statusstringpendingsucceeded | failed. See lifecycle.
reasonstringrequested_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:

StatusMeaningWebhook
pendingAccepted and submitted to the rail. The payment's amount_refunded is already reserved.
succeededConfirmed by the rail. Appears as a negative line on your next settlement report.refund.succeeded
failedThe 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 —

GET /v1/payments/pay_8Q2mX4nT1cVb — after both refunds
{
  "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 an invalid_request_error before any money moves.
  • While 0 < amount_refunded < amount, the payment reads partially_refunded: true. At full refund it flips to refunded: true. Status stays succeeded throughout — see Refund state.
  • List a payment's refunds with GET /v1/refunds?payment=pay_8Q2mX4nT1cVb.
  • A pending refund 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:

MethodsRefund pathWhat 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.
Credit-transfer refunds look different to customers A customer refunded on iDEAL or Bancontact sees a new deposit from the payment reference — not a cancellation of the original charge. Say so on your refund confirmation ("you'll receive a bank transfer within 2 business days") and support tickets drop noticeably.

Refunds and their fees are itemized as negative lines on your settlement report, referencing both the refund and the original payment id — see Settlement.