Skip to content
MOONIPdocs
IntegrationManual
PTEN
Go to dashboard

PIX Payments

PIX is Brazil's instant-payment rail. Funds settle in seconds, 24/7. Moonip 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  ────────▶  Moonip 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
Authorization: Bearer ak_live_…
Idempotency-Key: <unique per charge you intend to create>
Content-Type: application/json

Authorization: Bearer <key> is the canonical way to present the API key. Idempotency-Key is required on POST /v1/charges (400 without it): any non-blank string, scoped per merchant ((merchant, key)), replaying the same key and body returns the original charge.

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–604800 (up to 7 days). A value outside the range is rejected with 400 — never 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://api.moonip.io/v1/charges \
  -H "Authorization: Bearer $MOONIP_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.moonip.io/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 / pix.qr_code_base64The QR image, when the provider returns one: qr_code_url for an https:// URL (drop it into <img src="...">), otherwise qr_code_base64 (a PNG to decode). A provider that returns only br_code yields neither — always render br_code as the fallback.
checkout_urlMoonip'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.
settlementAlways present. settlement.end_to_end_id is usually empty until settle: it appears only after the charge reaches paid and has been settled/reconciled.

The pix object is { br_code?, qr_code_url?, qr_code_base64?, expires_at?, txid? } and is absent when there is no br_code, QR image or expiry to show.

#Statuses

StatusMeaning
pendingCreated; waiting for the payer.
processingPayment seen; not confirmed yet (e.g. held for review).
paidPaid and credited.
failedPayment refused.
expiredexpires_at passed without payment.
cancelledCancelled before payment.
refundedReturned to the payer.
disputedChargeback or dispute.

#TXID e end_to_end_id

ch_… is always the Moonip 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://api.moonip.io/v1/charges \
  -H "Authorization: Bearer $MOONIP_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, Moonip 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 → depends on how the lock is enforced on the provider serving your account:
    • Detective (fail-closed): the charge is NOT credited. Moonip automatically returns the money to whoever paid (a standard Pix devolução, addressed by the original end_to_end_id); the charge sits in processing until that return settles, and only then becomes refunded. You never receive third-party funds.
    • Preventive: the bank-side rule is expected to refuse the other payer before the payment lands. If a third party still manages to pay (the rule did not hold), the funds are credited anyway and the charge stays paid, flagged as a payer mismatch — the money is yours.
  • 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:

  • The capture-and-return guarantee is scoped to the detective (fail-closed) path: the charge sits in processing until the automatic return (devolução) settles and only then becomes refunded, and your merchant balance is never credited for the non-matching payer. On that path the check is settlement-time, not a bank-level block. On the preventive path a payment that slips past the bank-side rule is credited anyway and the charge stays paid (flagged as a payer mismatch). BACEN lets any payer scan the QR in either mode, so the payer's app may still show the charge.
  • 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://api.moonip.io/v1/charges/ch_a1b2c3d4-0000-0000-0000-000000000009 \
  -H "Authorization: Bearer $MOONIP_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. List charges

Endpoint GET /v1/charges — cursor pagination (never offset: there is no page/offset).

QueryTypeDefaultNotes
limitint251–100.
starting_afterstring—Cursor: the ch_… id (or raw id) of the last charge you saw.
statusstring—One of the statuses above.
created_after / created_beforeISO-8601 datetime—Strict UTC (…Z) — offsets are rejected.
customer_idstring—Filters by the payer document.
bash
curl "https://api.moonip.io/v1/charges?limit=25&status=paid" \
  -H "Authorization: Bearer $MOONIP_API_KEY"

Response 200 OK — the cursor envelope:

json
{
  "object": "list",
  "data": [ { "id": "ch_a1b2c3d4-0000-0000-0000-000000000009", "object": "charge", "status": "paid" } ],
  "has_more": true,
  "next_cursor": "ch_a1b2c3d4-0000-0000-0000-000000000009"
}

Walk the pages by passing next_cursor as starting_after until has_more is false. Ordering is newest first.

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

With the official SDK (same neutral package for every brand — configure it with the API base URL of this page, https://api.moonip.io):

nodepythonphpdotnet
client.charges.create(input, { idempotencyKey })client.charges.create(input, idempotency_key)$client->charges->create($input, $idempotencyKey)client.Charges.CreateAsync(input, idempotencyKey)
client.charges.retrieve(id)client.charges.retrieve(id)$client->charges->retrieve($id)client.Charges.RetrieveAsync(id)
client.charges.list(query)client.charges.list(**query)$client->charges->list($query)client.Charges.ListAsync(query)

charges.list returns the cursor envelope { object: "list", data, has_more, next_cursor }. The raw HTTP equivalents follow.

#Node.js (fetch)

js
const res = await fetch('https://api.moonip.io/v1/charges', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.MOONIP_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(`Moonip ${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://api.moonip.io/v1/charges');
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST           => true,
  CURLOPT_HTTPHEADER     => [
    'Authorization: Bearer ' . getenv('MOONIP_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("Moonip $status: $body");
$charge = json_decode($body, true);

#Python (requests)

python
import os, requests

r = requests.post(
    "https://api.moonip.io/v1/charges",
    headers={
        "Authorization": "Bearer " + os.environ['MOONIP_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: Refunds are requested from the admin panel today — the API does not expose a merchant-usable refund endpoint (it refuses API-key callers). The same goes for /v1/charges/{id}/refund, which does not exist.

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