Docs
API 2026-05-27.dahlia Dashboard

Payments

Create a Pix, show its code, and fulfil on payment_intent.succeeded.

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.succeeded and payment_intent.payment_failed. See Webhooks.
  • Optionally, the payer's name and CPF or CNPJ, or a Customer that holds them.
  1. Create and confirm the PaymentIntent#

    Send the amount in the currency's smallest unit (1000 is R$ 10,00), the currency, pix as the method and confirm=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 own Idempotency-Key, and send that same key again on a retry.

    Create a Pix payment
    curl 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"];
  2. Read the Pix code from next_action#

    The PaymentIntent comes back in requires_action, and next_action.type is pix_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://..."
        }
      }
    }
  3. Show it to the buyer#

    Put the QR image (image_url_png or image_url_svg) on screen. Next to it, print the text in data with 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_at is the deadline in Unix seconds; show it as a date and time.

  4. Fulfil on the webhook#

    Once the buyer pays, the PaymentIntent and its Charge move to succeeded, amount_received equals the amount, and Fluveo creates charge.succeeded and payment_intent.succeeded. The two deliveries may reach you in either order. Ship on payment_intent.succeeded, once per PaymentIntent id, and only after its Fluveo-Signature header 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.

Payer

To store the payer once as a Customer:

Create a Customer with a CPF
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#

payment_method_options[pix]
  • expires_after_secondsintegerOptional

    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.

  • expires_atintegerOptional

    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.

  • additional_informationarray of objectsOptional

    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). Needs confirm=true in 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, with next_action: null and last_payment_error.code payment_intent_payment_attempt_expired;
  • its Charge is failed with failure_code payment_intent_payment_attempt_expired;
  • Fluveo creates charge.failed and payment_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#

EventWhen
payment_intent.createdThe PaymentIntent was created.
payment_intent.requires_actionThe code is ready to show.
charge.succeeded, payment_intent.succeededThe payer paid. Fulfil now.
charge.failed, payment_intent.payment_failedThe 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.

FieldTest 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_action with a pix_display_qr_code. The QR images decode to the same text as data.
  • Payment. Send the payer email succeed_immediately@example.com: a few seconds after the confirm the PaymentIntent is succeeded, its Charge is succeeded with balance_transaction set, and your endpoint gets charge.succeeded and payment_intent.succeeded, then charge.updated. With ada@example.com the 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 with payment_method_options[pix][expires_after_seconds]=60. A few minutes later, GET /v1/payment_intents/{id} shows requires_payment_method with payment_intent_payment_attempt_expired, and your endpoint gets charge.failed and payment_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 seeWhy
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_invalidThe 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#