A refund gives a buyer back money from a paid payment, and your balance pays for it. One call, POST /v1/refunds, makes it. Return the whole payment at once, or smaller amounts over time until the total reaches what was captured. Each payment method allows this much:
| Method | Full refund | Partial refund | Time limit | Where the money goes |
|---|---|---|---|---|
| Card | Yes | Yes | None in the API | Back to the buyer's card. The card network takes some days to show it on the buyer's statement. |
| Pix | Yes | Yes | 90 days after the payment | Back to the account that paid, by the payment's own Pix transaction: a devolução. |
| Boleto | No | No | Not refundable | See Boleto has no refunds. |
Create a refund#
Point at the payment with payment_intent, or with its charge. Without amount, Fluveo returns whatever has not been refunded yet. To give back only part, put the part in amount, in the currency's smallest unit: 500 below is 5.00. Make one Idempotency-Key for each refund you intend: sending the call again with that key brings back the same refund instead of a second one.
curl https://api.fluveo.com/v1/refunds \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: order-1042-refund-1" \
-d payment_intent=pi_... \
-d amount=500 \
-d reason=requested_by_customerconst response = await fetch("https://api.fluveo.com/v1/refunds", {
method: "POST",
headers: {
Authorization: "Bearer sk_test_...",
"Idempotency-Key": "order-1042-refund-1",
},
body: new URLSearchParams({
payment_intent: "pi_...",
amount: "500",
reason: "requested_by_customer",
}),
});
const refund = await response.json();
console.log(refund.id, refund.status);import requests
response = requests.post(
"https://api.fluveo.com/v1/refunds",
headers={
"Authorization": "Bearer sk_test_...",
"Idempotency-Key": "order-1042-refund-1",
},
data={
"payment_intent": "pi_...",
"amount": 500,
"reason": "requested_by_customer",
},
)
refund = response.json()
print(refund["id"], refund["status"])<?php
$ch = curl_init("https://api.fluveo.com/v1/refunds");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer sk_test_...",
"Idempotency-Key: order-1042-refund-1",
],
CURLOPT_POSTFIELDS => http_build_query([
"payment_intent" => "pi_...",
"amount" => 500,
"reason" => "requested_by_customer",
]),
]);
$refund = json_decode(curl_exec($ch), true);
echo $refund["id"] . " " . $refund["status"];You get back a refund object whose id starts with re_. On the charge, amount_refunded goes up by that amount, and refunded becomes true when nothing is left to return.
The PaymentIntent stays succeeded#
A refund changes the refund and the charge, never the PaymentIntent. Even after a full refund, the PaymentIntent still answers status: "succeeded", because the payment did happen. To know how much of a payment came back, read its charge (the PaymentIntent's latest_charge): amount_refunded and refunded. Or follow the charge.refunded event, which carries that charge each time a refund is made.
The paid
pi_to refund. Fluveo refunds its latest charge. Send this orcharge.The charge to refund: a card's
ch_, a Pix'spy_. If you send both, the charge must belong to that PaymentIntent.In the smallest unit of the currency. Default: everything not yet refunded.
requested_by_customer,duplicateorfraudulent.Your own key-value pairs, sent as
metadata[key]=value, such asmetadata[order_id]=1042.
Do not send reverse_transfer or refund_application_fee. Fluveo sets both; any value other than its own is a 400.
A payment you authorized but never captured holds no money to give back yet. Cancel its PaymentIntent to let the hold go.
Your balance pays for refunds#
Fluveo takes each refund out of your store's balance and never lends you the difference. The balance is checked before the refund leaves, and it counts both your available and your pending money. A refund that returns everything left of a payment, with nothing else of yours on its way out, needs the balance to cover only that amount less the fee Fluveo kept on the payment. So a full refund of a new store's only payment goes through, and the balance then shows that fee below zero.
When the balance falls short, the payment method decides:
- A card refund is accepted anyway and waits. It answers 200 with
status: "pending"andpending_reason: "insufficient_funds", and Fluveo sends it by itself once your balance covers it. - A Pix refund is refused with 400
balance_insufficient, and nothing goes out.
See When your balance is too low and Balance and payouts.
Refund statuses#
| Status | Meaning |
|---|---|
pending | The refund was sent and the card network or the bank has not confirmed it yet. A Pix refund starts here. A card refund your balance does not cover yet also waits here, unsent, with pending_reason: "insufficient_funds". |
succeeded | The refund went through. |
failed | The refund could not be made; the money is back in your balance (its reservation is released) and failure_reason says why. |
canceled | The refund was canceled. |
requires_action | The refund waits on a step outside the API. |
A Pix refund opens in pending and waits for the bank. It ends succeeded once the bank hands the money back, or failed when it cannot. Count it as done only at succeeded. A card refund that waits for your balance is sent as soon as the balance covers it; after 45 days without enough balance it ends failed with failure_reason: "insufficient_funds". A card refund can also flip from succeeded to failed later, for instance when the buyer's card no longer exists. So keep listening for refund.failed, and close your books on a refund only after the window you care about. To track a refund, use its webhooks or read GET /v1/refunds/{id}.
Events#
| Event | When |
|---|---|
refund.created + charge.refunded | The refund was made, a card refund that waits for your balance included. charge.refunded carries the charge with its new amount_refunded, which grows even while the refund is still pending. These events do not prove success. |
refund.updated + charge.refund.updated | The refund's status changed (a pending refund that succeeds sends them with status: succeeded), or you changed its metadata. Both carry the refund. |
refund.failed + charge.refund.updated | The refund failed. The charge's amount_refunded goes back down, and its reserved money is released. |
Boleto has no refunds#
A paid Boleto cannot be refunded through Fluveo. A refund on a Boleto payment, paid or not, answers:
{
"error": {
"type": "invalid_request_error",
"message": "Refunds are not supported on this payment method."
}
}If you need to give money back for a Boleto purchase, agree on it with the buyer outside Fluveo.
Limits#
- Pix, 90 days. A Pix can be refunded only within 90 days of its payment. Later, the refund is refused with
paramcharge. - No more than was paid. The sum of refunds cannot pass the captured amount. Asking for more answers 400 on
amount, such as "Refund amount ($10.01) is greater than charge amount ($10.00)", or after a partial refund "Refund amount ($7.01) is greater than unrefunded amount on charge ($7.00)". - Already refunded. Refunding a fully refunded charge without an
amountanswers 400charge_already_refunded.
Read and change refunds#
GET /v1/refunds/{id}reads one refund.GET /v1/refundslists refunds, newest first, withlimit(1 to 100, default 10),starting_afterorending_before.GET /v1/charges/{charge}/refundslists the refunds of one charge.POST /v1/refunds/{id}changes onlymetadata.
See the API reference for every field of the refund object.
Errors you may see#
| Error | Why |
|---|---|
400 charge_already_refunded | The whole charge was already refunded. |
400 on amount | The amount is more than what is left to refund on the charge. |
| 400 "This uncaptured Charge was created by a PaymentIntent ..." | The payment is only authorized: cancel the PaymentIntent instead. |
| 400 "This PaymentIntent (pi_...) does not have a successful charge to refund." | The payment was never confirmed, or it was declined. |
400 on charge, "You may only specify one of these parameters: charge, payment_intent." | Send charge or payment_intent, not both. |
| 400 "Refunds are not supported on this payment method." | A Boleto payment. See Boleto has no refunds. |
400 on charge | A Pix paid more than 90 days ago. |
400 on reason | The reason is not one of the three above, such as reason=other. |
400 balance_insufficient | Your balance does not cover a Pix refund. A card refund waits instead. See When your balance is too low. |
404 resource_missing | The pi_ or ch_ is not one of your store's (on id for a ch_, on payment_intent for a pi_). |
POST /v1/refunds/{id}/cancel answers 400 "Canceling this refund is unsupported.": only a refund in requires_action can be canceled, and no Fluveo refund reaches it.
When your balance is too low#
When the balance does not cover a refund (see Your balance pays for refunds), what happens depends on the payment method. Either answer is expected, not a bug in your integration, and the same call can go either way depending on the balance.
A card refund waits. The answer is 200: a re_ with status: "pending" and pending_reason: "insufficient_funds", followed by refund.created and charge.refunded. The charge's amount_refunded already counts it, and your available balance shows it taken out, below zero if need be. Nothing reaches the buyer yet. Fluveo sends the refund by itself once new sales cover it, and it then goes on like any other refund. If 45 days pass first, it ends failed with failure_reason: "insufficient_funds", refund.failed arrives, and amount_refunded goes back down. Do not create the refund again while it waits: it is already counted.
A Pix refund is refused. The answer is 400 balance_insufficient, and nothing is refunded.
- See it with
GET /v1/balance: compare the refund with your balance. - Refund an amount the balance covers, or refund again once more payments have raised the balance.
- Send that retry once, with the same
Idempotency-Key. If it comes back as the same 400 withIdempotent-Replayed: true, Fluveo kept that answer for the key: send this one new attempt with a new key. Never loop on new keys.
See Errors for the shape of every error.
Test it#
- Take a payment with
pm_card_visa(see Accept a payment). Even on a new store, your balance covers the refunds below, because the refund that completes the payment needs it to cover only the payment less Fluveo's fee (see Your balance pays for refunds). - Refund
amount=500: you get are_, thenrefund.createdandcharge.refunded; the charge showsamount_refunded: 500. - Refund again with no
amount: the rest is refunded and the charge showsrefunded: true. Read the PaymentIntent: it still showsstatus: "succeeded". - Refund once more:
400 charge_already_refunded. - Take a payment with
pm_card_refundFailand refund it: the refund is created, then fails a little later withrefund.failed. - Send step 2 again with the same
Idempotency-Key: the samere_comes back and nothing is refunded twice. - Send
reason=other: 400 onreason. An unknownpi_...answers 404. - Create a Boleto PaymentIntent (see Boleto), then refund it with
payment_intentandamount=100: it answers the 400 above, paid or not.
To refund a test Pix, pay one with the payer email succeed_immediately@example.com (see Pix), wait for payment_intent.succeeded, then refund it with its payment_intent. The refund opens in pending, as a real Pix refund does.