Skip to main content

Static QR Code Guide

This guide explains how to generate static PIX QR Codes through the API, and why that is the recommended way to receive them.

Overview

A static QR Code is a reusable code: the same BR Code accepts multiple payments, from different payers, at different times. The amount can be fixed or open (typed by the payer in their banking app). It is the right format for:

  • Point of sale — a sign or sticker on the counter, a kiosk, a restaurant table
  • Informal recurring charges — monthly fees, rent, contributions
  • Donations and tips — open amount, the payer decides how much
  • A fixed payment link — a "pay here" page that does not change per order

If you need a single charge, with a defined amount, an expiry and one-to-one reconciliation against an order, use the Dynamic QR Code.

Generate the QR through the API, do not build the BR Code yourself

It is technically possible to assemble a static EMV/BR Code string yourself, following the PIX scheme specification, using only your key. The payment lands in the account and the credit is announced — but you are left without the thread tying the payment to the charge, without lookup, without cancellation and without policies. The next section spells out exactly what is lost.

Why generate it through the API

A static QR created through the API is born with an identifier of yours, validated here and registered at the settlement bank. That identifier is the thread stitching the payment end to end: it comes back in the webhook, in the statement and in the lookup.

A BR Code assembled elsewhere has no such thread registered anywhere. The payment lands in the account and the webhooks go out, but the identifier they carry is whatever you embedded in the code — if you embedded any — and, apart from the money, nothing about that QR exists on our side.

QR created through the APIBR Code you assembled
qrcode.paid webhookDelivered, with your identifierDelivered, with the txid you embedded — or with the field empty
pix.in.completed webhookDelivered, with your identifierSame
StatementThe entry carries identifierOnly carries an identifier if you embedded a txid
QR lookupGET .../pix/qr-code/lookup?identifier= worksThere is no QR to look up
CancellationDELETE takes the code out of circulationThere is nothing to cancel through the API
PoliciesThe qrCode section rules are evaluated at creationNo rule is evaluated
Identifier collisionidentifier validated and registeredThe txid is free-form and can collide with one of your charges

The identifier becomes a gamble

The settlement bank forwards the txid embedded in the BR Code as the payment's identifier. If you assembled the code with a txid of your own, it shows up in the webhooks and in the statement, and the thread does work. Except that value never went through our validation (38 characters, [A-Za-z0-9._-] charset, reserved fee- prefix) and matches no QR in here. If it happens to coincide with the identifier of an active dynamic QR on the same account, the payment is attributed to that charge, which then shows up as paid.

The most common case, though, is a static BR Code going out with *** in the txid field — the placeholder the specification reserves for "no identifier". The payment then arrives with no identifier at all. Both webhooks still go out, with an empty identifier, and the only way to reconcile is by approximation: amount, time and payer.

What does not exist on our side

Since the QR was never created here, there is no record of it. Lookup by identifier finds nothing and DELETE has nothing to cancel — to take the code out of circulation, you are left with collecting the printed sign. The qrCode policy section rules are not evaluated either, because there was no creation to evaluate. And nobody checks whether the embedded key belongs to the account or whether the EMV is well formed: a malformed code only fails in the payer's banking app, and you find out from the customer complaining.

There is no endpoint to register an external BR Code

The API cannot "adopt" a code generated elsewhere. POST /v1/accounts/{accountId}/pix/out/qr-code/decode decodes a BR Code, but it exists to pay someone else's QR, not to register a receiving QR of yours. If the QR is already printed and circulating, assembled externally, the way out is to generate a new one through the API and replace it.

Static or dynamic?

StaticDynamic
RoutePOST .../pix/qr-code/staticPOST .../pix/qr-code/dynamic
AmountOptional (omit it for an open QR)Required, greater than zero
ExpiryNever expiresexpirationDate (settlement bank default: 1 day)
PaymentsMany, on the same codeOne
Restrict the payerNoallowedPayerTaxNumber
Expiry webhookNot applicableqrcode.expired
Typical useCounter, donation, monthly feeCheckout, invoice, order

Step 1: Create the Static QR Code

Request

curl -X POST "https://tenant.api.corpx.com/v1/accounts/{accountId}/pix/qr-code/static" \
-H "Authorization: Bearer {token}" \
-H "X-Tenant-Id: {tenant_id}" \
-H "Content-Type: application/json" \
-d '{
"pixKey": "your-pix-key-here",
"value": 49.90,
"identifier": "store-downtown-register-01"
}'

Body Parameters

