PIX Cash-In -- Generate QR Code
Generates a PIX charge with a QR Code for receiving payments into the account associated with your API Key.
Endpoint
POST /api/external/pix/cash-inHeaders
| Header | Type | Required | Description |
|---|---|---|---|
Authorization | String | Yes | ApiKey {client_id}:{client_secret} |
Content-Type | String | Yes | application/json |
hmac | String | Yes | HMAC-SHA512 signature of the body (learn more) |
Idempotency-Key | String | No | Unique key to prevent duplicate processing (max 256 chars) |
X-Key-Case | String | No | Set to camelCase to receive response fields in camelCase (default is snake_case) |
Request Body
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
amount | Integer | Yes | Amount in centavos (R$ 30.00 = 3000) | 3000 |
description | String | No | Charge description. If omitted, the default is "Cobranca PIX". | "Pedido #1234" |
external_id | String | No | Your system identifier for tracking. Max 128 chars. Only a-zA-Z0-9._:- (out-of-pattern values are silently discarded and the field returns as null -- see warning below). Returned in responses and webhooks. | "order-9876" |
pix_key | String | No | Specific PIX key to generate the QR Code. If omitted, uses the most recent active key of the account (ordered by inserted_at DESC). | "12345678901" |
city | String | No | Recipient city in the QR Code. Default: SAO PAULO. Automatically truncated to 15 characters. | "RIO DE JANEIRO" |
Always send amount as integer (in centavos)
The amount field MUST be an integer in centavos. NEVER send float/decimal values:
- ✅
"amount": 3000→ R$ 30.00 - ❌
"amount": 30.00→ interpreted as30centavos = R$ 0.30 (charge 100× smaller than intended) - ❌
"amount": 30→ R$ 0.30 (also incorrect)
In JavaScript, always convert with Math.round before sending:
const valueInBRL = 30.0;
const amount = Math.round(valueInBRL * 100); // 3000In Python: amount = round(value_in_brl * 100). In Go: amount := int(math.Round(valueInBRL * 100)).
Monetary values (request vs response)
Request values are in centavos (R$ 1.00 = 100). Response values are in base units (R$ 1.00 = 10000). To convert the response to BRL, divide by 10,000. Never use floating point in either direction.
Invalid external_id = silent discard
If external_id contains characters outside a-zA-Z0-9._:-, exceeds 128 bytes, or is only whitespace, the server silently discards the value and the response returns with "external_id": null (no error is returned). Always check the external_id present in the response before using it for reconciliation -- if it came back null, your identifier was rejected by validation.
Example
BODY='{"amount":3000,"description":"Pedido #1234","external_id":"order-9876"}'
HMAC=$(echo -n "$BODY" | openssl dgst -sha512 -hmac "$CLIENT_SECRET" | awk '{print $2}')
curl -X POST https://api.owem.com.br/api/external/pix/cash-in \
-H "Authorization: ApiKey $CLIENT_ID:$CLIENT_SECRET" \
-H "Content-Type: application/json" \
-H "hmac: $HMAC" \
-d "$BODY"Success Response (200)
{
"worked": true,
"transaction_id": "7popu57v6us7p6pcicgq12345",
"qr_code": "00020126580014br.gov.bcb.pix...",
"qr_code_image": "data:image/png;base64,iVBORw0KGgo...",
"external_id": "order-9876",
"amount": 300000,
"status": "active",
"type": "dynamic",
"location_url": "https://qrcode.owem.com.br/pix/v2/a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
"expires_at": "2026-03-07T16:30:00Z"
}| Field | Type | Description |
|---|---|---|
worked | Boolean | true indicates operation success |
transaction_id | String | null | Unique charge identifier (QR Code tx_id, alphanumeric up to 35 chars per BACEN spec). null only in rare internal persistence failures |
qr_code | String | null | EMV copy-and-paste code for payment. null (literal JSON value, not the string "null") only in rare internal persistence failures |
qr_code_image | String | QR Code image encoded in base64 (PNG, prefix data:image/png;base64,). Empty string ("") in rare internal failures |
external_id | String | null | Your identifier, returned as sent. null if not provided, invalid (see warning above), or discarded by sanitization |
amount | Integer | Charge amount in base units (÷ 10,000 for BRL). 300000 = R$ 30.00 |
status | String | Initial status is always active (QR Code active for payment). Subsequent evolution (paid/expired/cancelled) is visible in queries and webhooks (see QR lifecycle) |
type | String | null | QR Code type: dynamic (default since April 2026) or static. Dynamic requires the payer's bank to query location_url and validate the JWS |
location_url | String | null | URL to query the BACEN dynamic payload (e.g., https://qrcode.owem.com.br/pix/v2/{uuid}). It is not the JWS itself -- the payer's bank performs a GET on this endpoint to receive the signed ISO 20022 payload (JWS PS256). Present only on dynamic QR; null on static |
expires_at | String | null | QR Code expiration date/time in ISO 8601 UTC with Z suffix (e.g., 2026-03-07T16:30:00Z). null if QR has no expiration |
Dynamic QR is the default
All production accounts use dynamic QR by default since April 2026. Dynamic QR includes a URL (location_url) that the payer's bank queries to obtain the signed JWS payload -- maximum compatibility with BACEN-strict banks.
Error Response (400)
{
"worked": false,
"detail": "O campo amount é obrigatório"
}Error Response (401)
{
"error": {
"status": 401,
"message": "Missing API key credentials. Use Authorization: ApiKey <client_id>:<client_secret>"
}
}Error Response (422)
{
"worked": false,
"detail": "Invalid HMAC signature"
}Recommended Flow
- Generate the charge with this endpoint
- Display the QR Code (
qr_code_image) or the copy-and-paste code (qr_code) to the payer - Receive confirmation via Webhook when payment is made
- Or query the status: by ID, by E2E, by Tag
QR Code validity
The generated QR Code is valid for 1 hour by default. Validity is configurable per account (qrcode_expiration_seconds field); always check the expires_at returned in the response for the exact date/time.
After expiration, the status changes to expired automatically (via internal worker, runs every 5 minutes). For charges manually cancelled by the merchant, the status is cancelled. See the full status list in Query Cash-In by ID.
QR Code Lifecycle
The QR Code transitions through these states after creation:
| State | Origin | Description |
|---|---|---|
active | Initial | QR generated successfully, ready to be scanned/paid |
paid | Payment received | BACEN payment confirmed and linked to the QR (via internal worker after ACCC) |
expired | QrExpirationChecker worker | TTL reached expires_at. Worker runs every 5 min marking expired active QRs and dispatching the pix.charge.expired webhook |
cancelled | Manual or bulk | Cancelled by the merchant (via portal) or in administrative operations (e.g., static→dynamic migration in April 2026 cancelled active static QRs with value) |
used | Legacy | Transient intermediate state of the old pipeline. New clients should treat it as equivalent to paid |
In queries, paid may appear as settled
The endpoints GET /transactions/:id and equivalents return the status field from the perspective of the transaction (not the QR). A paid QR appears as status: "settled" in the query, with type: "pix" (or type: "pix_qrcode" in a short window post-settlement while the final row in transactions is still being persisted). See Query Cash-In by ID for the full table.