PIX Payments
PIX is Brazil's instant-payment rail. Funds settle in seconds, 24/7. Rotta Pay gives you a single endpoint that returns a BR Code (Pix Copia e Cola) and a QR Code artifact ready to render in your checkout — in the same response that creates the charge.
#Flow at a glance
1. POST /v1/charges ────────▶ Rotta Pay returns a Charge: ch_…, status "pending", pix.br_code + QR
2. Render QR + Pix Copia e Cola in your UI
3. Customer scans / pastes in their bank app
4. Webhook charge.paid arrives at your endpoint
5. Mark order as paid#1. Create the charge
Endpoint POST /v1/charges (Pix-first shortcut: POST /v1/pix/charges fixes payment_method: "pix" and returns the exact same Charge)
Headers
apikey: ak_live_...
Idempotency-Key: <unique per charge you intend to create>
Content-Type: application/jsonBody
| Field | Type | Required | Notes |
|---|---|---|---|
amount | integer | yes | Cents. 1000 = R$ 10,00. Must be a positive integer. |
currency | string | no | Defaults to "BRL" — the only currency Pix charges accept. |
payment_method | string | yes | "pix" (the only value accepted today; the /v1/pix/charges shortcut fills it in for you). |
description | string | no | Up to 500 chars. Echoed back inside metadata.description. |
expires_in | integer | no | Seconds until the charge expires (the Pix QR time-to-live). 60–86400 (24 h — the ceiling for an immediate Pix charge; larger values are capped). Omit it to use the account's default window (usually 3600 = 1 h). pix.expires_at in the response is always the authoritative expiry. |
customer.name | string | no | Shown on the bank statement when supported. |
customer.document | string | no | CPF or CNPJ. |
customer.email | string | no | Used for receipts. |
customer.phone | string | no | |
expected_payer_tax_id | string | no | ⚠️ LOCKS the charge to this CPF/CNPJ: with the lock active on your account, only this document can pay and a payment from any other is refused (or returned, depending on the provider). Do not send it if the payer may be someone else. Without the lock configured on your account, the field is ignored and the charge is created normally. See Payer-CPF lock below. Requires customer.name. Must be a valid CPF/CNPJ and, if you also send customer.document, must match it. |
metadata | object | no | Free-form key/value, returned verbatim (reserved/underscore-prefixed keys are stripped). |
Example
curl -X POST https://rottapay.com/v1/charges \
-H "apikey: $ROTTAPAY_API_KEY" \
-H "Idempotency-Key: ORD-7821" \
-H "Content-Type: application/json" \
-d '{
"amount": 24900,
"currency": "BRL",
"payment_method": "pix",
"description": "Pedido #7821",
"expires_in": 1800,
"customer": { "name": "Maria Silva", "document": "12345678901" },
"metadata": { "order_id": "ORD-7821", "sku": "premium-monthly" }
}'Response 201 Created
{
"id": "ch_a1b2c3d4-0000-0000-0000-000000000009",
"object": "charge",
"amount": 24900,
"currency": "BRL",
"status": "pending",
"payment_method": "pix",
"customer": { "name": "Maria Silva", "document": "12345678901" },
"pix": {
"br_code": "000201BRCODEPIX",
"qr_code_url": "https://qr.example/img.png",
"expires_at": "2026-07-23T15:00:00.000Z"
},
"checkout_url": "https://checkout.rottapay.com/a1b2c3d4-0000-0000-0000-000000000009",
"settlement": {},
"metadata": { "order_id": "ORD-7821", "sku": "premium-monthly", "description": "Pedido #7821" },
"created_at": "2026-07-23T14:30:00.000Z"
}| Field | What to do with it |
|---|---|
pix.br_code | The EMV "Pix Copia e Cola" payload. Render in a copy-to-clipboard button. |
pix.qr_code_url / qr_code_base64 | One of the two is always present when Pix is available. Drop the URL into <img src="...">, or decode the base64 PNG. |
checkout_url | Rotta Pay's hosted checkout for this charge — a ready-made payment page (QR, copy-paste, live status). Redirect the payer here instead of building your own screen. Also returned by GET /v1/charges/{id}, so you can fetch it later from a stored id. You can't build this URL yourself: the checkout expects the id without the ch_ prefix. |
pix.expires_at | When this charge expires. Show a countdown; after this, status moves to expired. |
pix.txid | BACEN TXID — optional, present only when the provider bound one. Never treat it as the charge id. |
settlement | {} at creation. settlement.end_to_end_id appears only after the charge reaches paid and has been settled/reconciled — never on creation. |
#TXID e end_to_end_id
ch_… is always the Rotta Pay charge id — the one you store, look up, and reconcile webhooks by. pix.txid and settlement.end_to_end_id are contextual Pix data defined by BACEN, surfaced only when applicable:
{
"id": "ch_a1b2c3d4-0000-0000-0000-000000000009",
"pix": { "txid": "BACEN-TXID-77" },
"settlement": {}
}charge.id !== charge.pix.txid, always — even when both are present. Never key your database on the TXID or the end_to_end_id; use ch_….
#Payer-CPF lock
Set expected_payer_tax_id to require that only one specific CPF/CNPJ pays a charge — for example, to guarantee the buyer pays from their own account and not a third party's.
⚠️ Sending this field LOCKS the charge. With the lock active on your account, only the given CPF/CNPJ can pay: anyone paying from another account gets the payment refused by the bank (or returned, depending on the provider). Send it only when you want that restriction — e.g. a bet or deposit that must come from the customer's own account. If the payer may be someone else (a parent paying for a child, a company paying for a partner), do not send it: use
customer.documentalone, which is informational and locks nothing.
Availability. The lock is off by default: it depends on the provider serving your account and only applies where support has configured it. On an account without the lock configured, a charge carrying
expected_payer_tax_idis created normally, without the lock — any payer can pay and the payment is credited. You can tell from the response: the charge does not includeexpected_payer_tax_id. When the lock applies, the field is echoed on the charge and the rules below apply. Want the lock? Talk to support.
curl -X POST https://rottapay.com/v1/charges \
-H "apikey: $ROTTAPAY_API_KEY" \
-H "Idempotency-Key: ORD-7821" \
-H "Content-Type: application/json" \
-d '{
"amount": 24900,
"payment_method": "pix",
"customer": { "name": "Maria Silva", "document": "12345678901" },
"expected_payer_tax_id": "12345678901"
}'How it works:
- The charge is created and paid exactly like any other Pix charge.
- At settlement, Rotta Pay reads the real payer's document from the bank and compares it to
expected_payer_tax_id. - Match → the charge becomes
paidand your balance is credited, as usual. - A different payer paid → the charge is NOT credited. Rotta Pay automatically returns the money to whoever paid (a standard Pix devolução, addressed by the original
end_to_end_id) and the charge ends uprefunded. You never receive third-party funds. - The bank did not disclose the payer's document → the charge is held in
processing(never credited) for manual review, rather than crediting an unverifiable payer.
Notes:
- This is a settlement-time guarantee, not a bank-level block: BACEN lets any payer scan the QR, so the payer's app may still show the charge. The protection is that a non-matching payment is captured and returned automatically — your merchant balance is only ever credited for a matching payer.
expected_payer_tax_idrequirescustomer.name(needed on the charge and for the return), must be a checksum-valid CPF (11 digits) or CNPJ (14 digits), and — if you also sendcustomer.document— must match it. Otherwise the request is rejected400.- The value is echoed back on the charge (
expected_payer_tax_id); the real payer's document is never exposed.
#2. Fetch the charge
Endpoint GET /v1/charges/{id}
curl https://rottapay.com/v1/charges/ch_a1b2c3d4-0000-0000-0000-000000000009 \
-H "apikey: $ROTTAPAY_API_KEY"Accepts either the ch_…-prefixed id or the raw id. Once the Pix has been paid and settled, settlement.end_to_end_id appears and status becomes paid:
{
"id": "ch_a1b2c3d4-0000-0000-0000-000000000009",
"object": "charge",
"status": "paid",
"amount": 24900,
"currency": "BRL",
"payment_method": "pix",
"customer": { "name": "Maria Silva", "document": "12345678901" },
"settlement": { "end_to_end_id": "E-END-TO-END-99" },
"metadata": { "order_id": "ORD-7821" },
"created_at": "2026-07-23T14:30:00.000Z"
}#Polling vs webhooks
Rely exclusively on webhooks for fulfillment / database writes — webhooks are the source of truth. GET /v1/charges/{id} is for on-demand status checks (support tooling, a manual "check status" button), not a polling loop.
#3. Receive the webhook
When the payer pays, you get a signed POST to your registered webhook endpoint with the canonical event envelope:
{
"id": "evt_5f3a9b2c1d4e5f6a7b8c9d0e1f2a3b4c",
"object": "event",
"api_version": "2026-07-23",
"type": "charge.paid",
"created_at": "2026-07-23T14:31:00.000Z",
"data": {
"object": {
"id": "ch_a1b2c3d4-0000-0000-0000-000000000009",
"object": "charge",
"amount": 24900,
"currency": "BRL",
"status": "paid",
"payment_method": "pix",
"settlement": { "end_to_end_id": "E-END-TO-END-99" }
}
}
}See Webhooks for signature verification, retry semantics, and registration.
#Code samples
#Node.js (fetch)
const res = await fetch('https://rottapay.com/v1/charges', {
method: 'POST',
headers: {
'apikey': process.env.ROTTAPAY_API_KEY,
'Idempotency-Key': order.id,
'Content-Type': 'application/json',
},
body: JSON.stringify({
amount: 24900,
currency: 'BRL',
payment_method: 'pix',
customer: { name: order.customer.name, document: order.customer.document },
metadata: { order_id: order.id },
}),
});
if (!res.ok) throw new Error(`Rotta Pay ${res.status}: ${await res.text()}`);
const charge = await res.json();
// charge.id (ch_…) → store on the order; charge.pix.br_code / qr_code_url → render immediately#PHP
$ch = curl_init('https://rottapay.com/v1/charges');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'apikey: ' . getenv('ROTTAPAY_API_KEY'),
'Idempotency-Key: ' . $order->id,
'Content-Type: application/json',
],
CURLOPT_POSTFIELDS => json_encode([
'amount' => 24900,
'currency' => 'BRL',
'payment_method' => 'pix',
'customer' => ['name' => $order->customerName],
'metadata' => ['order_id' => $order->id],
]),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 201) throw new Exception("Rotta Pay $status: $body");
$charge = json_decode($body, true);#Python (requests)
import os, requests
r = requests.post(
"https://rottapay.com/v1/charges",
headers={
"apikey": os.environ['ROTTAPAY_API_KEY'],
"Idempotency-Key": order.id,
},
json={
"amount": 24900,
"currency": "BRL",
"payment_method": "pix",
"customer": {"name": order.customer.name},
"metadata": {"order_id": order.id},
},
timeout=10,
)
r.raise_for_status()
charge = r.json()#Edge cases & FAQ
Q: pix.br_code / QR fields are missing from the response.
A: Pix-first creation returns them synchronously. If they're absent, status will usually already show failed — check the request for a validation error first (GET /v1/charges/{id} to re-read the stored charge).
Q: How long is the QR valid?
A: See pix.expires_at on the charge — it is set per charge. After expiry, status becomes expired and a new charge must be created.
Q: Can I refund a PIX charge?
A: Yes — POST /v1/payments/{id}/refund, full or partial (strip the ch_ prefix to get the id it expects). See API Reference. A /v1/charges/{id}/refund shortcut isn't available yet.
Q: Will I be charged fees if the customer never pays?
A: No. Fees apply only on paid charges, and are never itemized in the public charge response (see API conventions §3).
Q: My customer paid the wrong amount.
A: PIX is exact-match. Underpaid charges stay pending and the bank refunds the payer. Overpaid is rare but treated the same way.