FieldTypeRequiredDescription
pixKeystringYesPIX key of the receiving account (must be registered)
valuenumberNoFixed amount in BRL. Omitted or 0 creates an open QR, where the payer types the amount
identifierstringRecommendedYour identifier for the QR (max 38 characters, charset [A-Za-z0-9._-], cannot start with fee-). If omitted, the API generates a UUID without hyphens
messagestringNoMessage up to 140 characters. See the warning below
message does not reach the static BR Code

The field is accepted and validated, but today it is not forwarded to the settlement bank when creating a static QR — it does not show up for the payer, nor does it come back filled in the response's message/description. If you need text visible on the charge, use the dynamic QR, which does forward the message.

Pick an identifier that means something

The identifier is your reconciliation key, and it comes back on every payment made to that QR. On a static QR it identifies the collection point, not the sale: store-downtown-register-01, table-14, website-donation. That way, when three payments arrive, you know which sign each one came from.

Success Response (201 Created)

{
"txid": "store-downtown-register-01",
"emv": "00020126580014br.gov.bcb.pix0136d2e1c0a4-8f5b-4c7e-9a1d-6b3f2e8c7a5052040000530398654049.905802BR5912YOUR COMPANY6004Lins62070503***6304A1B2",
"type": "static",
"status": "ACTIVE",
"value": 49.90,
"identifier": "store-downtown-register-01",
"accountId": "{accountId}",
"pixKey": "8026ff12-cb4a-4d19-9638-08bb15d450e2",
"allowChange": false,
"createdAt": "2026-02-10T12:00:00Z"
}
No data envelope

The response is a flat (top-level) object. There is no data envelope and no statusCode/title/message fields. The copy-and-paste code is in emv and the key in pixKey.

FieldDescription
emvPIX Copy and Paste code (EMV string). Render it as a QR
identifierThe identifier you sent (or the one generated by the API)
txidEchoes identifier — on a static QR the two hold the same value
typeAlways static on this endpoint
statusACTIVE at creation
valueThe fixed amount, or 0 when the QR is open
allowChangetrue when the payer may change the amount

Note there is no expiresAt: a static QR does not expire. It is valid until you cancel it.

Re-submission is not deduplicated

The API does not deduplicate creation by identifier — there is no Idempotency-Key handling on this endpoint. Before recreating a QR that may already exist, check it with the lookup (Step 4). Recreating with an identifier already in use depends on the settlement bank's answer and may fail with a conflict.

Step 2: Display the QR Code

Render the image from the emv string with any QR code library (qrcode.js, python-qrcode, etc.) and also offer the copy-and-paste field:

<input type="text" value="00020126580014br.gov.bcb.pix..." readonly />
<button onclick="navigator.clipboard.writeText(this.previousElementSibling.value)">
Copy
</button>

Since a static QR never expires, the image can be printed, glued to a sign or published on a website — it stays valid until the QR is cancelled.

Step 3: Receive Payments by Webhook

Every payment made to that QR fires two events, and both carry your identifier:

  • qrcode.paid — "the QR I created was paid" (charge reconciliation)
  • pix.in.completed — "my account was credited" (ledger, balance, statement)

Subscribe to both, or only to the one that fits your flow; they describe the same money from different angles.

qrcode.paid

{
"id": "qrcode-paid-E303062942026021014300000005ABCD",
"type": "qrcode.paid",
"occurredAt": "2026-02-10T14:30:12.000000000Z",
"schemaVersion": "1.0",
"environment": "production",
"tenantId": "tenant-yourcompany",
"accountId": "{accountId}",
"data": {
"qrcodeId": "store-downtown-register-01",
"identifier": "store-downtown-register-01",
"accountId": "{accountId}",
"tenantId": "tenant-yourcompany",
"type": "static",
"amount": 49.90,
"endToEnd": "E303062942026021014300000005ABCD",
"transactionId": "b7c1e0f2-3a4d-4e5f-8a9b-0c1d2e3f4a5b",
"status": "SUCCESS",
"receivedAt": "2026-02-10T14:30:12.000000000Z",
"payer": {
"name": "John Smith",
"document": "12345678901",
"bankIspb": "30306294",
"branch": "0020",
"account": "004912314"
},
"payee": {
"name": "YOUR COMPANY LTDA",
"document": "12345678000190"
}
}
}

On a static QR, qrcodeId and identifier hold the same value: the identifier you chose at creation. What tells one payment from another is endToEnd.

Each payment is an independent event

