kamiPay LogokamiPay Docs

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

NameTypeDescription
amountfloatRequired. 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_namestringRequired. Full name of the End User.
receiver_id_typestringRequired. Type of the End User's identity document, one of the Document Types.
receiver_documentstringRequired. Number of the End User's identity document.
receiver_countrystringRequired, 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").
currencystringOptional. 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.
expireintegerOptional. 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_idstringOptional. 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_datastringOptional. Free-text description for this charge (e.g. "Hotel booking #234").
store_idintegerOptional. ID of your store. Default: 1. Only needed if you operate with multiple stores.
checkout_idintegerOptional. ID of your checkout. Default: 1. Only needed if you operate with multiple checkouts.
addressstringOptional. 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_descriptionstringOptional. 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_country can be omitted with a CPF or CNPJ (see below). A blank or whitespace-only value counts as missing, and letter case is ignored in all four.
  • receiver_country is 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 as ARG and DNI.
  • With a CPF or CNPJ, receiver_country is optional, and the document is taken as issued by Brazil. If you send a country with one, it must be BR or BRA.
  • receiver_id_type is a document type that the country issues, as listed in Document Types.
  • receiver_document is a valid document of that type. Spaces, dots, dashes and slashes are ignored, so 20-12345678-6 and 20123456786 are the same CUIT. Where the table below lists a check digit, it is verified.
  • receiver_name has at least 2 characters and at least one letter, not counting leading or trailing spaces.

Document Types

Issuing countryreceiver_id_typeDocument format
Argentina (AR / ARG)DNI7 or 8 digits.
Argentina (AR / ARG)CUIT, CUIL11 digits, the last one a check digit.
Chile (CL / CHL)RUT7 or 8 digits followed by a check digit (0 to 9, or K).
Uruguay (UY / URY)CI7 or 8 digits, the last one a check digit.
Uruguay (UY / URY)RUT12 digits.
Bolivia (BO / BOL)CI4 to 10 digits, optionally followed by a 2-character complement, the department of issue (such as LP), or both.
Bolivia (BO / BOL)NIT7 to 13 digits, not starting with 0.
Mexico (MX / MEX)CURP18 characters in the official format, with the birth date as YYMMDD. The last one is a check digit.
Mexico (MX / MEX)INEThe 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)RFC12 characters for a company, 13 for an individual, in the official format, with the date of incorporation or birth as YYMMDD.
Brazil (BR / BRA)CPF11 digits, the last two check digits.
Brazil (BR / BRA)CNPJ14 characters, the last two check digits. The first 12 can include letters.
Venezuela (VE / VEN)CI5 to 9 digits, optionally preceded by V or E.
Colombia (CO / COL)CI5 to 10 digits.
Paraguay (PY / PRY)CI5 to 10 digits.
Ecuador (EC / ECU)CI5 to 10 digits.
Peru (PE / PER)DNI8 digits, without the check character printed next to them on the card.
Peru (PE / PER)RUC11 digits, the last one a check digit.
Peru (PE / PER)CE, CPP9 digits.
Any countryPASSPORT6 to 20 letters (A to Z) or digits.
Any countryOTHERAny 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:

  1. The sale is attached to the checkout's printed QR: a customer scanning it sees the sale's amount.
  2. The response has qr_type: "printed_dynamic", a kamipay_id starting with cqr_, and the printed QR's EMV in printed_dynamic_qr_data.pqr_emv. It also carries an emv, 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 detailWhen
"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"
  }
}
  • detail carries the same fields the original's creation returned, including amount_ars, the Integrator Fee split and printed_dynamic_qr_data when the original had them. It never includes address.
  • It doesn't say whether the original was paid or expired. Check it with Tx Status, by its kamipay_id or operation_id.
  • A detail with only message and kamipay_id means 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 new external_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):

typeExample msg
missingField required
value_errorreceiver_country must be an ISO 3166-1 alpha-2 or alpha-3 code
value_errorreceiver_country must be BRA for a CPF
value_errorreceiver_id_type must be one of CE, CI, CNPJ, CPF, CPP, CUIL, CUIT, CURP, DNI, INE, NIT, OTHER, PASSPORT, RFC, RUC, RUT
value_errorreceiver_id_type CUIT is not valid for country CHL
value_errorreceiver_document is not a valid CUIT
value_errorreceiver_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:

typemsgctx
greater_than_equalInput should be greater than or equal to 300{"ge": 300}
less_than_equalInput 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-id header. Quote it when you contact support about a request.
  • detail's type depends on the error. A 422's detail is a list; the other errors' detail is a string or an object.

Response Fields

FieldTypeDescription
emvstringThe PIX brcode to render as a QR Code. Pass this string to any QR library.
operation_idstringBanking-level tracking number for this transaction.
kamipay_idstringkamiPay's unique identifier for this transaction: dqr_…, or cqr_… for a sale on a printed checkout. Use this for tracking and support.
amount_brlfloatFinal amount in Brazilian Reais that the payer will pay.
amount_usdtfloatUSDt equivalent of the BRL amount (always present).
amount_arsfloatThe ARS amount sent in the request. Its BRL equivalent is what the payer will be charged. Only present when currency is "ARS".
ratefloatBRL/USDt exchange rate used for this transaction.
dateintegerUnix timestamp of QR Code creation.
expirationintegerUnix timestamp when the QR Code expires.
external_idstring | nullThe external_id you sent in the request. null if not provided, or sent as "" or blank.
qr_typestringWhich 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_dataobjectThe 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

IdentifierDescription
kamipay_idkamiPay's identifier for this transaction. Use this for tracking and when contacting support.
operation_idBanking-level tracking number assigned by the payment gateway.
external_idYour 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_id and checkout_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.

On this page