Pix is an instant bank transfer that the buyer approves in their own bank app. It goes through the same PaymentIntent call as a card: only the method fields change. You create the payment, Fluveo answers with a code, the buyer scans or pastes that code, and a webhook tells you seconds after the money lands. You decide how long the code can be paid, 24 hours at most. The currency Pix charges in is in the methods table.
Before you start#
- Your test secret key
sk_test_..., on an account with Pix turned on. - A webhook endpoint that listens for
payment_intent.succeededandpayment_intent.payment_failed. See Webhooks. - Optionally, the payer's name and CPF or CNPJ, or a Customer that holds them.
Create and confirm the PaymentIntent#
Send the amount in the currency's smallest unit (
1000is R$ 10,00), the currency,pixas the method andconfirm=true. The payer is optional; this example names one.payment_method_options[pix][expires_after_seconds]sets the life of the code, from 1 to 86400 seconds. Give the order its ownIdempotency-Key, and send that same key again on a retry.Create a Pix paymentcurl https://api.fluveo.com/v1/payment_intents \ -H "Authorization: Bearer sk_test_..." \ -H "Idempotency-Key: order-1042-pix" \ -d amount=1000 \ -d currency=brl \ -d "payment_method_types[]=pix" \ -d confirm=true \ -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" \ -d "payment_method_data[billing_details][phone]=+5511999999999" \ -d "payment_method_data[billing_details][tax_id]=12345678909" \ -d "payment_method_options[pix][expires_after_seconds]=1800"const body = new URLSearchParams([ ["amount", "1000"], ["currency", "brl"], ["payment_method_types[]", "pix"], ["confirm", "true"], ["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"], ["payment_method_options[pix][expires_after_seconds]", "1800"], ]); const response = await fetch("https://api.fluveo.com/v1/payment_intents", { method: "POST", headers: { Authorization: "Bearer sk_test_...", "Idempotency-Key": "order-1042-pix", }, body, }); const paymentIntent = await response.json(); console.log(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-pix", }, data={ "amount": 1000, "currency": "brl", "payment_method_types[]": "pix", "confirm": "true", "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", "payment_method_options[pix][expires_after_seconds]": 1800, }, ) payment_intent = response.json() print(payment_intent["status"], payment_intent["next_action"])<?php $fields = [ "amount" => 1000, "currency" => "brl", "payment_method_types[]" => "pix", "confirm" => "true", "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", "payment_method_options[pix][expires_after_seconds]" => 1800, ]; $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-pix", ], CURLOPT_POSTFIELDS => http_build_query($fields), ]); $paymentIntent = json_decode(curl_exec($ch), true); echo $paymentIntent["status"];Read the Pix code from next_action#
The PaymentIntent comes back in
requires_action, andnext_action.typeispix_display_qr_code. Store the PaymentIntent id on your order.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, "hosted_instructions_url": "https://...", "image_url_png": "https://...", "image_url_svg": "https://..." } } }Show it to the buyer#
Put the QR image (
image_url_pngorimage_url_svg) on screen. Next to it, print the text indatawith a button that copies it: that text is the Pix copy-paste code (Pix copia e cola), for a buyer who pays on the same phone.expires_atis the deadline in Unix seconds; show it as a date and time.Fulfil on the webhook#
Once the buyer pays, the PaymentIntent and its Charge move to
succeeded,amount_receivedequals the amount, and Fluveo createscharge.succeededandpayment_intent.succeeded. The two deliveries may reach you in either order. Ship onpayment_intent.succeeded, once per PaymentIntent id, and only after itsFluveo-Signatureheader checks out (Webhook signatures).
Who pays: the payer#
You may leave the payer out. If you have it, put it in payment_method_data[billing_details] or point to a Customer; when a request carries both, billing_details is used. The bank sees a named payer only when the Pix has a name and a CPF or CNPJ with valid check digits.
The payer's name, up to 200 characters.
The payer's email. An email that is no address answers 400
email_invalid.The payer's phone, such as
+5511999999999.The payer's CPF (11 digits) or CNPJ (14 digits). Dots, dashes and slashes are ignored. A number with wrong check digits is not refused: the Pix goes out without a named payer.
The id of a Customer; its
nameand its one tax id of typebr_cpforbr_cnpjname the payer when the request sends nobilling_details.
To store the payer once as a Customer:
curl https://api.fluveo.com/v1/customers \
-H "Authorization: Bearer sk_test_..." \
-H "Idempotency-Key: customer-ada-1" \
-d "name=Ada Pagadora" \
-d "email=ada@example.com" \
-d "tax_id_data[0][type]=br_cpf" \
-d "tax_id_data[0][value]=12345678909"const response = await fetch("https://api.fluveo.com/v1/customers", {
method: "POST",
headers: { Authorization: "Bearer sk_test_...", "Idempotency-Key": "customer-ada-1" },
body: new URLSearchParams([
["name", "Ada Pagadora"],
["email", "ada@example.com"],
["tax_id_data[0][type]", "br_cpf"],
["tax_id_data[0][value]", "12345678909"],
]),
});
const customer = await response.json();import requests
customer = requests.post(
"https://api.fluveo.com/v1/customers",
headers={"Authorization": "Bearer sk_test_...", "Idempotency-Key": "customer-ada-1"},
data={
"name": "Ada Pagadora",
"email": "ada@example.com",
"tax_id_data[0][type]": "br_cpf",
"tax_id_data[0][value]": "12345678909",
},
).json()<?php
$ch = curl_init("https://api.fluveo.com/v1/customers");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer sk_test_...", "Idempotency-Key: customer-ada-1"],
CURLOPT_POSTFIELDS => http_build_query([
"name" => "Ada Pagadora",
"email" => "ada@example.com",
"tax_id_data[0][type]" => "br_cpf",
"tax_id_data[0][value]" => "12345678909",
]),
]);
$customer = json_decode(curl_exec($ch), true);Then create the Pix with customer=cus_... in place of the billing_details fields.
To find, update or delete that Customer, or change its tax IDs later, see Customers.
Pix options#
How long the code stays payable, from 1 to 86400 seconds (24 hours). Without this option and without
expires_at, the code lasts 24 hours.The Unix time when the code stops being payable, in the future and at most 24 hours from now. Send either this or
expires_after_seconds, never both.Extra lines the payer sees in their bank app: 1 to 50 lines, each sent as
payment_method_options[pix][additional_information][0][name](up to 50 characters) and[value](up to 200 characters). Needsconfirm=truein the same request.
Switch to another method#
While the code waits, the buyer can pick another method. Confirm the PaymentIntent with the new payment_method, or update it with that payment_method (it answers requires_confirmation with next_action: null) and then confirm. The old code is dropped with no event; if the buyer still pays it, Fluveo returns that money to the payer on its own.
When nobody pays: expiry#
A Pix that is not paid by expires_at expires, after a short safety margin while Fluveo checks with the bank. Then:
- the PaymentIntent goes back to
requires_payment_method, withnext_action: nullandlast_payment_error.codepayment_intent_payment_attempt_expired; - its Charge is
failedwithfailure_codepayment_intent_payment_attempt_expired; - Fluveo creates
charge.failedandpayment_intent.payment_failed; their webhook deliveries can repeat.
Release the order, or create a new Pix for it with a new Idempotency-Key. A Pix paid after it expired is not added to the PaymentIntent: Fluveo returns that money to the payer on its own, and the PaymentIntent stays requires_payment_method.
Events of a Pix#
| Event | When |
|---|---|
payment_intent.created | The PaymentIntent was created. |
payment_intent.requires_action | The code is ready to show. |
charge.succeeded, payment_intent.succeeded | The payer paid. Fulfil now. |
charge.failed, payment_intent.payment_failed | The code expired unpaid. |
A repeated bank notice does not create another event for the same transition. Webhook deliveries can repeat and arrive out of order. Deduplicate them by event id.
Test it#
Use a test key sk_test_... and the test payer below. Everything is simulated: no real Pix is created.
| Field | Test value |
|---|---|
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 (a CPF with valid check digits) |
What you can test today with only your key:
- Create. The PaymentIntent answers
requires_actionwith apix_display_qr_code. The QR images decode to the same text asdata. - Payment. Send the payer email
succeed_immediately@example.com: a few seconds after the confirm the PaymentIntent issucceeded, its Charge issucceededwithbalance_transactionset, and your endpoint getscharge.succeededandpayment_intent.succeeded, thencharge.updated. Withada@example.comthe same happens about 3 minutes after the confirm. - Expiry. Send the payer email
expire_immediately@example.com, which is never paid, and create the Pix withpayment_method_options[pix][expires_after_seconds]=60. A few minutes later,GET /v1/payment_intents/{id}showsrequires_payment_methodwithpayment_intent_payment_attempt_expired, and your endpoint getscharge.failedandpayment_intent.payment_failed. - Validation. An email that is not an address, both expiry options in one request, or an expiry over 86400 seconds answers 400 and names the
param.
Paying a test Pix. Fluveo's test bank pays a test Pix through the same path as a real one, so you get the same events, balance and balance transactions. The payer email decides when: a local part (before the @) ending in succeed_immediately is paid within seconds; one ending in expire_immediately, expire_with_delay or fill_never is never paid and expires at its own expires_at; any other email is paid about 3 minutes after the confirm, unless the code expires first; a Pix with no email is never paid on its own. To pay a waiting Pix yourself, open the hosted_instructions_url in its pix_display_qr_code, follow simular o pagamento and press Autorizar pagamento de teste. A paid, canceled or expired Pix is never paid again, and nothing is simulated with a live key. See Testing.
Common errors#
| What you see | Why |
|---|---|
400 payment_method_not_available, param payment_method_types, "Pix is not available for this payment." | Pix is not turned on for the account. |
400, param currency, "The currency provided (...) is invalid. Payments with pix support the following currencies: ..." | The currency is not one Pix charges in. |
400 email_invalid | The payer's email is not an address. |
400, "capture_method=manual is not supported by payment method type pix." | A Pix is captured when it is paid: leave capture_method out. |
400, param payment_method_options[pix][expires_at] or [expires_after_seconds] | Both expiry options were sent, or the deadline is in the past or more than 24 hours ahead. |
See Errors for the error object.
Next#
- Refund a Pix.
- Boleto.
- Accept a payment, the same call for every method.
- Check webhook signatures.
- The PaymentIntents reference.