---
title: "Pix"
description: "Create a Pix, show its code, and fulfil on payment_intent.succeeded."
url: https://docs.fluveo.com/payments/pix/
---

> Index of every page: https://docs.fluveo.com/llms.txt. Integrating Fluveo? Read https://docs.fluveo.com/agents/integrate.md first.

> Too long? This page with code in one language only: https://docs.fluveo.com/payments/pix.curl.md (cURL), https://docs.fluveo.com/payments/pix.node.md (Node.js), https://docs.fluveo.com/payments/pix.python.md (Python), https://docs.fluveo.com/payments/pix.php.md (PHP).

# Pix

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](https://docs.fluveo.com/payments/accept-a-payment.md) 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](https://docs.fluveo.com/payments.md#payment-methods).

## 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](https://docs.fluveo.com/webhooks.md).
- Optionally, the payer's name and CPF or CNPJ, or a Customer that holds them.

### Step 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:

```bash
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"
```

Node.js:

```javascript
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);
```

Python:

```python
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:

```php
<?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"];
```

### Step 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)*

```json
{
  "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://..."
    }
  }
}
```

### Step 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.

### Step 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](https://docs.fluveo.com/webhooks/signatures.md)).

> **For AI agents:**
>
> For Pix, payment_method_data[type]=pix must also appear in payment_method_types[]. The payer is optional: payment_method_data[billing_details] (name, email, phone, tax_id, each optional) or customer=cus_...; when both come, billing_details wins. Keep the response's next_action.pix_display_qr_code.data exactly as received: it is the code the buyer pays. A Pix is never paid at create time: wait for payment_intent.succeeded.

## 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**

- `payment_method_data[billing_details][name]` (string, optional): The payer's name, up to 200 characters.
- `payment_method_data[billing_details][email]` (string, optional): The payer's email. An email that is no address answers 400 `email_invalid`.
- `payment_method_data[billing_details][phone]` (string, optional): The payer's phone, such as `+5511999999999`.
- `payment_method_data[billing_details][tax_id]` (string, optional): 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.
- `customer` (string, optional): The id of a Customer; its `name` and its one tax id of type `br_cpf` or `br_cnpj` name the payer when the request sends no `billing_details`.

To store the payer once as a Customer:

**Create a Customer with a CPF**

cURL:

```bash
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"
```

Node.js:

```javascript
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();
```

Python:

```python
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:

```php
<?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](https://docs.fluveo.com/payments/customers.md).

## Pix options

**payment_method_options[pix]**

- `expires_after_seconds` (integer, optional): 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_at` (integer, optional): 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_information` (array of objects, optional): 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

| 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_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](https://docs.fluveo.com/start/testing.md).

> **For AI agents:**
>
> To prove the success path in test mode, send payment_method_data[billing_details][email]=succeed_immediately@example.com: the Pix is paid within seconds and your endpoint gets payment_intent.succeeded. Any other email is paid about 3 minutes after the confirm; expire_immediately@example.com is never paid, so with a 60-second expiry it proves the failure path. Do not call /v1/test_helpers or any /__mock/ path: they are not part of the merchant API.

## 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](https://docs.fluveo.com/errors.md) for the error object.

## Next

- [Refund a Pix](https://docs.fluveo.com/payments/refunds.md).
- [Boleto](https://docs.fluveo.com/payments/boleto.md).
- [Accept a payment](https://docs.fluveo.com/payments/accept-a-payment.md), the same call for every method.
- [Check webhook signatures](https://docs.fluveo.com/webhooks/signatures.md).
- [The PaymentIntents reference](https://docs.fluveo.com/api/payment_intents.md).
