QR Code Creation
Create a dynamic Pix QR code to receive a payment in BRL, ARS or USDt.
This endpoint creates a dynamic Pix QR code. The payer scans it with the application of any Brazilian bank. Each request sends the amount and identifies the End User of the charge. kamiPay converts the amount and creates the QR code.
If you do not send currency, kamiPay reads amount as BRL. To charge in ARS or USDt, send that currency in currency.
Wallet configuration: your destination wallet is configured during onboarding. Each charge settles to the wallet of its checkout, never to a wallet that the request names. If you have more than one store or checkout, use store_id and checkout_id to select a wallet. For more information, see Multi-Wallet Routing.
Dynamic QR charges can use blockchain batched settlement. If the checkout uses it, the charges accumulate. kamiPay then pays them in one USDt transfer for each window, not in one transfer for each charge.
Request Body
| Name | Type | Description |
|---|---|---|
| amount | float | Required. The amount to charge, in the currency of currency. kamiPay converts the amount to BRL. If the BRL amount is less than 5 BRL or more than 100,000 BRL, the API rejects the request with 400. Thus, when the exchange rate changes, the same ARS or USDt amount can be out of this range. In BRL, the API rounds an amount with more than 2 decimals to 2 decimals, and does not reject it (10.129 → 10.13). Round the amount before you send it, so that the amount that you keep is equal to amount_brl. |
| receiver_name | string | Required. The full name of the End User. |
| receiver_id_type | string | Required. The type of the End User's identity document: one of the Document Types. |
| receiver_document | string | Required. The number of the End User's identity document. |
| receiver_country | string | Required, except with a CPF or CNPJ. The country that issued the End User's identity document, as an ISO 3166-1 alpha-3 code ("ARG"). |
| currency | string | Optional. The currency of amount: "ARS", "BRL" or "USDt". The API ignores letter case, so "ars", "brl" and "usdt" are also correct. If you do not send it, the value is "BRL". A USDt charge is approximate. The payer pays in BRL, and BRL has cents. Thus, kamiPay converts your amount to BRL, rounds it to the cent, and calculates the USDt again from the BRL amount. The USDt that the charge settles is amount_usdt in the response (amount: 2 → amount_brl: 10.69 → amount_usdt: 1.999545). For ARS, see the note in the 200 response. |
| expire | integer | Optional. The time in seconds until the QR code expires, from 300 (5 minutes) to 86400 (24 hours). If you do not send it, or send null, the value is 300. If the value is out of this range, the API rejects the request with 422. A sale on a printed checkout always expires after 5 minutes and does not use this value (see Printed Dynamic QRs). But the API also rejects an out-of-range value for a sale. |
| external_id | string | Optional. Your identifier for this charge. We recommend that you send it, to prevent duplicate charges and to track each charge. Two charges that you create with the same credentials cannot have the same external_id, whatever their type and age. If you send an external_id again, the API rejects the request with 409, and the 409 has the original charge (see the 409 response). If you do not send it, or send "" or only spaces, the charge has no external_id, and each request creates a new QR code. |
| external_data | string | Optional. A text description of this charge (for example, "Hotel booking #234"). |
| store_id | integer | Optional. The identifier of your store. If you have more than one store, send it. If you do not send it, the value is 1. |
| checkout_id | integer | Optional. The identifier of your checkout. If you have more than one checkout, send it. If you do not send it, the value is 1. |
| address | string | Optional. A check, not a selection of a wallet. The charge always settles to the wallet of its checkout. If you send an address that is not that wallet, the API rejects the request with 400. To change the wallet of a checkout, use Update Checkout. |
| transfer_description | string | Optional. A text note about the charge. kamiPay keeps it with the charge for compliance and reports. The payer does not see it, and the API does not return it. If the note has more than 140 characters, kamiPay keeps only the first 140 characters. |
End User
Each QR code identifies its End User: the person or company for whom you create the charge (your customer, or your own company). The End User is not the payer who scans the QR code.
Send the End User in the four receiver_* fields. These rules apply:
- All four fields are required, with one exception: with a
CPForCNPJ, you can omitreceiver_country. The API counts a blank value, or a value with only spaces, as missing. The API ignores letter case in all four fields. receiver_countryis the country that issued the document, not the nationality or the country of residence of the End User. For example, for a Venezuelan citizen with an Argentine DNI, sendARGandDNI.- With a
CPForCNPJ,receiver_countryis optional, and kamiPay reads the document as a document from Brazil. If you send a country with aCPForCNPJ, the country must beBRA. receiver_id_typeis a document type that the country issues, as Document Types shows.receiver_documentis a correct document of that type. The API ignores spaces, dots, dashes and slashes, so20-12345678-6and20123456786are the same CUIT. If Document Types gives a check digit for the type, the API makes sure that the check digit is correct.receiver_namehas at least 2 characters, and at least one of them is a letter. The API does not count the spaces at the start and at the end.- kamiPay keeps only the first 128 characters of each field. The API does not reject a longer value. It cuts the value before the checks of these rules.
Document Types
| Issuing country | receiver_id_type | Document format |
|---|---|---|
Argentina (ARG) | DNI | 7 or 8 digits. |
Argentina (ARG) | CUIT, CUIL | 11 digits. The last digit is a check digit. |
Chile (CHL) | RUT | 7 or 8 digits, then a check digit (0 to 9, or K). |
Uruguay (URY) | CI | 7 or 8 digits. The last digit is a check digit. |
Uruguay (URY) | RUT | 12 digits. |
Bolivia (BOL) | CI | 4 to 10 digits. Optionally, then a 2-character complement, the department of issue (such as LP), or both. |
Bolivia (BOL) | NIT | 7 to 13 digits. The first digit is not 0. |
Mexico (MEX) | CURP | 18 characters in the official format, with the birth date as YYMMDD. The last character is a check digit. |
Mexico (MEX) | INE | The 18-character elector key on the credential (6 letters, 8 digits, H or M, 3 digits), or the 9 to 13 digits of its CIC or OCR. |
Mexico (MEX) | RFC | 12 characters for a company, or 13 characters for an individual, in the official format. The date of incorporation or birth is in it as YYMMDD. |
Brazil (BRA) | CPF | 11 digits. The last two digits are check digits. |
Brazil (BRA) | CNPJ | 14 characters. The last two characters are check digits. The first 12 characters can include letters. |
Venezuela (VEN) | CI | 5 to 9 digits. Optionally, V or E before them. |
Colombia (COL) | CI | 5 to 10 digits. |
Paraguay (PRY) | CI | 5 to 10 digits. |
Ecuador (ECU) | CI | 5 to 10 digits. |
Peru (PER) | DNI | 8 digits, without the check character that the card shows next to them. |
Peru (PER) | RUC | 11 digits. The last digit is a check digit. |
Peru (PER) | CE, CPP | 9 digits. |
| Any country | PASSPORT | 6 to 20 letters (A to Z) or digits. |
| Any country | OTHER | A document that is not in this table, with at least one character. Send only OTHER as the type, and the document number as the document shows it. |
If a country that is not in this table issued the End User's document, identify the End User only with a passport (PASSPORT). If the End User has no passport, use OTHER.
Printed Dynamic QR Integration
Some checkouts have a Printed Dynamic QR as their default QR. If the checkout of the request is one of them, this endpoint creates a sale on the checkout, not a standalone QR code. A sale is a charge for one payer. If you do not send store_id and checkout_id, the checkout of the request is checkout 1 of store 1.
- kamiPay attaches the sale to the printed QR of the checkout. A payer who scans the printed QR sees the amount of the sale.
- The response has
qr_type: "printed_dynamic", akamipay_idthat starts withcqr_, and the EMV of the printed QR inprinted_dynamic_qr_data.pqr_emv. - The response also has an
emv. Thisemvis for the location of the checkout, not for the sale. With some payment gateways, it is the printed QR. With other payment gateways, it is a different EMV, and its text shows the amount of this sale, also after a newer sale replaces it.
Thus, a point of sale can show a permanent printed QR, and you can also show the returned emv on a screen.
A printed checkout holds one pending sale at a time. Before you create a new sale while a sale is pending, read One pending sale per checkout. The new sale displaces the unpaid sale, and each QR of the checkout then charges the newest sale. kamiPay credits the payment to the newest sale, and never sends a webhook for the displaced sale.
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 for whom you create the charge
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 for whom you create the charge
"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 for whom you create the charge
"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 (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"
}
}If currency is "ARS", the response also has amount_ars: the ARS amount that you sent, exactly as you sent it. amount_brl is the amount that the payer pays, and amount_usdt is the amount that the charge settles. rate is still the BRL/USDt exchange rate. The API does not return the ARS/BRL exchange rate, which is amount_ars / amount_brl. A BRL or USDt request does not have amount_ars.
Bad Request
{
"detail": "Converted BRL amount 1.0 is out of range [5, 100000]"
}The detail values that you can get, each with one example (the amount and the identifiers in it change):
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 BRL amount, after the conversion, is less than 5 BRL or more than 100,000 BRL. The text says "Converted" also when you send BRL. |
"Incorrect data Address, Store, Checkout: checkout 99 not found in store 1" | The store has no checkout with that checkout_id (1 if you do not send it). |
"Incorrect data Address, Store, Checkout: address doesn't match the wallet of checkout 1" | The address that you sent is not the wallet of the checkout. |
"Incorrect data Address, Store, Checkout: user has no access to store 2" | The user that your credentials belong to has no access to that store. |
"Wallet network deprecated" | The wallet of the checkout is not on Polygon (see Wallets). |
"Failed to convert USDT to BRL: …" | kamiPay cannot convert an ARS or USDt amount to BRL at this time. The text names ARS or USDT, and the text after the colon changes. Send the request again later. |
Unauthorized
With an invalid or expired token:
{
"detail": "Could not validate credentials"
}Conflict (Duplicate External ID)
Your credentials already created a charge with this external_id. The API does not create a new charge. detail has the original charge, with its emv. Thus, if you did not get the response when you created the charge, you can still show its QR code.
{
"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"
}
}detailhas the same fields that the creation of the original charge returned. If the original charge hadamount_ars, the Integrator Fee split orprinted_dynamic_qr_data,detailhas them too. It never hasaddress.- In the 409,
amount_arsis the quoted amount, rounded to the cent. The 200 gives the amount exactly as you sent it. Thus, for an ARS amount with more than 2 decimals, the two values are different. detaildoes not tell if the original charge was paid or expired. To find its status, use Tx Status with itskamipay_idoroperation_id.- If
detailhas onlymessageandkamipay_id, kamiPay cannot return the original charge at this time. This can occur when a first request is still creating the charge, or when an earlier attempt failed before it created its QR code. It can also occur when thekamipay_idis for a different type of charge, or when the charge is old. Send the request again after a few seconds. If you get the same response again, create the charge with a newexternal_id.
Validation Error
The API returns this error when the request body is not correct. For example, the End User is missing or not correct, currency is not one of its values, or expire is out of range. detail has one entry for each field that is not correct. For a missing field, input is the full request body.
For example, a request with a CUIT that has a wrong check digit, 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 that you can get, each with one example of its msg (the field, document type or country in it changes):
type | Example msg |
|---|---|
missing | Field required |
value_error | receiver_country must be an ISO 3166-1 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 |
For an incorrect currency, the entry is for the full request body: loc is ["body"], and input is the body exactly as you sent it. 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": {}
}
}
]
}This msg is the text that the API returns. Thus, it shows 'USDT', but this page writes currency as "USDt". Both values are correct, because the API ignores the letter case of currency.
A request without amount, or with amount: null, also gets an entry for the full request body. Its loc is ["body"], its type is value_error, and its msg is Value error, Either 'amount' or 'original_amount' must be provided. The msg names original_amount, but the field to send is amount.
For an expire out of range, the entry has loc ["body", "expire"], the expire that you sent as input, and the limit 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 your code, use loc and type, not msg. The API examines some fields only after the fields that they depend on are correct. Thus, when you correct one field, you can get an error for a different field.
On a printed checkout that has no Printed Dynamic QR, the API also returns 422, with a text detail (see Errors when creating a sale):
{
"detail": "This checkout does not have a printed dynamic QR assigned.Please create one or contact support if you need assistance."
}A request that gets a 422 does not create a charge. Thus, you can correct it and send it again with the same external_id.
Internal Server Error
{
"detail": "Internal Server Error during Pix Generation"
}The API can also return a 500 with the plain text Internal Server Error, not JSON.
Service Unavailable
On a printed checkout, when kamiPay disables Printed Dynamic QRs for a period:
{
"detail": "printed_qr_disabled"
}When this endpoint is in maintenance:
{
"detail": "Service is temporarily unavailable due to maintenance"
}The API does not create a charge. Send the request again later.
- Each response has an
x-request-idheader, except the 503 for maintenance and a 500 in plain text. When you contact support about a request, give itsx-request-id. - The type of
detailchanges with the error. For a 422,detailis a list, except on a printed checkout without a Printed Dynamic QR, where it is a string. For the other errors,detailis a string or an object. A 500 in plain text has nodetail.
Response Fields
| Field | Type | Description |
|---|---|---|
| emv | string | The Pix code to show as a QR code. Give this string to a QR code library. |
| operation_id | string | kamiPay's identifier for the Pix request of this charge (ptxr_…). The pay-in webhook sends it as pix_id. |
| kamipay_id | string | kamiPay's unique identifier for this charge: dqr_…, or cqr_… for a sale on a printed checkout. Use it to track the charge, and when you contact support. |
| amount_brl | float | The final amount in BRL that the payer pays. |
| amount_usdt | float | The USDt equivalent of the BRL amount. It is always present. |
| amount_ars | float | The ARS amount that you sent in the request. The payer pays its BRL equivalent. Only present if currency is "ARS". |
| rate | float | The BRL/USDt exchange rate for this charge. |
| date | integer | The Unix timestamp of the creation of the QR code. |
| expiration | integer | The Unix timestamp at which the QR code expires. |
| external_id | string | null | The external_id that you sent in the request. If you did not send it, or if you sent "" or only spaces, it is null. |
| qr_type | string | The QR that the payer can scan. "dynamic": show the emv. "printed_dynamic": the checkout has a printed QR, and the payer can scan the printed QR or the emv that you show. The response then also has printed_dynamic_qr_data. Always read qr_type, not the configuration of the checkout. For a checkout with a printed QR, it can be "dynamic". |
| printed_dynamic_qr_data | object | The printed QR of the checkout: its EMV in pqr_emv, and its printed_dynamic_qr_id. Only present if qr_type is "printed_dynamic". |
Transaction Identifiers
| Identifier | Description |
|---|---|
kamipay_id | kamiPay's identifier for this charge. Use it to track the charge, and when you contact support. |
operation_id | kamiPay's identifier for the Pix request (ptxr_…). The pay-in webhook sends it as pix_id. |
external_id | Your identifier. It is the value that you sent in the request. Use it to match QR codes to the orders in your system. |
Multi-Wallet Routing
When the payer pays a QR code, kamiPay settles the USDt to a wallet. There are two usual configurations:
- Centralized: all payments go to your main wallet. You collect the funds and distribute them to your users or clients.
- Direct to client: the payments go directly to the wallets of your clients. kamiPay selects the wallet for each charge from
store_idandcheckout_id.
With the second configuration, each merchant, store or user of your platform receives its own settlement, and you do not distribute the funds manually.
Each wallet that receives settlements must first be a whitelisted wallet at kamiPay. In each request, use store_id and checkout_id to select the wallet that receives the payment.
For the relation between stores, checkouts and wallets, see Account Administration.
For more information about multi-wallet configurations, contact us. We can help you select the configuration for your use case.