The same QR paid five times produces five qrcode.paid, each with its own endToEnd. Do not treat the first event as "charge settled" and stop listening — the code stays active. Use endToEnd as the deduplication key for your own bookkeeping.

There is no qrcode.expired or qrcode.cancelled for a static QR: it does not expire, and cancelling is a synchronous action of yours whose confirmation is the DELETE HTTP response.

Step 4: Reconcile

The QR's identifier shows up in both places where you look for money:

In the statement, every entry carries the identifier field:

curl -X GET "https://tenant.api.corpx.com/v1/accounts/{accountId}/statement?from=2026-02-10&to=2026-02-10" \
-H "Authorization: Bearer {token}" \
-H "X-Tenant-Id: {tenant_id}"

In the payment lookup, you search straight by the identifier:

curl -X GET "https://tenant.api.corpx.com/v1/accounts/{accountId}/pix/payments/lookup?identifier=store-downtown-register-01" \
-H "Authorization: Bearer {token}" \
-H "X-Tenant-Id: {tenant_id}"

Checking the QR state

curl -X GET "https://tenant.api.corpx.com/v1/accounts/{accountId}/pix/qr-code/lookup?identifier=store-downtown-register-01" \
-H "Authorization: Bearer {token}" \
-H "X-Tenant-Id: {tenant_id}"

The response is the same flat object from creation, plus the payment fields when there is one: paidAmount, endToEndId, paidAt, payer.

The lookup shows one payment, not the history

On a reusable QR, the lookup reflects the latest payment known to the settlement bank, and status turns to PAID after the first one. It does not list previous payments. For the full history, use the qrcode.paid webhooks or the statement filtered by period.

Possible statuses: ACTIVE, AWAITING-PAYMENT, PAID, CANCELLED, EXPIRED, UNKNOWN — always in UPPERCASE.

Step 5: Cancel the QR Code

A static QR is valid until cancelled. Cancel it when the sign goes out of circulation, the register is decommissioned or the campaign ends:

curl -X DELETE "https://tenant.api.corpx.com/v1/accounts/{accountId}/pix/qr-code?identifier=store-downtown-register-01" \
-H "Authorization: Bearer {token}" \
-H "X-Tenant-Id: {tenant_id}"

Response (200 OK):

{
"identifier": "store-downtown-register-01",
"status": "CANCELLED"
}

After cancellation the printed BR Code is no longer chargeable — whoever scans it gets an error in their banking app.

Policies

Creating a static QR goes through the qrCode section of the tenant's or the account's policy. A violation returns 422 with errorCode: policy_denied and the list of violations:

RuleruleEffect
Static creationqrCode.canCreateStaticfalse refuses creation
Minimum amountqrCode.minAmountRefuses a QR below the minimum
Maximum amountqrCode.maxAmountRefuses a QR above the maximum

The amount rules do not apply to an open QR: without value at creation there is no amount to compare, and both are skipped. If your amount control must always hold, do not use an open QR. Details in Policies and Rules.

Required Scope

OperationScope
Create a static QRqrcode.manage
Look up and cancelqrcode.manage or read

A credential with only qrcode.manage creates and follows QR codes without seeing the account's balance and statement. See the Authentication Guide.

Best Practices

  1. Always generate through the API — a BR Code assembled elsewhere costs you reconciliation, lookup and cancellation
  2. Name the collection point — a static QR's identifier identifies the sign/register/campaign, not the sale
  3. Deduplicate by endToEnd — that is what tells one payment from another on the same QR
  4. Do not rely on the lookup alone — it shows the latest payment; the history lives in the webhooks and the statement
  5. Cancel what left circulation — a static QR does not expire on its own
  6. Prefer a fixed amount when a policy limit matters — an open QR skips the minAmount/maxAmount rules

Common Errors

Errors come as { "errorCode": "...", "message": "..." }:

errorCodeHTTPCauseSolution
missing_field400pixKey missingSend a key registered on the account
invalid_identifier400identifier over 38 characters, outside the [A-Za-z0-9._-] charset, or starting with fee-Adjust the identifier
invalid_field400message over 140 charactersShorten the message
invalid_payload400Malformed JSONFix the request body
missing_param400Lookup or cancellation without identifier in the queryProvide ?identifier= (alias: txid)
not_found404QR not found on lookupCheck the identifier and the accountId
policy_denied422A qrCode section rule refused the creationCheck violations in the body — Policies and Rules
partner_unavailable503Settlement bank unavailableRetry the call

Example:

{
"errorCode": "missing_field",
"message": "pixKey is required"
}

Full list in Errors.

Next Steps