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.
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 API | BR Code you assembled | |
|---|---|---|
qrcode.paid webhook | Delivered, with your identifier | Delivered, with the txid you embedded — or with the field empty |
pix.in.completed webhook | Delivered, with your identifier | Same |
| Statement | The entry carries identifier | Only carries an identifier if you embedded a txid |
| QR lookup | GET .../pix/qr-code/lookup?identifier= works | There is no QR to look up |
| Cancellation | DELETE takes the code out of circulation | There is nothing to cancel through the API |
| Policies | The qrCode section rules are evaluated at creation | No rule is evaluated |
| Identifier collision | identifier validated and registered | The 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.
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?
| Static | Dynamic | |
|---|---|---|
| Route | POST .../pix/qr-code/static | POST .../pix/qr-code/dynamic |
| Amount | Optional (omit it for an open QR) | Required, greater than zero |
| Expiry | Never expires | expirationDate (settlement bank default: 1 day) |
| Payments | Many, on the same code | One |
| Restrict the payer | No | allowedPayerTaxNumber |
| Expiry webhook | Not applicable | qrcode.expired |
| Typical use | Counter, donation, monthly fee | Checkout, 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
| Field | Type | Required | Description |
|---|---|---|---|
pixKey | string | Yes | PIX key of the receiving account (must be registered) |
value | number | No | Fixed amount in BRL. Omitted or 0 creates an open QR, where the payer types the amount |
identifier | string | Recommended | Your 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 |
message | string | No | Message up to 140 characters. See the warning below |
message does not reach the static BR CodeThe 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.
identifier that means somethingThe 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"
}
data envelopeThe 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.
| Field | Description |
|---|---|
emv | PIX Copy and Paste code (EMV string). Render it as a QR |
identifier | The identifier you sent (or the one generated by the API) |
txid | Echoes identifier — on a static QR the two hold the same value |
type | Always static on this endpoint |
status | ACTIVE at creation |
value | The fixed amount, or 0 when the QR is open |
allowChange | true 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.
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.
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.
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:
| Rule | rule | Effect |
|---|---|---|
| Static creation | qrCode.canCreateStatic | false refuses creation |
| Minimum amount | qrCode.minAmount | Refuses a QR below the minimum |
| Maximum amount | qrCode.maxAmount | Refuses 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
| Operation | Scope |
|---|---|
| Create a static QR | qrcode.manage |
| Look up and cancel | qrcode.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
- Always generate through the API — a BR Code assembled elsewhere costs you reconciliation, lookup and cancellation
- Name the collection point — a static QR's
identifieridentifies the sign/register/campaign, not the sale - Deduplicate by
endToEnd— that is what tells one payment from another on the same QR - Do not rely on the lookup alone — it shows the latest payment; the history lives in the webhooks and the statement
- Cancel what left circulation — a static QR does not expire on its own
- Prefer a fixed amount when a policy limit matters — an open QR skips the
minAmount/maxAmountrules
Common Errors
Errors come as { "errorCode": "...", "message": "..." }:
errorCode | HTTP | Cause | Solution |
|---|---|---|---|
missing_field | 400 | pixKey missing | Send a key registered on the account |
invalid_identifier | 400 | identifier over 38 characters, outside the [A-Za-z0-9._-] charset, or starting with fee- | Adjust the identifier |
invalid_field | 400 | message over 140 characters | Shorten the message |
invalid_payload | 400 | Malformed JSON | Fix the request body |
missing_param | 400 | Lookup or cancellation without identifier in the query | Provide ?identifier= (alias: txid) |
not_found | 404 | QR not found on lookup | Check the identifier and the accountId |
policy_denied | 422 | A qrCode section rule refused the creation | Check violations in the body — Policies and Rules |
partner_unavailable | 503 | Settlement bank unavailable | Retry the call |
Example:
{
"errorCode": "missing_field",
"message": "pixKey is required"
}
Full list in Errors.
Next Steps
- Dynamic QR Code Guide - Single charges with amount and expiry
- Refund Guide - Return a received payment
- Webhooks - Set up notifications