Skip to content
ROTTA PAYdocs
PTEN
Go to dashboard

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

text
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

text
apikey: ak_live_...
Idempotency-Key: <unique per charge you intend to create>
Content-Type: application/json

Body

FieldTypeRequiredNotes
amountintegeryesCents. 1000 = R$ 10,00. Must be a positive integer.
currencystringnoDefaults to "BRL" — the only currency Pix charges accept.
payment_methodstringyes"pix" (the only value accepted today; the /v1/pix/charges shortcut fills it in for you).
descriptionstringnoUp to 500 chars. Echoed back inside metadata.description.
expires_inintegernoSeconds 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.namestringnoShown on the bank statement when supported.
customer.documentstringnoCPF or CNPJ.
customer.emailstringnoUsed for receipts.
customer.phonestringno
expected_payer_tax_idstringno⚠️ 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.
metadataobjectnoFree-form key/value, returned verbatim (reserved/underscore-prefixed keys are stripped).

Example

bash
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

json
{
  "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"
}
FieldWhat to do with it
pix.br_codeThe EMV "Pix Copia e Cola" payload. Render in a copy-to-clipboard button.
pix.qr_code_url / qr_code_base64One of the two is always present when Pix is available. Drop the URL into <img src="...">, or decode the base64 PNG.
checkout_urlRotta 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_atWhen this charge expires. Show a countdown; after this, status moves to expired.
pix.txidBACEN 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:

json
{
  "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.document alone, 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_id is created normally, without the lock — any payer can pay and the payment is credited. You can tell from the response: the charge does not include expected_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.

bash
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 paid and 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 up refunded. 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_id requires customer.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 send customer.document — must match it. Otherwise the request is rejected 400.
  • 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}

bash
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:

json
{
  "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:

json
{
  "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)

js
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

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)

python
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.

#Next

→ Configure webhooks for production