QR Code Creation
Generate dynamic PIX QR Codes to receive payments in BRL, ARS, or USDt.
This endpoint generates a dynamic PIX QR Code that can be scanned from any Brazilian banking app. Every request sends the amount and identifies the charge's End User. kamiPay handles the conversion and generates the QR automatically.
By default, amount is interpreted as BRL. If you want to charge in ARS or USDt instead, pass currency accordingly.
Wallet configuration: Your destination wallet is set up during onboarding. Each charge settles to the wallet of its checkout, never to one named in the request. If you operate with multiple stores or checkouts, you can use store_id and checkout_id to select a specific wallet. For more details, see Multi-Wallet Routing.
Dynamic QR charges are eligible for blockchain batched settlement: when the checkout opts in, charges accumulate and are paid out in one consolidated USDT transfer per window instead of one transfer per charge.
Request Body
| Name | Type | Description |
|---|---|---|
| amount | float | Required. Amount to charge, expressed in the currency specified by currency. Once converted to BRL, it must be between R$ 5 and R$ 100,000, or the request is rejected with 400. For ARS and USDt the check runs after the conversion, so a fixed ARS or USDt amount can fall out of range as the rate moves. In BRL, an amount with more than 2 decimals is rounded to 2, not rejected (10.129 → 10.13): round it before sending, so the amount you store matches amount_brl. |
| receiver_name | string | Required. Full name of the End User. |
| receiver_id_type | string | Required. Type of the End User's identity document, one of the Document Types. |
| receiver_document | string | Required. Number of the End User's identity document. |
| receiver_country | string | Required, except with a CPF or CNPJ. Country that issued the End User's identity document, as an ISO 3166-1 alpha-2 or alpha-3 code ("AR" or "ARG"). |
| currency | string | Optional. Accepted values: "ARS", "BRL", "USDt". Case-insensitive ("ars", "brl", "usdt" are all valid). If not sent, defaults to "BRL". A USDt charge is approximate: the payer pays in BRL, which has cents, so your amount is converted to BRL, rounded to the cent, and the USDt recomputed from it. The USDt the charge settles is the response's amount_usdt (amount: 2 → amount_brl: 10.69 → amount_usdt: 1.999545). For ARS, see the note on the 200 response. |
| expire | integer | Optional. QR Code expiration time in seconds, from 300 (5 min) to 86400 (24h). If not sent, or sent as null, defaults to 300. Outside that range, the request is rejected with 422. It doesn't apply to a sale on a printed checkout, which always expires in 5 minutes (see Printed Dynamic QRs), but a value out of range is rejected there too. |
| external_id | string | Optional. Your internal ID for this transaction. Recommended for deduplication and tracking. No two pay-ins created with the same credentials can share it, whatever their type (dynamic, printed or static QR) and however old: a repeat is rejected with 409, which carries the original charge (see the 409 response). Omitted, "" or blank, it counts as none, and every such request creates a new QR. |
| external_data | string | Optional. Free-text description for this charge (e.g. "Hotel booking #234"). |
| store_id | integer | Optional. ID of your store. Default: 1. Only needed if you operate with multiple stores. |
| checkout_id | integer | Optional. ID of your checkout. Default: 1. Only needed if you operate with multiple checkouts. |
| address | string | Optional. A check, not a choice of wallet: the charge always settles to its checkout's wallet, and an address you send must match that wallet, or the request is rejected with 400. To change where a checkout's charges settle, change its wallet with Update Checkout. |
| transfer_description | string | Optional. Free-text note about the charge, kept with it for compliance and reporting: not shown to the payer and not returned by the API. Longer values are truncated to 140 characters. |
End User
Every QR Code identifies its End User: the person or company on whose behalf the charge is created (your customer, or your own company). The End User is not the payer who scans the QR.
Send the End User in the four receiver_* fields, following these rules:
- All four fields are required, with one exception:
receiver_countrycan be omitted with aCPForCNPJ(see below). A blank or whitespace-only value counts as missing, and letter case is ignored in all four. receiver_countryis the country that issued the document, not the End User's nationality or country of residence: a Venezuelan citizen identified with an Argentine DNI is sent asARGandDNI.- With a
CPForCNPJ,receiver_countryis optional, and the document is taken as issued by Brazil. If you send a country with one, it must beBRorBRA. receiver_id_typeis a document type that the country issues, as listed in Document Types.receiver_documentis a valid document of that type. Spaces, dots, dashes and slashes are ignored, so20-12345678-6and20123456786are the same CUIT. Where the table below lists a check digit, it is verified.receiver_namehas at least 2 characters and at least one letter, not counting leading or trailing spaces.
Document Types
| Issuing country | receiver_id_type | Document format |
|---|---|---|
Argentina (AR / ARG) | DNI | 7 or 8 digits. |
Argentina (AR / ARG) | CUIT, CUIL | 11 digits, the last one a check digit. |
Chile (CL / CHL) | RUT | 7 or 8 digits followed by a check digit (0 to 9, or K). |
Uruguay (UY / URY) | CI | 7 or 8 digits, the last one a check digit. |
Uruguay (UY / URY) | RUT | 12 digits. |
Bolivia (BO / BOL) | CI | 4 to 10 digits, optionally followed by a 2-character complement, the department of issue (such as LP), or both. |
Bolivia (BO / BOL) | NIT | 7 to 13 digits, not starting with 0. |
Mexico (MX / MEX) | CURP | 18 characters in the official format, with the birth date as YYMMDD. The last one is a check digit. |
Mexico (MX / MEX) | INE | The 18-character elector key printed on the credential (6 letters, 8 digits, H or M, 3 digits), or the 9 to 13 digits of its CIC or OCR. |
Mexico (MX / MEX) | RFC | 12 characters for a company, 13 for an individual, in the official format, with the date of incorporation or birth as YYMMDD. |
Brazil (BR / BRA) | CPF | 11 digits, the last two check digits. |
Brazil (BR / BRA) | CNPJ | 14 characters, the last two check digits. The first 12 can include letters. |
Venezuela (VE / VEN) | CI | 5 to 9 digits, optionally preceded by V or E. |
Colombia (CO / COL) | CI | 5 to 10 digits. |
Paraguay (PY / PRY) | CI | 5 to 10 digits. |
Ecuador (EC / ECU) | CI | 5 to 10 digits. |
Peru (PE / PER) | DNI | 8 digits, without the check character printed next to them on the card. |
Peru (PE / PER) | RUC | 11 digits, the last one a check digit. |
Peru (PE / PER) | CE, CPP | 9 digits. |
| Any country | PASSPORT | 6 to 20 letters (A to Z) or digits. |
| Any country | OTHER | Any document not listed above, with at least one character. Send only OTHER as the type and the document number as it appears on the document. |
An End User whose document was issued by a country not listed above can only be identified with a PASSPORT, or with OTHER when it has no passport.
Printed Dynamic QR Integration
When a checkout_id and store_id are provided and that checkout has a Printed Dynamic QR configured as its default, this endpoint creates a sale, a charge for one customer, on that checkout instead of a standalone QR:
- The sale is attached to the checkout's printed QR: a customer scanning it sees the sale's amount.
- The response has
qr_type: "printed_dynamic", akamipay_idstarting withcqr_, and the printed QR's EMV inprinted_dynamic_qr_data.pqr_emv. It also carries anemv, but it belongs to the checkout's location, not to the sale: on some gateways it is the printed QR itself, on others a separate EMV whose text keeps showing this sale's amount even after a newer sale replaces it.
This allows POS setups where a permanent QR is already on display, while still letting you show the returned emv on a screen.
A printed checkout holds one pending sale at a time. Creating a new sale replaces the previous unpaid one, and every QR of the checkout then charges the newest sale. The payment is credited to the newest sale, and the displaced one never gets a webhook. Read One pending sale per checkout before you create a new sale while one is pending.
Example Request
const url = `${baseURL}/v2/charge/create_dynamic_pix`;
const body = {
amount: 150,
currency: "BRL", // "ARS", "BRL", or "USDt"
external_id: "order-001", // recommended
// End User: the person or company on whose behalf the charge is created
receiver_name: "Juan Perez",
receiver_id_type: "DNI",
receiver_document: "12345678",
receiver_country: "ARG", // country that issued the document
};
const response = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${access_token}`,
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});import requests
url = f"{base_url}/v2/charge/create_dynamic_pix"
body = {
"amount": 150,
"currency": "BRL", # "ARS", "BRL", or "USDt"
"external_id": "order-001", # recommended
# End User: the person or company on whose behalf the charge is created
"receiver_name": "Juan Perez",
"receiver_id_type": "DNI",
"receiver_document": "12345678",
"receiver_country": "ARG", # country that issued the document
}
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
response = requests.post(url, json=body, headers=headers)package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
func main() {
url := fmt.Sprintf("%s/v2/charge/create_dynamic_pix", baseURL)
requestBody, _ := json.Marshal(map[string]interface{}{
"amount": 150,
"currency": "BRL", // "ARS", "BRL", or "USDt"
"external_id": "order-001", // recommended
// End User: the person or company on whose behalf the charge is created
"receiver_name": "Juan Perez",
"receiver_id_type": "DNI",
"receiver_document": "12345678",
"receiver_country": "ARG", // country that issued the document
})
req, _ := http.NewRequest("POST", url, bytes.NewBuffer(requestBody))
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", access_token))
req.Header.Add("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
fmt.Println("Error making request:", err)
return
}
defer resp.Body.Close()
}Response
QR Code Created Successfully
{
"emv": "00020101021226810014br.gov.bcb.pix25...",
"operation_id": "ptxr_01knhd0c2ten6t5d65pyrrsmv2",
"kamipay_id": "dqr_01knhczzereejbh6fan5c9k5bk",
"amount_brl": 150.0,
"amount_usdt": 28.322199,
"rate": 5.296199,
"date": 1775479302,
"expiration": 1775479602,
"external_id": "order-001",
"qr_type": "dynamic"
}On a printed checkout, the request creates a sale instead (see Printed Dynamic QR Integration). For example, a sale for "order-002":
{
"emv": "00020101021226810014br.gov.bcb.pix25...",
"operation_id": "ptxr_01knhdmgf8fn48qpd6dpfzy6x7",
"kamipay_id": "cqr_01knhdm3veep4aq0nj1wzca92w",
"amount_brl": 150.0,
"amount_usdt": 28.322199,
"rate": 5.296199,
"date": 1775479962,
"expiration": 1775480262,
"external_id": "order-002",
"qr_type": "printed_dynamic",
"printed_dynamic_qr_data": {
"pqr_emv": "00020126840014br.gov.bcb.pix25...",
"printed_dynamic_qr_id": "pqr_01km0mbfxdf4g87eb8yytas95k"
}
}When currency is "ARS", the response also includes amount_ars: the ARS amount you sent, exactly as you sent it. amount_brl is what the payer pays, and amount_usdt what the charge settles. rate is still the BRL/USDt rate; the ARS → BRL rate isn't returned, and is amount_ars / amount_brl. amount_ars is not present for BRL or USDt requests.
Bad Request
{
"detail": "Converted BRL amount 1.0 is out of range [5, 100000]"
}The detail you can get, each with one example (the amount and the ids in it vary):
Example detail | When |
|---|---|
"amount must be greater than 0" | amount is zero or negative. |
"Converted BRL amount 1.0 is out of range [5, 100000]" | The amount, once converted to BRL, is below R$ 5 or above R$ 100,000. It says "Converted" even if you sent BRL. |
"Incorrect data Address, Store, Checkout: checkout 99 not found in store 1" | The store has no checkout with that checkout_id (if not sent, 1). |
"Incorrect data Address, Store, Checkout: address doesn't match the wallet of checkout 1" | The address you sent isn't the wallet of the checkout. |
"Incorrect data Address, Store, Checkout: user has no access to store 2" | The user your credentials belong to has no access to that store. |
Unauthorized
With an invalid or expired token:
{
"detail": "Could not validate credentials"
}Conflict (Duplicate External ID)
Your credentials already created a pay-in with this external_id. No new charge is created: detail carries the original one, emv included, so if the response to its creation got lost, you can still show its QR.
{
"detail": {
"message": "Duplicate external_id",
"kamipay_id": "dqr_01knhczzereejbh6fan5c9k5bk",
"emv": "00020101021226810014br.gov.bcb.pix25...",
"operation_id": "ptxr_01knhd0c2ten6t5d65pyrrsmv2",
"amount_brl": 150.0,
"amount_usdt": 28.322199,
"rate": 5.296199,
"date": 1775479302,
"expiration": 1775479602,
"external_id": "order-001",
"qr_type": "dynamic"
}
}detailcarries the same fields the original's creation returned, includingamount_ars, the Integrator Fee split andprinted_dynamic_qr_datawhen the original had them. It never includesaddress.- It doesn't say whether the original was paid or expired. Check it with Tx Status, by its
kamipay_idoroperation_id. - A
detailwith onlymessageandkamipay_idmeans the original can't be returned yet: a first request may still be creating the charge, an earlier attempt may have failed before creating its QR, the id may belong to another type of pay-in, or the charge may be an older one. Retry in a few seconds, and if it persists, create the charge with a newexternal_id.
Validation Error
Returned when the request body is invalid: for example, when the End User is missing or invalid, currency isn't one of its accepted values, or expire is out of range. detail has one entry per invalid field; for a missing field, input is the whole request body.
For example, a request with a CUIT whose check digit is wrong and without receiver_name returns:
{
"detail": [
{
"type": "value_error",
"loc": ["body", "receiver_document"],
"msg": "receiver_document is not a valid CUIT",
"input": "20-12345678-9"
},
{
"type": "missing",
"loc": ["body", "receiver_name"],
"msg": "Field required",
"input": {
"amount": 150,
"currency": "BRL",
"external_id": "order-001",
"receiver_id_type": "CUIT",
"receiver_document": "20-12345678-9",
"receiver_country": "ARG"
}
}
]
}The End User errors you can get, each with one example of its msg (the field, document type or country named in it varies):
type | Example msg |
|---|---|
missing | Field required |
value_error | receiver_country must be an ISO 3166-1 alpha-2 or alpha-3 code |
value_error | receiver_country must be BRA for a CPF |
value_error | receiver_id_type must be one of CE, CI, CNPJ, CPF, CPP, CUIL, CUIT, CURP, DNI, INE, NIT, OTHER, PASSPORT, RFC, RUC, RUT |
value_error | receiver_id_type CUIT is not valid for country CHL |
value_error | receiver_document is not a valid CUIT |
value_error | receiver_name must have at least 2 characters and at least one letter |
An invalid currency is reported on the request body as a whole: loc is ["body"], and input is the body exactly as you sent it. Sending the example request with "currency": "EUR" returns:
{
"detail": [
{
"type": "value_error",
"loc": ["body"],
"msg": "Value error, 'currency' must be one of ['ARS', 'USDT', 'BRL'], got 'EUR'",
"input": {
"amount": 150,
"currency": "EUR",
"external_id": "order-001",
"receiver_name": "Juan Perez",
"receiver_id_type": "DNI",
"receiver_document": "12345678",
"receiver_country": "ARG"
},
"ctx": {
"error": {}
}
}
]
}Its msg is quoted exactly as the API returns it, so it lists 'USDT', although currency is documented above as "USDt": both are accepted, since currency is case-insensitive.
An expire out of range returns an entry with loc ["body", "expire"], the expire you sent as input, and the limit it crossed in ctx:
type | msg | ctx |
|---|---|---|
greater_than_equal | Input should be greater than or equal to 300 | {"ge": 300} |
less_than_equal | Input should be less than or equal to 86400 | {"le": 86400} |
To handle these errors in code, rely on loc and type, not on msg. Some fields can only be checked once the fields they depend on are valid, so fixing one can reveal an error in another.
A request rejected with this error creates no charge, so you can fix it and send it again with the same external_id.
Internal Server Error
{
"detail": "Internal Server Error during Pix Generation"
}Service Unavailable
{
"detail": "service_unavailable"
}- Every response carries an
x-request-idheader. Quote it when you contact support about a request. detail's type depends on the error. A 422'sdetailis a list; the other errors'detailis a string or an object.
Response Fields
| Field | Type | Description |
|---|---|---|
| emv | string | The PIX brcode to render as a QR Code. Pass this string to any QR library. |
| operation_id | string | Banking-level tracking number for this transaction. |
| kamipay_id | string | kamiPay's unique identifier for this transaction: dqr_…, or cqr_… for a sale on a printed checkout. Use this for tracking and support. |
| amount_brl | float | Final amount in Brazilian Reais that the payer will pay. |
| amount_usdt | float | USDt equivalent of the BRL amount (always present). |
| amount_ars | float | The ARS amount sent in the request. Its BRL equivalent is what the payer will be charged. Only present when currency is "ARS". |
| rate | float | BRL/USDt exchange rate used for this transaction. |
| date | integer | Unix timestamp of QR Code creation. |
| expiration | integer | Unix timestamp when the QR Code expires. |
| external_id | string | null | The external_id you sent in the request. null if not provided, or sent as "" or blank. |
| qr_type | string | Which QR the payer can scan. "dynamic": render the emv. "printed_dynamic": the checkout has a printed QR; the payer can scan either the printed QR or the emv you render, and the response adds printed_dynamic_qr_data. Always read qr_type, never the checkout's setup: it can come back "dynamic" for a checkout with a printed QR. |
| printed_dynamic_qr_data | object | The checkout's printed QR: its EMV in pqr_emv, and its printed_dynamic_qr_id. Only present when qr_type is "printed_dynamic". |
Transaction Identifiers
| Identifier | Description |
|---|---|
kamipay_id | kamiPay's identifier for this transaction. Use this for tracking and when contacting support. |
operation_id | Banking-level tracking number assigned by the payment gateway. |
external_id | Your identifier. Reflects what you sent in the request. Useful for matching QR Codes to orders in your system. |
Multi-Wallet Routing
kamiPay gives you flexibility in how USDt settlements are routed after a QR Code is paid. There are two common setups:
- Centralized: All payments go to your main wallet. You collect and distribute funds to your users/clients on your end.
- Direct to client: Payments go directly to your clients' wallets. kamiPay handles the routing per transaction using
store_idandcheckout_id.
This makes it easy to build platforms where each merchant, store, or user receives their own settlement without you having to redistribute manually.
Any wallet used for settlement must be previously whitelisted with kamiPay. Use store_id and checkout_id in your request to select which wallet receives each payment.
For how stores, checkouts and wallets fit together, see Account Administration.
Contact us for more details on multi-wallet configurations so we can help you implement the setup that best fits your use case.