Docs
API 2026-05-27.dahlia Dashboard

Payments

Refund a card or Pix payment in full or in part, and follow it to the end. Boleto has no refunds.

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:

MethodFull refundPartial refundTime limitWhere the money goes
CardYesYesNone in the APIBack to the buyer's card. The card network takes some days to show it on the buyer's statement.
PixYesYes90 days after the paymentBack to the account that paid, by the payment's own Pix transaction: a devolução.
BoletoNoNoNot refundableSee 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.

Refund 5.00 of a payment
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_customer
const 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.

Parameters
  • payment_intentstringOptional

    The paid pi_ to refund. Fluveo refunds its latest charge. Send this or charge.

  • chargestringOptional

    The charge to refund: a card's ch_, a Pix's py_. If you send both, the charge must belong to that PaymentIntent.

  • amountintegerOptional

    In the smallest unit of the currency. Default: everything not yet refunded.

  • reasonstringOptional

    requested_by_customer, duplicate or fraudulent.

  • metadataobjectOptional

    Your own key-value pairs, sent as metadata[key]=value, such as metadata[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" and pending_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#

StatusMeaning
pendingThe 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".
succeededThe refund went through.
failedThe refund could not be made; the money is back in your balance (its reservation is released) and failure_reason says why.
canceledThe refund was canceled.
requires_actionThe 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#

EventWhen
refund.created + charge.refundedThe 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.updatedThe 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.updatedThe 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:

400 response
{
  "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 param charge.
  • 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 amount answers 400 charge_already_refunded.

Read and change refunds#

  • GET /v1/refunds/{id} reads one refund.
  • GET /v1/refunds lists refunds, newest first, with limit (1 to 100, default 10), starting_after or ending_before.
  • GET /v1/charges/{charge}/refunds lists the refunds of one charge.
  • POST /v1/refunds/{id} changes only metadata.

See the API reference for every field of the refund object.

Errors you may see#

ErrorWhy
400 charge_already_refundedThe whole charge was already refunded.
400 on amountThe 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 chargeA Pix paid more than 90 days ago.
400 on reasonThe reason is not one of the three above, such as reason=other.
400 balance_insufficientYour balance does not cover a Pix refund. A card refund waits instead. See When your balance is too low.
404 resource_missingThe 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 with Idempotent-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#

  1. 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).
  2. Refund amount=500: you get a re_, then refund.created and charge.refunded; the charge shows amount_refunded: 500.
  3. Refund again with no amount: the rest is refunded and the charge shows refunded: true. Read the PaymentIntent: it still shows status: "succeeded".
  4. Refund once more: 400 charge_already_refunded.
  5. Take a payment with pm_card_refundFail and refund it: the refund is created, then fails a little later with refund.failed.
  6. Send step 2 again with the same Idempotency-Key: the same re_ comes back and nothing is refunded twice.
  7. Send reason=other: 400 on reason. An unknown pi_... answers 404.
  8. Create a Boleto PaymentIntent (see Boleto), then refund it with payment_intent and amount=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.

Next#