# Direct API (Smart Transfers)

No Sandbox environment
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


Prerequisites
Please make sure you have completed all the steps in our dedicated prerequisites article before continuing this guide.

Version header required
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`.

## Data flow overview

As you can see in the diagram below, the flow involves:

1. Creating a Payment Authorization and redirecting your user to their bank to approve it.
2. Waiting for the authorization to become `AUTHORIZED`.
3. Creating a Charge each time you want to move funds.
4. Following each Charge until it is `SETTLED` (or `FAILED`).



```mermaid
sequenceDiagram
    autonumber
    participant App as Your application
    participant Belvo as Belvo
    participant Bank as User's bank

    App->>Belvo: POST /payments/br/payment-authorizations/
    Belvo-->>App: 201 - Created (authorization_url)
    App->>Bank: Redirect the user to authorization_url
    Note over Bank: The user reviews and approves the limits
    Bank-->>App: User returns to your return_url
    Belvo-->>App: WEBHOOK PAYMENT_AUTHORIZATION
    App->>Belvo: GET /payments/br/payment-authorizations/{id}/
    Belvo-->>App: 200 - OK (status: AUTHORIZED)

    rect rgba(0, 100, 255, .05)
        Note over App,Bank: Repeat whenever you need to move funds
        App->>Belvo: POST /payments/br/payment-authorizations/{id}/charges/
        Belvo->>Bank: Pix transfer request
        Belvo-->>App: 201 - Created
        Belvo-->>App: WEBHOOK CHARGE
        App->>Belvo: GET /payments/br/payment-authorizations/{id}/charges/{charge_id}/
        Belvo-->>App: 200 - OK (status: SETTLED)
    end
```

## Create a Payment Authorization

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 `id` of a customer and of a bank account you've already created in Belvo, in `payer.customer` and `beneficiary.target`.
- **With a new customer and bank account:** send their details in the same request: `payer.customer` with the payer's `identifier` and `name`, and `beneficiary.target` with the account's `holder` and `details`. Belvo creates the customer and the bank account together with the Payment Authorization.


To create one, make a POST Create a Payment Authorization request:

With existing customer and bank account

```shell Create a Payment Authorization
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"
    }
  }'
```

With new customer and bank account

```shell Create a Payment Authorization
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": {
        "identifier": "12345678909",
        "name": "Caetano Veloso"
      },
      "institution": "600f1b4a-1ef9-4f89-b341-1a35f0c32cc0"
    },
    "beneficiary": {
      "type": "BANK_ACCOUNT",
      "target": {
        "holder": {
          "identifier": "12345678909",
          "name": "Caetano Veloso"
        },
        "details": {
          "account_type": "CHECKINGS",
          "agency": "0001",
          "institution": "f512d996-583a-4a91-8b5b-eba2e103b068",
          "number": "457220"
        }
      }
    },
    "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"
    }
  }'
```

The beneficiary must be the payer's own account
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. |


### Debit a specific account

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.


```json payer with account_details
"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`.

### Authorization limits

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


Who checks the limits
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`:


```json Response: 201 - Created
{
  "id": "9fc68b84-f2d6-4142-b2ad-9d2d1ad70432", // [!code highlight]
  "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=..." // [!code highlight]
  },
  "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`.

Authorization URL expiry
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.

## Wait for the authorization to be approved

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:


```json PAYMENT_AUTHORIZATION webhook
{
  "schema_version": "2.0.0",
  "resource": "PAYMENT_AUTHORIZATION", // [!code highlight]
  "resource_id": "9fc68b84-f2d6-4142-b2ad-9d2d1ad70432", // [!code highlight]
  "resource_version": "V2",
  "timestamp": "2026-10-16T14:33:10Z"
}
```


```shell Get the Payment Authorization
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.

## Create a Charge

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:


```shell Create a charge
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. |


Charges always pay the authorization's beneficiary
You can't send a different beneficiary on a Charge. Every Charge pays the account set on the Payment Authorization.


```json Response: 201 - Created
{
  "id": "0d3ffb69-f83b-456e-ad8e-208d0998d71d", // [!code highlight]
  "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.

A 201 doesn't mean the Charge succeeded
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.

## Listen for Charge webhooks

Belvo sends a `CHARGE` webhook whenever a Charge's status changes. As with the authorization, fetch the Charge to read its status:


```json CHARGE webhook
{
  "schema_version": "2.0.0",
  "resource": "CHARGE", // [!code highlight]
  "resource_id": "0d3ffb69-f83b-456e-ad8e-208d0998d71d", // [!code highlight]
  "resource_version": "V2",
  "timestamp": "2026-10-16T15:00:07Z"
}
```


```shell Get the charge
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).

What SETTLED confirms
`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.

Done!
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.

## Cancel a Charge or an authorization

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

## Change the beneficiary account

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:


```shell Change the beneficiary account
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`.

## Statuses reference

### Payment Authorization

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


### Charge

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

## Errors and edge cases

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


No idempotency yet
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.