Each payment you take is one PaymentIntent (pi_...): it holds the amount, the method and a status that moves from your first call to the end. Every payment method goes through the same call, POST /v1/payment_intents: you send the amount, your store's currency and the payment method, with confirm=true. A card is usually decided in the answer; a Pix or a Boleto waits for the buyer, and a webhook tells you when it is paid. This page shows the one-call flow, the two-call flow, 3D Secure and manual capture.
What changes by payment method#
Everything else in the request stays the same: the key, the Idempotency-Key, amount, confirm=true, metadata.
| Card | Pix | Boleto | |
|---|---|---|---|
currency | usd or brl | brl | brl |
payment_method_types[] | card | pix | boleto |
| How you name the method | payment_method=pm_... (in tests, pm_card_visa) | payment_method_data[type]=pix and the payer in payment_method_data[billing_details] | payment_method_data[type]=boleto, the tax id in payment_method_data[boleto][tax_id], and the payer's name, email and address |
| Options | capture_method, return_url | payment_method_options[pix][expires_after_seconds] | payment_method_options[boleto][expires_after_days] |
| The answer | succeeded (or requires_action for 3D Secure) | requires_action with a Pix code | requires_action with a Boleto line and PDF |
| Details | Cards | Pix | Boleto |
Take a payment in one call#
Create and confirm the PaymentIntent#
Send the amount in the currency's smallest unit, the currency, the payment method and
confirm=true. Use oneIdempotency-Keyper order, and send that same key again if you retry.For a card, name the PaymentMethod in
payment_method; in tests,pm_card_visais always approved.2000is $20.00.Card: $20.00curl https://api.fluveo.com/v1/payment_intents \ -H "Authorization: Bearer sk_test_..." \ -H "Idempotency-Key: order-1042-pay" \ -d amount=2000 \ -d currency=usd \ -d "payment_method_types[]=card" \ -d payment_method=pm_card_visa \ -d confirm=true \ -d "metadata[order_id]=1042"const response = await fetch("https://api.fluveo.com/v1/payment_intents", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Idempotency-Key": "order-1042-pay", }, body: new URLSearchParams({ amount: "2000", currency: "usd", "payment_method_types[]": "card", payment_method: "pm_card_visa", confirm: "true", "metadata[order_id]": "1042", }), }); const paymentIntent = await response.json(); console.log(response.status, paymentIntent.status);import requests response = requests.post( "https://api.fluveo.com/v1/payment_intents", headers={ "Authorization": "Bearer sk_test_...", "Idempotency-Key": "order-1042-pay", }, data={ "amount": 2000, "currency": "usd", "payment_method_types[]": "card", "payment_method": "pm_card_visa", "confirm": "true", "metadata[order_id]": "1042", }, ) payment_intent = response.json() print(response.status_code, payment_intent["status"])<?php $ch = curl_init("https://api.fluveo.com/v1/payment_intents"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer sk_test_...", "Idempotency-Key: order-1042-pay", ], CURLOPT_POSTFIELDS => http_build_query([ "amount" => 2000, "currency" => "usd", "payment_method_types" => ["card"], "payment_method" => "pm_card_visa", "confirm" => "true", "metadata" => ["order_id" => "1042"], ]), ]); $paymentIntent = json_decode(curl_exec($ch), true); echo $paymentIntent["status"];For a Pix, describe the method and its payer in
payment_method_data.1000is R$ 10,00.Pix in BRL: R$ 10,00curl https://api.fluveo.com/v1/payment_intents \ -H "Authorization: Bearer sk_test_..." \ -H "Idempotency-Key: order-1042-pay" \ -d amount=1000 \ -d currency=brl \ -d "payment_method_types[]=pix" \ -d "payment_method_data[type]=pix" \ -d "payment_method_data[billing_details][name]=Ada Pagadora" \ -d "payment_method_data[billing_details][email]=ada@example.com" \ --data-urlencode "payment_method_data[billing_details][phone]=+5511999999999" \ -d "payment_method_data[billing_details][tax_id]=12345678909" \ -d confirm=true \ -d "metadata[order_id]=1042"const response = await fetch("https://api.fluveo.com/v1/payment_intents", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Idempotency-Key": "order-1042-pay", }, body: new URLSearchParams({ amount: "1000", currency: "brl", "payment_method_types[]": "pix", "payment_method_data[type]": "pix", "payment_method_data[billing_details][name]": "Ada Pagadora", "payment_method_data[billing_details][email]": "ada@example.com", "payment_method_data[billing_details][phone]": "+5511999999999", "payment_method_data[billing_details][tax_id]": "12345678909", confirm: "true", "metadata[order_id]": "1042", }), }); const paymentIntent = await response.json(); console.log(response.status, paymentIntent.status, paymentIntent.next_action);import requests response = requests.post( "https://api.fluveo.com/v1/payment_intents", headers={ "Authorization": "Bearer sk_test_...", "Idempotency-Key": "order-1042-pay", }, data={ "amount": 1000, "currency": "brl", "payment_method_types[]": "pix", "payment_method_data[type]": "pix", "payment_method_data[billing_details][name]": "Ada Pagadora", "payment_method_data[billing_details][email]": "ada@example.com", "payment_method_data[billing_details][phone]": "+5511999999999", "payment_method_data[billing_details][tax_id]": "12345678909", "confirm": "true", "metadata[order_id]": "1042", }, ) payment_intent = response.json() print(response.status_code, payment_intent["status"], payment_intent["next_action"])<?php $ch = curl_init("https://api.fluveo.com/v1/payment_intents"); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ "Authorization: Bearer sk_test_...", "Idempotency-Key: order-1042-pay", ], CURLOPT_POSTFIELDS => http_build_query([ "amount" => 1000, "currency" => "brl", "payment_method_types" => ["pix"], "payment_method_data" => [ "type" => "pix", "billing_details" => [ "name" => "Ada Pagadora", "email" => "ada@example.com", "phone" => "+5511999999999", "tax_id" => "12345678909", ], ], "confirm" => "true", "metadata" => ["order_id" => "1042"], ]), ]); $paymentIntent = json_decode(curl_exec($ch), true); echo $paymentIntent["status"];A Boleto is the same call with the fields of Boleto.
Read the result#
A card that is approved answers
200withstatus: "succeeded". The PaymentIntent'slatest_chargeis thech_of the charge, andamount_receivedis what the buyer paid.Card: response (trimmed){ "id": "pi_...", "object": "payment_intent", "amount": 2000, "amount_received": 2000, "currency": "usd", "status": "succeeded", "latest_charge": "ch_...", "payment_method": "pm_...", "capture_method": "automatic_async", "metadata": { "order_id": "1042" } }A Pix or a Boleto answers
status: "requires_action", andnext_actionholds what you show the buyer: apix_display_qr_code(see Pix) orboleto_display_details(see Boleto). It is never paid at create time. No Charge exists yet:latest_chargestaysnulluntil the buyer pays or the code expires, and the Charge shows up withcharge.succeededorcharge.failed.Pix: response (trimmed){ "id": "pi_...", "object": "payment_intent", "amount": 1000, "currency": "brl", "status": "requires_action", "latest_charge": null, "next_action": { "type": "pix_display_qr_code", "pix_display_qr_code": { "data": "00020101021226...", "expires_at": 1767225600 } } }capture_methodisautomatic_asyncwhen you send none. It captures exactly likeautomatic: the money is taken as soon as the card is approved, so asucceededPaymentIntent is already captured and needs no capture call. You may sendcapture_method=automaticinstead and the payment behaves the same; the field then showsautomatic. Onlymanualholds the money until you capture (see Authorize now, capture later).Fulfil the order from the webhook#
Fluveo sends
payment_intent.succeeded(withcharge.succeeded) to your webhook endpoint when the money is in. Ship the order when that event arrives and itsFluveo-Signaturechecks out, once per PaymentIntent id. The answer to your POST is only a hint: a payment that needs 3D Secure, a Pix, a Boleto, or a card you capture later, finishes after the call returns.In test mode, Fluveo's test bank pays a Pix or a Boleto for you, through the same path as a real payment: you get the same
charge.succeededandpayment_intent.succeeded, thencharge.updatedonce the sale's balance transaction is booked, and the sale shows in your balance and your balance transactions as a real one does. The payer email inpayment_method_data[billing_details][email]decides when:- an address whose part before the
@ends insucceed_immediately, such assucceed_immediately@example.com, is paid within seconds of the confirm; - an address ending in
expire_immediately,expire_with_delayorfill_neverbefore the@is never paid: it waits and expires on its own deadline (a Pix's expiry, a Boleto's clearing period), so give a Pix a shortexpires_after_secondsto watch it expire; - any other address, such as
ada@example.com, is paid about 3 minutes after the confirm, unless the Pix code expires first; - a Pix with no email is never paid on its own.
You can also pay a waiting one yourself: open the Pix's
hosted_instructions_urlor the Boleto'shosted_voucher_urlin a browser, follow simular o pagamento and press Autorizar pagamento de teste. A Pix or Boleto that is already paid, canceled or expired is never paid again. With a live key nothing is simulated: the email is just the payer's email, and the pages have no test link.- an address whose part before the
Take a payment in two calls#
Create the PaymentIntent first when you want its id before the buyer picks a card, for example to store it with the order. Then confirm it.
curl https://api.fluveo.com/v1/payment_intents \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: order-1043-create" \
-d amount=4500 \
-d currency=usd \
-d "payment_method_types[]=card"The PaymentIntent starts in requires_payment_method. Confirm it with the card:
curl https://api.fluveo.com/v1/payment_intents/pi_.../confirm \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: order-1043-confirm" \
-d payment_method=pm_card_visaThe amount in the smallest unit of the currency:
2000is $20.00 inusd,1000is R$ 10,00 inbrl. A Boleto takes at least200.A currency your account accepts, in lower case, such as the
default_currencyofGET /v1/account. Any other is a 400 oncurrency.card,pixorboleto. A method inpayment_method_data[type]must also be in this list. The currency each method charges in is on Payments.The card to charge: a
pm_id, or a test card name likepm_card_visa.Options of one method:
payment_method_options[pix]orpayment_method_options[boleto].trueconfirms in the same call. Defaultfalse.For a card:
automatic_async(the default),automaticormanual.automatic_asyncandautomaticboth capture as soon as the card is approved;manualholds the money until you capture. See Authorize now, capture later.automatic(the default) ormanual. Withmanual, a card that passes 3D Secure does not charge on its own: the PaymentIntent stops inrequires_confirmationand charges only when you call/confirmagain.Where the buyer comes back after 3D Secure. Only with
confirm=true. Required when you leave outpayment_method_types, unless you sendautomatic_payment_methods[allow_redirects]=neveroroff_session=true. See 3D Secure.A
cus_of your store. For a Pix, the Customer's name and tax id (br_cpforbr_cnpj) name the payer when the request carries none. A Boleto never takes its payer from the Customer: send it in the request (see Boleto).Your own text about the payment.
The buyer's email for the receipt.
Text added to what the buyer sees on their card statement.
Your own key-value pairs, sent as
metadata[key]=value. Keys that start withfluveo_are refused.
The API reference lists every route of the PaymentIntent. A parameter Fluveo does not take answers a 400 that names it, so an unknown field never passes in silence.
Statuses#
| Status | What it means | What you do |
|---|---|---|
requires_payment_method | No payment method yet, the last card was declined, or a Pix or Boleto expired unpaid. | Confirm with a payment method, or create a new Pix or Boleto. |
requires_confirmation | A card is set but the payment is not confirmed. An update that sets another payment_method on a waiting Pix drops its code and lands here. With confirmation_method=manual, a card that passed 3D Secure waits here too, with no next_action, no Charge and no event. | Call /confirm. |
requires_action | The buyer must act: authenticate the card (3D Secure), or pay the Pix or the Boleto. | Send the buyer to next_action.redirect_to_url.url, or show the Pix code or the Boleto. A confirm with another card replaces a 3D Secure or Pix wait; a waiting Boleto refuses it with 400. |
processing | The card network has not answered yet, or a Boleto's line is not ready yet. | Wait for the webhook. |
requires_capture | The card is authorized; money is held, not taken. | Capture or cancel. |
succeeded | The money is taken. | Fulfil the order. |
canceled | Closed for good; nothing will be charged on it. | Nothing; create a new one to try again. |
Events#
| Event | When |
|---|---|
payment_intent.created | The PaymentIntent was created. |
payment_intent.requires_action | The buyer must authenticate (3D Secure), or the Pix code or Boleto is ready to show. |
payment_intent.processing | A card payment is waiting on the card network. A Boleto never sends it. |
charge.succeeded + payment_intent.succeeded | The buyer paid and the money is in. |
charge.succeeded + payment_intent.amount_capturable_updated | A manual-capture card payment is authorized. |
charge.captured + payment_intent.succeeded | You captured an authorized payment. |
charge.failed + payment_intent.payment_failed | The card was declined, or a Pix or Boleto expired unpaid. The PaymentIntent goes back to requires_payment_method. |
payment_intent.canceled | The PaymentIntent moved to canceled. |
A Pix or a Boleto creates its events in this order: payment_intent.created, payment_intent.requires_action, then the success or failure events. Its charge appears only with them: while the buyer has not paid, latest_charge is null. Events can arrive more than once and out of order. Keep the evt_ ids you handled and skip a repeat; read the PaymentIntent again when the order matters. The full list is on Event types.
When a card is declined#
A declined card answers 402 with an error of type card_error. error.code says why (for example card_declined, expired_card, incorrect_cvc), error.decline_code gives the bank's reason when there is one, and error.payment_intent holds the PaymentIntent, now back in requires_payment_method.
{
"error": {
"type": "card_error",
"code": "card_declined",
"decline_code": "generic_decline",
"message": "Your card was declined.",
"payment_intent": { "id": "pi_...", "status": "requires_payment_method" }
}
}Ask the buyer for another card and confirm the same PaymentIntent again, with a new Idempotency-Key because it is a new attempt. See Errors for every code.
3D Secure#
Some banks ask the buyer to prove they own the card. Fluveo handles this with a redirect:
- Confirm with
return_url, the page on your site where the buyer should land afterwards. - If the bank asks for authentication, the answer has
status: "requires_action"and anext_actionof typeredirect_to_url. - Send the buyer's browser to
next_action.redirect_to_url.url. It is a Fluveo page; the bank's check opens inside it, so the address bar stays on Fluveo. In test mode it opens a test page with Complete and Fail instead (see Testing). - The buyer comes back to your
return_url. Do not trust that visit alone: read the PaymentIntent, or wait forpayment_intent.succeededorpayment_intent.payment_failed.
{
"status": "requires_action",
"next_action": {
"type": "redirect_to_url",
"redirect_to_url": {
"url": "https://.../v1/card_actions/...",
"return_url": "https://shop.example.com/orders/1042/return"
}
}
}Authorize now, capture later#
Use capture_method=manual when you only charge once you ship. The card is authorized and the money is held; nothing is taken until you capture. This works for every card payment.
curl https://api.fluveo.com/v1/payment_intents \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: order-1044-pay" \
-d amount=5000 \
-d currency=usd \
-d "payment_method_types[]=card" \
-d payment_method=pm_card_visa \
-d capture_method=manual \
-d confirm=trueThe PaymentIntent is now requires_capture and amount_capturable is 5000. Capture all of it with no body, or a smaller amount with amount_to_capture; the rest is released to the buyer.
curl https://api.fluveo.com/v1/payment_intents/pi_.../capture \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: order-1044-capture" \
-d amount_to_capture=3000const response = await fetch("https://api.fluveo.com/v1/payment_intents/pi_.../capture", {
method: "POST",
headers: {
Authorization: "Bearer sk_test_...",
"Idempotency-Key": "order-1044-capture",
},
body: new URLSearchParams({ amount_to_capture: "3000" }),
});
console.log((await response.json()).status);import requests
response = requests.post(
"https://api.fluveo.com/v1/payment_intents/pi_.../capture",
headers={
"Authorization": "Bearer sk_test_...",
"Idempotency-Key": "order-1044-capture",
},
data={"amount_to_capture": 3000},
)
print(response.json()["status"])<?php
$ch = curl_init("https://api.fluveo.com/v1/payment_intents/pi_.../capture");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer sk_test_...",
"Idempotency-Key: order-1044-capture",
],
CURLOPT_POSTFIELDS => http_build_query(["amount_to_capture" => 3000]),
]);
echo json_decode(curl_exec($ch), true)["status"];A capture makes the PaymentIntent succeeded and sends charge.captured and payment_intent.succeeded. One capture per PaymentIntent: final_capture and multiple captures are not available.
To drop an authorization instead, cancel it. cancellation_reason is optional and takes requested_by_customer, duplicate, fraudulent or abandoned.
curl https://api.fluveo.com/v1/payment_intents/pi_.../cancel \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: order-1044-cancel" \
-d cancellation_reason=requested_by_customerCapturing or canceling a PaymentIntent in the wrong status answers 400 payment_intent_unexpected_state.
Test it#
Use these test card names with a sk_test_ key. Each use makes a new pm_ for your store. Every test value is on Testing.
| PaymentMethod | Result |
|---|---|
pm_card_visa | Approved (Visa ending 4242). |
pm_card_mastercard, pm_card_amex, pm_card_visa_debit | Approved. |
pm_card_visa_chargeDeclined | 402 card_declined, decline_code generic_decline. |
pm_card_visa_chargeDeclinedInsufficientFunds | 402 card_declined, decline_code insufficient_funds. |
pm_card_chargeDeclinedExpiredCard | 402 expired_card. |
pm_card_chargeDeclinedIncorrectCvc | 402 incorrect_cvc. |
pm_card_chargeDeclinedProcessingError | 402 processing_error. |
pm_card_authenticationRequired | Asks for 3D Secure: confirm it with a return_url. |
- Create and confirm with
pm_card_visa: the answer issucceeded, and your endpoint getspayment_intent.succeeded. - Do it again with
pm_card_visa_chargeDeclined: the answer is402, then confirm the same PaymentIntent withpm_card_visaand a new key. - Authorize with
capture_method=manual, then captureamount_to_capture=1500:amount_receivedis1500. - Confirm
pm_card_authenticationRequiredwith areturn_url: the answer isrequires_action. Opennext_action.redirect_to_url.urlin a browser and complete the test page. - Send the first request again with the same
Idempotency-Key: you get the same PaymentIntent back, with the headerIdempotent-Replayed: true, and no second charge. - Create a Pix with the payer email
succeed_immediately@example.com: the answer isrequires_actionwith apix_display_qr_code, and a few seconds later the PaymentIntent issucceededand your endpoint getspayment_intent.succeeded. See Pix.
Next#
- Use the hosted checkout page to let Fluveo take the card for you.
- Refund a payment
- Check webhook signatures
- Idempotency