Skip to content
DEMO PAYdocs
PTEN
Go to dashboard

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

text
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

text
apikey: dm_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_idstringnoPayer-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.
metadataobjectnoFree-form key/value, returned verbatim (reserved/underscore-prefixed keys are stripped).

Example

bash
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

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.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"
}
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_urlDemo 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 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:

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.

bash
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 paid and 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 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://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:

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

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)

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

#Next

Configure webhooks for production