Skip to content

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-in

Headers

HeaderTypeRequiredDescription
AuthorizationStringYesApiKey {client_id}:{client_secret}
Content-TypeStringYesapplication/json
hmacStringYesHMAC-SHA512 signature of the body (learn more)
Idempotency-KeyStringNoUnique key to prevent duplicate processing (max 256 chars)
X-Key-CaseStringNoSet to camelCase to receive response fields in camelCase (default is snake_case)

Request Body

FieldTypeRequiredDescriptionExample
amountIntegerYesAmount in centavos (R$ 30.00 = 3000)3000
descriptionStringNoCharge description. If omitted, the default is "Cobranca PIX"."Pedido #1234"
external_idStringNoYour 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_keyStringNoSpecific PIX key to generate the QR Code. If omitted, uses the most recent active key of the account (ordered by inserted_at DESC)."12345678901"
cityStringNoRecipient 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 as 30 centavos = R$ 0.30 (charge 100× smaller than intended)
  • "amount": 30 → R$ 0.30 (also incorrect)

In JavaScript, always convert with Math.round before sending:

js
const valueInBRL = 30.0;
const amount = Math.round(valueInBRL * 100); // 3000

In 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

bash
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)

json
{
  "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"
}
FieldTypeDescription
workedBooleantrue indicates operation success
transaction_idString | nullUnique charge identifier (QR Code tx_id, alphanumeric up to 35 chars per BACEN spec). null only in rare internal persistence failures
qr_codeString | nullEMV copy-and-paste code for payment. null (literal JSON value, not the string "null") only in rare internal persistence failures
qr_code_imageStringQR Code image encoded in base64 (PNG, prefix data:image/png;base64,). Empty string ("") in rare internal failures
external_idString | nullYour identifier, returned as sent. null if not provided, invalid (see warning above), or discarded by sanitization
amountIntegerCharge amount in base units (÷ 10,000 for BRL). 300000 = R$ 30.00
statusStringInitial status is always active (QR Code active for payment). Subsequent evolution (paid/expired/cancelled) is visible in queries and webhooks (see QR lifecycle)
typeString | nullQR Code type: dynamic (default since April 2026) or static. Dynamic requires the payer's bank to query location_url and validate the JWS
location_urlString | nullURL 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_atString | nullQR 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)

json
{
  "worked": false,
  "detail": "O campo amount é obrigatório"
}

Error Response (401)

json
{
  "error": {
    "status": 401,
    "message": "Missing API key credentials. Use Authorization: ApiKey <client_id>:<client_secret>"
  }
}

Error Response (422)

json
{
  "worked": false,
  "detail": "Invalid HMAC signature"
}
  1. Generate the charge with this endpoint
  2. Display the QR Code (qr_code_image) or the copy-and-paste code (qr_code) to the payer
  3. Receive confirmation via Webhook when payment is made
  4. 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:

StateOriginDescription
activeInitialQR generated successfully, ready to be scanned/paid
paidPayment receivedBACEN payment confirmed and linked to the QR (via internal worker after ACCC)
expiredQrExpirationChecker workerTTL reached expires_at. Worker runs every 5 min marking expired active QRs and dispatching the pix.charge.expired webhook
cancelledManual or bulkCancelled by the merchant (via portal) or in administrative operations (e.g., static→dynamic migration in April 2026 cancelled active static QRs with value)
usedLegacyTransient 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.

Owem Pay Instituição de Pagamento — ISPB 37839059