PIX Payments
PIX is Brazil's instant-payment rail. Funds settle in seconds, 24/7. Demo 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 ────────▶ Demo 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: dm_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 | Payer-CPF lock (opt-in). The CPF/CNPJ (11 or 14 digits) that MUST pay this charge. A payment from any other document is automatically returned — 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://demo.zentry.cloud/v1/charges \
-H "apikey: $DEMO_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.demo.zentry.cloud/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 | Demo 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 Demo 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.
curl -X POST https://demo.zentry.cloud/v1/charges \
-H "apikey: $DEMO_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, Demo 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. Demo 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://demo.zentry.cloud/v1/charges/ch_a1b2c3d4-0000-0000-0000-000000000009 \
-H "apikey: $DEMO_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://demo.zentry.cloud/v1/charges', {
method: 'POST',
headers: {
'apikey': process.env.DEMO_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(`Demo 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://demo.zentry.cloud/v1/charges');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'apikey: ' . getenv('DEMO_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("Demo Pay $status: $body");
$charge = json_decode($body, true);#Python (requests)
import os, requests
r = requests.post(
"https://demo.zentry.cloud/v1/charges",
headers={
"apikey": os.environ['DEMO_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.