Smart Transfers isn't available in Sandbox at the moment. To test your integration, use Production with real accounts.
With Smart Transfers, your user approves a Payment Authorization once at their bank, and you then trigger Pix transfers into another account they hold, whenever you need to, within the limits they approved. In this guide, we'll show you:
- the general flow of data
- how to create a Payment Authorization and its limits
- how to trigger a Charge against an authorized Payment Authorization
- how to follow authorizations and Charges with webhooks
- how to cancel a scheduled Charge or an authorization, and how to change the beneficiary account
Please make sure you have completed all the steps in our dedicated prerequisites article before continuing this guide.
Every request in this guide must include the X-Belvo-API-Resource-Version: Payments-BR.V2 header. Without it, Belvo responds with 404 - Not Found.
As you can see in the diagram below, the flow involves:
- Creating a Payment Authorization and redirecting your user to their bank to approve it.
- Waiting for the authorization to become
AUTHORIZED. - Creating a Charge each time you want to move funds.
- Following each Charge until it is
SETTLED(orFAILED).
The Payment Authorization is the consent your user gives at their bank. It defines the account that receives the funds and the limits every Charge must respect.
You can create the Payment Authorization in two ways:
- With an existing customer and bank account: send the
idof a customer and of a bank account you've already created in Belvo, inpayer.customerandbeneficiary.target. - With a new customer and bank account: send their details in the same request:
payer.customerwith the payer'sidentifierandname, andbeneficiary.targetwith the account'sholderanddetails. Belvo creates the customer and the bank account together with the Payment Authorization.
To create one, make a POST Create a Payment Authorization request:
curl --request POST 'https://api.belvo.com/payments/br/payment-authorizations/' \
-u SECRET_ID:SECRET_PASSWORD \
-H 'Content-Type: application/json' \
-H 'X-Belvo-API-Resource-Version: Payments-BR.V2' \
--data '{
"payment_method": "PIX_OF_SMART_TRANSFERS",
"description": "Top-up of your Acme account",
"external_id": "c169a8a9-e9f5-48db-9f4a-818caef9356b",
"return_url": "https://www.acmecorp.com/smart-transfers/return",
"expiration_date_time": "2027-10-16T23:59:59Z",
"payer": {
"customer": "4714c93c-0132-4da4-b8fe-659515e1b1b6",
"institution": "600f1b4a-1ef9-4f89-b341-1a35f0c32cc0"
},
"beneficiary": {
"type": "BANK_ACCOUNT",
"target": "a80d5a9d-20ae-479a-8dd7-ff3443bcbbfc"
},
"payment_method_configuration": {
"total_allowed_amount": "5000.00",
"payment_transaction_limit": "500.00",
"periodic_limits": {
"day": {
"quantity_limit": 2,
"transaction_limit": "500.00"
},
"month": {
"transaction_limit": "2000.00"
}
},
"start_date_time": "2026-10-16T14:30:00Z"
},
"metadata": {
"internal_reference_id": "GGq73487w2"
}
}'Smart Transfers only moves money between accounts held by the same person or company. For an individual payer, beneficiary.target.holder.identifier must be the payer's CPF (payer.customer.identifier). For a business payer, the beneficiary's CNPJ must share the payer's CNPJ root (first eight digits).
If you send a different holder, the authorization is created with status FAILED and the request returns an error. If you use an existing bank account id as the target, make sure it belongs to the payer.
| Parameter | Required | Description |
|---|---|---|
payment_method | true | Must be PIX_OF_SMART_TRANSFERS. |
description | true | A description for your internal reference. Keep it to 128 characters or fewer. |
external_id | optional (highly recommended) | Your own identifier for this authorization, as a UUID. Belvo doesn't enforce uniqueness, so use it to correlate the authorization with your records, not to prevent duplicates. |
return_url | true | The URL your user is sent back to after approving (or rejecting) the authorization at their bank. On failure, Belvo appends error and error_description query parameters. |
expiration_date_time | optional | When the authorization stops accepting Charges, in ISO 8601 format. Must be in the future and after start_date_time. The consent expires at 23:59:59 UTC on that date. If you omit it, the authorization doesn't expire on a date (it can still be consumed or revoked). |
payer.customer | true | The person or company paying. Either an existing Belvo customer id, or an object with the payer's identifier (CPF or CNPJ) and name. If the customer doesn't exist yet, Belvo creates it. |
payer.institution | true | The Belvo id of the payer's bank. Use the List all payment institutions endpoint to get it. |
payer.account_details | optional | The specific account to debit. If you omit it, the user chooses the account at their bank. See Debit a specific account below. |
beneficiary.type | true | Must be BANK_ACCOUNT. |
beneficiary.target | true | The account that receives the funds. Either an existing bank account id, or a bank account object with holder (identifier, name) and details (account_type: CHECKINGS, SAVINGS, or PAYMENTS; agency; institution; number). It must be held by the payer (see the warning above). |
payment_method_configuration | true | The limits for this authorization. The object is required, but every field inside it is optional. See Authorization limits below. |
metadata | optional | Up to 50 key-value pairs for your own use. Keys up to 40 characters, string values up to 500 characters, no nesting. |
By default, your user chooses which of their accounts to pay from when they approve the authorization at their bank. If you already know the account to debit, for example because you monitor its balance with Banking Aggregation, send its details in payer.account_details. The user can then only approve the authorization for that account, and every Charge debits it.
"payer": {
"customer": "4714c93c-0132-4da4-b8fe-659515e1b1b6",
"institution": "600f1b4a-1ef9-4f89-b341-1a35f0c32cc0",
"account_details": {
"account_type": "CACC",
"ispb": "12345678",
"issuer": "6272",
"number": "11188222"
}
}| Field | Description |
|---|---|
account_type | The type of account: CACC (checking), SVGS (savings), SLRY (salary), or TRAN (payment account). |
ispb | The 8-digit ISPB code of the payer's bank. |
issuer | The account's branch (agency) code, up to 4 digits. In Banking Aggregation, this is the account's agency. |
number | The account number, up to 20 digits. In Banking Aggregation, this is the account's number. |
If you send account_details, include all four fields. payer.institution is still required, and it must be the payments institution of the same bank as ispb.
The limits in payment_method_configuration are what your user sees and approves at their bank, so set them to cover the transfers you expect to make, and no more.
| Field | Description |
|---|---|
total_allowed_amount | The maximum total of all Charges over the life of the authorization, as a string (minimum 0.01). When it's used up, the authorization becomes CONSUMED. |
payment_transaction_limit | The maximum amount of a single Charge, as a string. Must not be greater than total_allowed_amount. |
periodic_limits | Caps per calendar window. Include any of day, week, month, or year. Each window you include must set at least one of quantity_limit (the maximum number of successful Charges in that window, minimum 1) and transaction_limit (the maximum total amount charged in that window, as a string). Reaching a window's cap only blocks Charges until the window resets; it doesn't consume the authorization. |
start_date_time | When the authorization starts accepting Charges, in ISO 8601 format. It can be immediate, but not in the past. If you omit it, Charges can start from the day the authorization is created. |
When you create a Charge, Belvo rejects it straight away if its amount is above payment_transaction_limit or total_allowed_amount. Running totals and the periodic_limits are enforced by the user's bank, so a Charge that breaks them is created and then moves to FAILED with a status_reason_code such as CHARGE_PERIOD_LIMIT_REACHED.
The response contains the authorization id and the authorization_url:
{
"id": "9fc68b84-f2d6-4142-b2ad-9d2d1ad70432",
"created_at": "2026-10-16T14:30:02Z",
"updated_at": "2026-10-16T14:30:02Z",
"authorized_at": null,
"status": "AWAITING_AUTHORIZATION",
"status_reason_code": null,
"status_reason_message": null,
"status_updated_at": "2026-10-16T14:30:02Z",
"payment_method": "PIX_OF_SMART_TRANSFERS",
"description": "Top-up of your Acme account",
"external_id": "c169a8a9-e9f5-48db-9f4a-818caef9356b",
"return_url": "https://www.acmecorp.com/smart-transfers/return",
"expiration_date_time": "2027-10-16T23:59:59Z",
"payer": {
"customer": "4714c93c-0132-4da4-b8fe-659515e1b1b6",
"institution": "600f1b4a-1ef9-4f89-b341-1a35f0c32cc0",
"representative_identifier": null,
"bank_account": null
},
"beneficiary": {
"type": "BANK_ACCOUNT",
"target": "a80d5a9d-20ae-479a-8dd7-ff3443bcbbfc"
},
"payment_method_configuration": {
"total_allowed_amount": "5000.00",
"payment_transaction_limit": "500.00",
"periodic_limits": {
"day": {
"quantity_limit": 2,
"transaction_limit": "500.00"
},
"month": {
"transaction_limit": "2000.00"
}
},
"start_date_time": "2026-10-16T14:30:00Z",
"authorization_url": "https://institution.example.com/authorize?client_id=..."
},
"charges": [],
"metadata": {
"internal_reference_id": "GGq73487w2"
}
}Make sure to save the id: you need it to create Charges. Then redirect your user to payment_method_configuration.authorization_url.
Each bank sets how long its authorization_url stays valid, which can be as short as a few minutes and up to around 60 minutes. If it expires before your user approves, create a new Payment Authorization. An authorization can't be renewed.
Your user approves (or rejects) the authorization at their bank and is then sent back to your return_url. Whenever the authorization's status changes, Belvo sends a PAYMENT_AUTHORIZATION webhook. Version 2 webhooks don't carry the status, so fetch the authorization to read it:
{
"schema_version": "2.0.0",
"resource": "PAYMENT_AUTHORIZATION",
"resource_id": "9fc68b84-f2d6-4142-b2ad-9d2d1ad70432",
"resource_version": "V2",
"timestamp": "2026-10-16T14:33:10Z"
}curl --request GET 'https://api.belvo.com/payments/br/payment-authorizations/{payment_authorization_id}/' \
-u SECRET_ID:SECRET_PASSWORD \
-H 'X-Belvo-API-Resource-Version: Payments-BR.V2'Once status is AUTHORIZED, you can create Charges. payer.bank_account then shows the (masked) account the user chose to pay from. If the status is FAILED, check status_reason_code; for example, USER_REJECTED means your user declined at their bank. See Statuses reference for every status.
Each Charge moves one amount from the payer's account to the beneficiary account on the authorization. Make a POST Create a Charge request to the Charges endpoint of the authorization:
curl --request POST 'https://api.belvo.com/payments/br/payment-authorizations/{payment_authorization_id}/charges/' \
-u SECRET_ID:SECRET_PASSWORD \
-H 'Content-Type: application/json' \
-H 'X-Belvo-API-Resource-Version: Payments-BR.V2' \
--data '{
"amount": "150.50",
"date": "2026-10-16",
"statement_description": "Acme top-up",
"external_id": "5f0b8f3e-3c1a-4a7e-9d61-1d2b7a1e9c42",
"metadata": {
"internal_reference_id": "GGq73487w2"
}
}'| Parameter | Required | Description |
|---|---|---|
amount | true | The amount to transfer, as a string with two decimal places (minimum 0.01). Must not be greater than the authorization's payment_transaction_limit or total_allowed_amount. |
date | true | The date the transfer should happen, in YYYY-MM-DD format. It can be today. It must be on or after the authorization's start_date_time date (or its creation date, if you didn't set one), compared in UTC: late in the evening in Brasília, "today" may already be tomorrow in UTC. A future date creates a SCHEDULED Charge. |
statement_description | true | The description shown on your user's bank statement (1 to 140 characters). |
external_id | optional (highly recommended) | Your own identifier for this Charge, as a UUID. Belvo doesn't enforce uniqueness. |
metadata | optional | Key-value pairs for your own use, with the same rules as on the authorization. |
You can't send a different beneficiary on a Charge. Every Charge pays the account set on the Payment Authorization.
{
"id": "0d3ffb69-f83b-456e-ad8e-208d0998d71d",
"created_at": "2026-10-16T15:00:01Z",
"status": "PENDING",
"status_reason_code": null,
"status_reason_message": null,
"status_updated_at": "2026-10-16T15:00:01Z",
"payment_authorization": "9fc68b84-f2d6-4142-b2ad-9d2d1ad70432",
"payment_method": "PIX_OF_SMART_TRANSFERS",
"amount": "150.50",
"date": "2026-10-16",
"statement_description": "Acme top-up",
"end_to_end_id": "E12345678202610161500abcDEF12345",
"beneficiary": {
"bank_account": {
"id": "a80d5a9d-20ae-479a-8dd7-ff3443bcbbfc",
"details": {
"account_type": "CHECKINGS",
"agency": "0001",
"code": "260",
"institution": "f512d996-583a-4a91-8b5b-eba2e103b068",
"number": "**7220"
},
"holder": {
"identifier": "12345678909",
"name": "Caetano Veloso"
}
}
},
"payer": {},
"external_id": "5f0b8f3e-3c1a-4a7e-9d61-1d2b7a1e9c42",
"metadata": {
"internal_reference_id": "GGq73487w2"
},
"previous": null,
"next": null
}Make sure to save the Charge id. The end_to_end_id is the Pix end-to-end identifier for the transfer; use it to reconcile the payment. payer stays empty until the bank returns the debited account.
If the bank rejects the Charge immediately, Belvo still responds with 201 - Created, but the Charge's status is already FAILED, and no webhook is sent for that transition. Always check status in the response before waiting for a webhook.
Belvo sends a CHARGE webhook whenever a Charge's status changes. As with the authorization, fetch the Charge to read its status:
{
"schema_version": "2.0.0",
"resource": "CHARGE",
"resource_id": "0d3ffb69-f83b-456e-ad8e-208d0998d71d",
"resource_version": "V2",
"timestamp": "2026-10-16T15:00:07Z"
}curl --request GET 'https://api.belvo.com/payments/br/payment-authorizations/{payment_authorization_id}/charges/{charge_id}/' \
-u SECRET_ID:SECRET_PASSWORD \
-H 'X-Belvo-API-Resource-Version: Payments-BR.V2'A Charge is successful when its status is SETTLED. For the webhook format, delivery, and retry policy, see Payments Webhooks (Brazil).
SETTLED confirms that the funds left the payer's account. Belvo works with the payer's bank over Open Finance, so it doesn't see the beneficiary side. Confirm the credit on the receiving account yourself.
You've created a Payment Authorization, waited for your user to approve it, and moved funds with a Charge. Create more Charges against the same authorization whenever you need to, within its limits.
| Action | Endpoint | When it's allowed |
|---|---|---|
| Cancel a scheduled Charge | POST /payments/br/payment-authorizations/{payment_authorization_id}/charges/{charge_id}/cancel/ | The Charge is PENDING or SCHEDULED, its date is after today (Brasília time, cutoff 23:59), and the authorization is AUTHORIZED. The Charge becomes CANCELED. |
| Cancel the authorization | POST /payments/br/payment-authorizations/{payment_authorization_id}/cancel/ | The authorization is AUTHORIZED. It becomes REVOKED, and no more Charges can be created. |
Both return 204 - No Content. The status change happens asynchronously, and you receive a webhook when it does. You can't retry a failed Smart Transfers Charge: create a new Charge instead.
While a Payment Authorization is AUTHORIZED, you can switch the account that receives the funds to another bank account you've already registered, without asking your user to approve a new authorization. Make a PATCH Change a Payment Authorization's beneficiary request:
curl --request PATCH 'https://api.belvo.com/payments/br/payment-authorizations/{payment_authorization_id}/' \
-u SECRET_ID:SECRET_PASSWORD \
-H 'Content-Type: application/json' \
-H 'X-Belvo-API-Resource-Version: Payments-BR.V2' \
--data '{
"beneficiary": {
"type": "BANK_ACCOUNT",
"target": "b6278377-f710-4d1a-a026-7bab757256d0"
}
}'target must be the id of an existing bank account held by the same person or company as the current beneficiary. Belvo responds with 200 - OK and the updated Payment Authorization, and later Charges pay the new account. If the authorization isn't AUTHORIZED, Belvo responds with 400 - Bad Request.
| Status | Meaning |
|---|---|
AWAITING_AUTHORIZATION | Created. Waiting for the user to approve it at their bank. |
PARTIALLY_ACCEPTED | Waiting for more approvals at the bank (for example, a business account that needs several signatures). |
AUTHORIZED | Approved. You can create Charges. |
FAILED | Not approved, or couldn't be created. See status_reason_code. |
CONSUMED | The total_allowed_amount was used up, or the authorization expired. No more Charges can be created. (Reaching a periodic_limits cap doesn't consume the authorization.) |
REVOKED | Cancelled by you, the user, or the bank. No more Charges can be created. |
| Status | Meaning |
|---|---|
PENDING | Created and being processed. |
SCHEDULED | Accepted by the bank for a future date. |
SETTLED | The funds left the payer's account. Final. |
FAILED | The transfer didn't happen. See status_reason_code. Final. |
CANCELED | You cancelled the Charge before its date. Final. |
For the full list of status_reason_code values, see Payments Brazil Codes and Messages.
| Situation | What happens |
|---|---|
Missing X-Belvo-API-Resource-Version header, or Smart Transfers isn't enabled for your organization. | 404 - Not Found. |
| Invalid request body. | 400 - Bad Request with a list of {field, code, message, request_id} errors. |
| The bank or Open Finance network rejects the authorization during creation (for example, the beneficiary isn't held by the payer). | The authorization is saved as FAILED, and the request currently returns 500 - Internal Server Error with a list of {code, message} errors, without the authorization id. To find it, list the customer's authorizations (GET /payments/br/payment-authorizations/?customer={customer_id}). |
You create a Charge on an authorization that isn't AUTHORIZED. | 400 - Bad Request: Payment Authorization is not AUTHORIZED. |
| The bank rejects a Charge immediately. | 201 - Created with status: FAILED, and no webhook. |
These endpoints don't support idempotency keys yet, and external_id doesn't prevent duplicates. If a request times out, fetch the authorization's Charges (GET /payments/br/payment-authorizations/{payment_authorization_id}/charges/) and check for your external_id before you retry, so you don't transfer twice.