> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lerian.studio/llms.txt
> Use this file to discover all available pages before exploring further.

# Migrate Bank Transfer fees to Midaz

> Move fee packages from the standalone plugin-fees to the Midaz Fees Engine, and switch Bank Transfer 3.1.0 or later to Midaz fees without a gap in charging.

Bank Transfer 3.0.x charges fees through the standalone Fees plugin, `plugin-fees`. Bank Transfer 3.1.0 and later can charge them through the [Fees Engine](/en/products/midaz/fees/fees-engine-overview) that runs inside Midaz 4.1.0 and later. This page moves your fee packages from plugin-fees to Midaz. It then switches Bank Transfer over in an order that keeps every transfer charged.

Follow this page when all of these are true:

* You run Bank Transfer 3.0.x.
* Your fee packages live in plugin-fees.
* You upgrade to Bank Transfer 3.1.0 or later and to Midaz 4.1.0 or later.

Bank Transfer uses only the fee packages of plugin-fees. If you also keep billing packages in plugin-fees, recreate them as Midaz [billing packages](/en/products/midaz/fees/using-fee-engine) before you retire plugin-fees. This page does not map them.

<Note>
  The Lerian Console manages Midaz fee packages only. It does not show the packages that live in plugin-fees. See [Console](#console).
</Note>

## How Bank Transfer chooses who charges the fee

***

From 3.1.0, the Bank Transfer variable `MIDAZ_FEE_MODE` decides who calculates and charges the fee of each transfer.

| Value | Who charges the fee | Midaz API that Bank Transfer posts on |
| - | - | - |
| `legacy` | plugin-fees, as in Bank Transfer 3.0.x | `/v1` |
| `native` | The Midaz Fees Engine, from the fee packages of the ledger | `/v2` |
| `auto` (default) | Bank Transfer reads the Midaz version. It uses `native` on Midaz 4.1.0 or later, and `legacy` on an earlier version. | `/v2` or `/v1` |

* Bank Transfer does not start with any other value.
* Under `auto`, Bank Transfer reads the Midaz version again every `MIDAZ_FEE_MODE_REFRESH` (default `5m`). When it cannot read the version, it keeps the mode it has, or uses `legacy` if it has none.
* Bank Transfer fixes the mode of a P2P transfer or TED OUT at `/initiate`, and of a TED IN when it receives it. Every later step of that transfer uses the same mode: retries, confirmation, cancellation and reconciliation. A change of mode applies to new transfers only.
* In `legacy` mode, `BTF_FEE_ENABLED` turns the plugin-fees integration on. With `false`, the default, Bank Transfer does not call plugin-fees, and every transfer runs with fee 0.
* Set `native` only on Midaz 4.1.0 or later. Midaz 4.0.x charges fees without marking the fee entries, so Bank Transfer cannot read the fee that it charged.

<Warning>
  Do not rely on `auto` during the migration. Under `auto`, Bank Transfer 3.1.0 moves to `native` by itself as soon as it reads Midaz 4.1.0 or later. New transfers then use the Midaz packages. If your packages are still only in plugin-fees, Midaz finds no package, and those transfers carry no fee.

  In Bank Transfer 3.1.0, `BTF_FEE_ENABLED` does not stop Midaz from charging fees in `native` mode. To stop a fee, disable its Midaz package with `enable: false`, or delete the package. Midaz then also charges no fee on a native transfer that it has not posted yet.
</Warning>

## Before you start

***

You need:

* Midaz 4.1.0 or later, or a plan to upgrade to it. See [Updating Midaz](/en/products/midaz/updating-midaz).
* Bank Transfer 3.1.0 or later, or a plan to upgrade to it.
* Access to the plugin-fees API, to read and estimate your packages.
* Access to the Midaz fee package API, or to the Console [Fee Packages](/en/products/midaz/fees/console/managing-fee-packages) page.
* For each ledger and transfer type, the Midaz transaction route that Bank Transfer posts with. The tenant policy `routing.ledger_bindings` names it. See [Ledger routing](/en/interfaces/ted-jd/ted-configuration#ledger-routing).
* Three Midaz permissions for the Midaz credentials of Bank Transfer: the resource `packages` with action `get`, the resource `estimates` with action `post`, and the resource `organizations` with action `get`. In single-tenant mode, these are the credentials of `MIDAZ_CLIENT_ID`. In multi-tenant mode, Bank Transfer uses the Midaz credentials of each tenant, so grant all three permissions to the application of each tenant.

`legacy` mode does not use these permissions. In `native` mode, without `packages`, every P2P and TED OUT initiation answers `503 BTF-2000`. Without `estimates`, every initiation that matches a package answers `503 BTF-2000`. `organizations` is needed because this page sets `MIDAZ_FEE_MODE=native` explicitly: Bank Transfer then makes one read of the Midaz organization list, to check the `/v2` API, before it first uses a set of credentials. Under `auto`, it does not make this read. Without `organizations`, Bank Transfer in single-tenant mode does not start, and in multi-tenant mode every P2P and TED OUT initiation of that tenant answers `503 BTF-2000`.

The examples below use these shell variables. Every UUID is a placeholder. Use the values from your own environment.

Bank Transfer reaches the Midaz ledger at `MIDAZ_TRANSACTION_URL`, or at `MIDAZ_BASE_URL` when that is unset. It strips a trailing `/v1` or `/v2` from that address. Strip it in `MIDAZ_LEDGER_URL` too, because the paths below carry the version.

```bash theme={null}
# Addresses, as Bank Transfer has them.
FEES_BASE_URL="https://plugin-fees.example.com"    # FEES_BASE_URL
MIDAZ_LEDGER_URL="https://midaz.example.com"

# Two different bearers. They are not interchangeable.
FEES_BEARER_TOKEN="..."                            # for plugin-fees
MIDAZ_BEARER_TOKEN="..."                           # for the ledger

MIDAZ_ORGANIZATION_ID="3fa85f64-5717-4562-b3fc-2c963f66afa6"
MIDAZ_LEDGER_ID="9c858901-8a57-4791-81fe-4a34d4dd8ab5"
```

## Map a plugin-fees package to a Midaz package

***

Migrate only the packages that are enabled in plugin-fees. Leave the disabled ones out. Midaz refuses a package whose amount range overlaps another package with the same route and segment, and it counts disabled packages too (error `0199`). So a disabled copy can block a package that you need.

The fee rules keep their field names and their JSON shape. Three things change: where the organization goes, where the ledger goes, and what `transactionRoute` holds.

| plugin-fees | Midaz | What to do |
| - | - | - |
| `X-Organization-Id` header | `{organization_id}` in the URL | Send the organization in the path. |
| `ledgerId` in the body | `{ledger_id}` in the URL | Send the ledger in the path. |
| `transactionRoute` | `transactionRoute` | Replace the value. See [Transaction route](#transaction-route). |
| `feeGroupLabel`, `description` | Same fields | Copy. |
| `segmentId` | `segmentId` | Copy it or leave it out, as [Package scope](#package-scope) says. Leave it out of every TED IN package. |
| `minimumAmount`, `maximumAmount` | Same fields | Copy. |
| `waivedAccounts` | `waivedAccounts` | Copy. Both accept account aliases and the `segment:<segment-uuid>` form. |
| `enable` | `enable` | Migrate only packages with `true`. Create each one with `false`. You enable it when you switch to `native`. |
| `feeLabel`, `calculationModel`, `referenceAmount`, `priority`, `isDeductibleFrom` of each fee | Same fields | Copy. |
| `creditAccount` of each fee | `creditAccount` | Copy. The account must exist in the ledger. Midaz checks it when you create the package. |
| `routeFrom`, `routeTo` of each fee | Same fields | Midaz writes the value as the operation route of the fee entry. Use the ID of a Midaz operation route, or leave the field out. In `legacy` mode, Bank Transfer dropped a value that was not a route ID. |
| `id`, `createdAt`, `updatedAt`, `deletedAt` | New values | Leave them out. Midaz assigns a new ID. Keep a record of the plugin-fees package that each Midaz package replaces. |
| No equivalent | `metadataSelector` (Midaz 4.2.0 or later) | Leave it out. The fee preview of Bank Transfer does not read it, so the fee shown at initiation could differ from the fee that Midaz charges. |

Midaz rejects a field that it does not know. Send none of `ledgerId`, `id`, `createdAt`, `updatedAt` and `deletedAt`.

A TED OUT charges its fee on top of the amount. In `native` mode, Bank Transfer refuses a TED OUT with `422 BTF-3002` when the package that matches it has a fee with `isDeductibleFrom: true`.

### Transaction route

plugin-fees compares `transactionRoute` with a name that Bank Transfer sends: `ted_out`, `ted_in` or `p2p`. Midaz compares it with the `routeId` of the transaction, which is the ID of a Midaz transaction route. Midaz accepts only a UUID in this field.

For each package, use the transaction route that `routing.ledger_bindings` names for the ledger and the transfer type of the package.

<Warning>
  A Midaz package charges every `/v2` transaction of its ledger that matches its scope, from any product, not only the transfers of Bank Transfer. A package with no `transactionRoute` matches every transaction. A package with a route matches every transaction posted with that route. Give each Bank Transfer package its transaction route, and do not use those transaction routes in other products.

  A ledger whose binding uses `mode: omit` has no safe package: the only one that can match its transfers has no route, so it charges every `/v2` transaction of the ledger. Give that binding the Midaz transaction route of each transfer type on the Console [Accounting Routes](/en/interfaces/ted-jd/console/bt-accounting-routes) page, and check **Cutover readiness** there. This changes the accounting of every Bank Transfer posting on that ledger. Do it after step 2, once Midaz runs 4.1.0 or later, and before you switch.
</Warning>

### Package scope

plugin-fees and Midaz select the package of a transfer in different ways, so an exact copy can charge a different fee. Apply these rules to the enabled packages of each ledger:

* **The ledger has one enabled package.** plugin-fees applies it to every transfer in its amount range, P2P, TED OUT and TED IN alike. It ignores the `transactionRoute` and the `segmentId` of that package. In Midaz, create one copy for each transfer type that it charged, each with the transaction route of that type, and without `segmentId`.
* **A route has one package.** When the ledger has more than one enabled package, plugin-fees applies the only package of a route to every transfer on that route, inside its amount range. It ignores the `segmentId` of that package. Create the Midaz package without `segmentId`.
* **A route has several packages.** plugin-fees then picks by the segment of the sender. A sender with a segment gets only a package with that segment. A sender with no segment gets only a package with no segment. Keep each `segmentId`. Midaz also applies a package with no segment to a sender who has a segment, when no package of that segment covers the amount. If the route has a package with no segment, decide which behavior you want. When every fee of that package is charged on top of the amount (`isDeductibleFrom: false`), you can keep the plugin-fees behavior. Add `segment:<segment-uuid>` to the `waivedAccounts` of that package for every segment of the ledger, and for every segment that you create later. Midaz then charges none of its fees to a sender in those segments. On a fee taken out of the amount, the waiver also exempts a recipient in those segments, so do not use it there. For such a package, the engines cannot match. Decide whether a sender whose segment has no package of its own for that amount pays it in Midaz, or whether you leave it out, so that a sender with no segment pays none of its fees.
* **TED IN.** For a TED IN, Bank Transfer sent plugin-fees the segment of the recipient account. Midaz reads the segment from the source accounts, and it skips the external account, which is the only source of a TED IN. So a Midaz package with a `segmentId` never matches a TED IN. Create every TED IN package without `segmentId`. If the ledger has more than one TED IN package in plugin-fees, plugin-fees charged a recipient with a segment only a package with that segment, and a recipient with no segment only a package with no segment. Midaz cannot see the segment of the recipient, and it applies the packages with no segment to every recipient. Decide the TED IN price that applies to every recipient.
* **A package with no route.** When the ledger has more than one enabled package, plugin-fees never applied a package with no route to a Bank Transfer transfer, because Bank Transfer always sends a route. Leave it out.
* **Two packages that match.** Midaz selects the package that matches the most constraints. When two packages match equally, Midaz refuses the transaction with error `0198`. At initiation, Bank Transfer answers `422 BTF-2005`.

## Migrate step by step

***

<Steps>
  <Step title="Pin the legacy mode">
    Set `MIDAZ_FEE_MODE=legacy` in the Bank Transfer environment. In the Helm chart, the key is `bankTransfer.configmap.MIDAZ_FEE_MODE`, and the chart renders `auto` when the key is unset. Bank Transfer 3.0.x ignores the variable, so you can set it ahead of the upgrade.

    Check the value that your deployment renders before you upgrade, for example with `helm template` or `helm diff`. A rolling update does not stop after its first pod unless you pause it.

    Keep `BTF_FEE_ENABLED=true` and the `FEES_*` variables as they are.
  </Step>

  <Step title="Upgrade Bank Transfer, then Midaz">
    Upgrade Bank Transfer to 3.1.0 or later. Wait until every Bank Transfer pod runs 3.1.0 or later. An earlier version cannot settle a transfer that Bank Transfer created in `native` mode.

    Check the log of every pod for `midaz: fee mode resolved` with `feeMode=legacy` and `source=config`. In single-tenant mode, a pod logs it at boot. In multi-tenant mode, it logs it for each tenant at the first transfer of that tenant. `source=config` proves that the pin works. Check it while Midaz still runs a version earlier than 4.1.0. There, `auto` also resolves to `legacy`, so a transfer that pays its plugin-fees fee does not prove the pin.

    Upgrade Midaz to 4.1.0 or later.

    Make a small transfer. Check that plugin-fees still charges its fee.
  </Step>

  <Step title="Recreate each enabled package in Midaz">
    List the enabled packages of each ledger in plugin-fees. Each answer holds one page, and its `total` counts the items of that page only. Read the next page until a page returns fewer items than `limit`. The list leaves out deleted packages.

    ```bash theme={null}
    curl -s "$FEES_BASE_URL/v1/packages?ledgerId=$MIDAZ_LEDGER_ID&enable=true&limit=100&page=1" \
      -H "Authorization: Bearer $FEES_BEARER_TOKEN" \
      -H "X-Organization-Id: $MIDAZ_ORGANIZATION_ID"
    ```

    Create each package in Midaz with the mapping and the scope rules above. Set `enable` to `false`. Keep a list of the packages that you create: you enable exactly these when you switch. This example recreates a flat TED OUT fee. Its `transactionRoute` is the TED OUT transaction route of the ledger binding.

    ```bash theme={null}
    curl -s -X POST "$MIDAZ_LEDGER_URL/v2/organizations/$MIDAZ_ORGANIZATION_ID/ledgers/$MIDAZ_LEDGER_ID/packages" \
      -H "Authorization: Bearer $MIDAZ_BEARER_TOKEN" \
      -H 'Content-Type: application/json' \
      -d '{
        "feeGroupLabel": "TED OUT fee",
        "description": "Fee on outgoing TEDs",
        "transactionRoute": "0199a5c1-3e2f-7b4d-9a8e-5f6d7c8b9a0e",
        "minimumAmount": "0.01",
        "maximumAmount": "1000000.00",
        "fees": {
          "tedOutFee": {
            "feeLabel": "TED OUT fee",
            "calculationModel": {
              "applicationRule": "flatFee",
              "calculations": [{ "type": "flat", "value": "5.00" }]
            },
            "referenceAmount": "originalAmount",
            "priority": 1,
            "isDeductibleFrom": false,
            "creditAccount": "fees-revenue"
          }
        },
        "enable": false
      }'
    # -> {"id": "..."}  this is the Midaz package ID
    ```

    You can also create the packages on the Console [Fee Packages](/en/products/midaz/fees/console/managing-fee-packages) page.
  </Step>

  <Step title="Compare the fees of both engines">
    For each package, estimate the same transaction in plugin-fees and in Midaz. Compare the fee amounts. They must be equal. Use amounts at both ends of the range of the package, and one amount in the middle. For a package that carries the segment waivers from [Package scope](#package-scope), use a sender outside those segments. Midaz exempts a sender in them by design, and plugin-fees does not.

    ```bash theme={null}
    TRANSACTION='{
      "send": {
        "asset": "BRL",
        "value": "1500.00",
        "source": { "from": [{ "accountAlias": "@sender", "amount": { "asset": "BRL", "value": "1500.00" } }] },
        "distribute": { "to": [{ "accountAlias": "@external/BRL", "amount": { "asset": "BRL", "value": "1500.00" } }] }
      }
    }'

    # plugin-fees: the plugin-fees package ID, and the ledger in the body
    curl -s -X POST "$FEES_BASE_URL/v1/estimates" \
      -H "Authorization: Bearer $FEES_BEARER_TOKEN" \
      -H "X-Organization-Id: $MIDAZ_ORGANIZATION_ID" \
      -H 'Content-Type: application/json' \
      -d '{ "packageId": "0198f1a2-6c3d-7e4f-8a9b-1c2d3e4f5a6b", "ledgerId": "'"$MIDAZ_LEDGER_ID"'", "transaction": '"$TRANSACTION"' }'

    # Midaz: the Midaz package ID, and the ledger in the URL
    curl -s -X POST "$MIDAZ_LEDGER_URL/v2/organizations/$MIDAZ_ORGANIZATION_ID/ledgers/$MIDAZ_LEDGER_ID/estimates" \
      -H "Authorization: Bearer $MIDAZ_BEARER_TOKEN" \
      -H 'Content-Type: application/json' \
      -d '{ "packageId": "0199b7d2-4f5e-7a6b-8c9d-2e3f4a5b6c7d", "transaction": '"$TRANSACTION"' }'
    ```

    In the Midaz answer, each fee entry has `"feeLeg": "true"` in its `metadata`. You can also use the Console [Fee Calculator](/en/products/midaz/fees/console/managing-fee-calculations).

    An estimate calculates the one package that you name. It does not check `transactionRoute` or `segmentId`, so it does not show which package a transfer gets. The next step tests that.
  </Step>

  <Step title="Rehearse the selection in homologation">
    Run steps 1 to 4 in homologation first, with the same packages. Then test which package each transfer gets, before you switch production:

    1. Under `legacy`, call `POST /v1/transfers/initiate` for each case: P2P and TED OUT, from a sender in each segment of the ledger and from a sender with no segment, with amounts at both ends of the range of each package. Record `feeAmount` and `packageAppliedId` from each answer. Do not process these initiations. An initiation posts nothing to Midaz. It writes a `payment_initiations` row that expires at `expiresAt`, keeps a duplicate fingerprint for 300 seconds by default, and sends a `payment_initiation.created` event to your webhook and event consumers. A TED OUT initiation outside the operating window answers `422 BTF-0010`.
    2. Switch homologation to `native`, as in the next step.
    3. Repeat the same initiations. Compare each `feeAmount`. `packageAppliedId` now names the Midaz package. Check that it is the package that replaces the plugin-fees one. An identical initiation inside that duplicate window answers `409 BTF-0012`, so wait for it to pass.
    4. TED IN has no initiation. Under each mode, receive one small TED IN for a recipient in each segment and for a recipient with no segment. Compare the fees.

    Expect a difference only where [Package scope](#package-scope) says the engines cannot match, and only the one you chose there. Fix every other difference in the Midaz packages, and repeat the cases.
  </Step>

  <Step title="Switch Bank Transfer to native">
    Before the switch, run the P2P and TED OUT initiation cases of step 5 in production, under `legacy`. Use test accounts of your own, one in each segment and one with no segment. Record each `feeAmount`. Step 5 says what an initiation writes.

    1. Give the Midaz credentials of Bank Transfer the `packages` (`get`), `estimates` (`post`) and `organizations` (`get`) permissions.
    2. Enable each Midaz package that you created in step 3. Send `"enable": true` with [Update a Package](/en/reference/products/midaz/v2/update-package). No initiation tests a TED IN, so first compare the `transactionRoute` of each TED IN package with the **Transaction route** of the `TED_IN` binding of its ledger, on the Console [Accounting Routes](/en/interfaces/ted-jd/console/bt-accounting-routes) page.
    3. Set `MIDAZ_FEE_MODE=native`.
    4. Restart every Bank Transfer pod.

    Right after the restart, run the same cases again, with the same accounts. Compare each `feeAmount`, and check each `packageAppliedId`. Expect a difference only where [Package scope](#package-scope) says the engines cannot match, and only the one you chose there. An identical initiation inside the duplicate window answers `409 BTF-0012`, so wait for the window to pass between the two runs. An initiation posts nothing to Midaz, so these cases show a wrong route or segment ID without moving money.

    If another difference shows, [roll back](#roll-back). Correct the fees or the amount range of a package with Update a Package. Update a Package cannot change `transactionRoute` or `segmentId`, so for a wrong route or segment, create a corrected package with `enable: false`, and delete the wrong one only after the native transfers that used it have finished. This query lists the transfers created in `native` mode since the restart began. Check the fee of each one by hand.

    ```sql theme={null}
    SELECT id, type, status, created_at FROM transfers
     WHERE ledger_fee_mode = 'native'
       AND created_at >= '2026-10-09 14:00:00-03';  -- just before the restart began
    ```

    New transfers now use Midaz fees. A P2P transfer or TED OUT initiated before the restart, and a TED IN received before it, keep the `legacy` mode until they finish.
  </Step>

  <Step title="Check real transfers">
    Make a small P2P transfer or TED OUT. Check the fee that Bank Transfer shows at initiation. Check the fee that it records on the transfer. Both must match the fee that plugin-fees charged before, except for a difference that you chose in [Package scope](#package-scope).

    Check the first TED IN that arrives after the switch the same way.

    In Midaz, the metadata of each transaction has `packageAppliedID`. It is the ID of the Midaz package that charged the fee.
  </Step>

  <Step title="Retire plugin-fees">
    Keep plugin-fees running while this query returns rows. In multi-tenant mode, run it on the Bank Transfer database of each tenant.

    ```sql theme={null}
    SELECT id, status FROM transfers
     WHERE type = 'TED_IN' AND ledger_fee_mode = 'legacy'
       AND status NOT IN ('COMPLETED', 'REJECTED', 'FAILED', 'CANCELLED');
    ```

    These are TED INs received before the switch. When Bank Transfer resumes one, it asks plugin-fees for its fee. If plugin-fees is stopped, Bank Transfer credits the recipient with no fee. When `fees.fail_closed_default` is `true`, it returns the TED to the sender instead, unless Bank Transfer cannot read that policy.

    Then back up the plugin-fees MongoDB database, and stop plugin-fees. After that, you cannot [roll back](#roll-back).

    Keep `MIDAZ_FEE_MODE=native`. Under `auto`, a pod that cannot read the Midaz version starts in `legacy` mode, and `legacy` needs plugin-fees.
  </Step>
</Steps>

## Roll back

***

You can return to plugin-fees while plugin-fees still runs with its packages unchanged:

1. Set `MIDAZ_FEE_MODE=legacy`.
2. Restart every Bank Transfer pod.

New transfers then use plugin-fees again. This needs `BTF_FEE_ENABLED=true` and the `FEES_*` variables still in place.

Transfers created in `native` mode keep that mode. Bank Transfer finishes them on the Midaz `/v2` API, and Midaz charges their fees when it posts them. Keep the Midaz packages enabled until every native transfer has finished.

Keep Bank Transfer on 3.1.0 or later while any transfer created in `native` mode is still open. An earlier version settles that transfer on `/v1`, so its fee is not charged or not recorded.

## Console

***

The Fees Engine pages of the Console, [Fee Packages](/en/products/midaz/fees/console/managing-fee-packages) and [Fee Calculator](/en/products/midaz/fees/console/managing-fee-calculations), work on Midaz fee packages only. They do not read plugin-fees.

## See also

***

* [What is Fees Engine?](/en/products/midaz/fees/fees-engine-overview)
* [Using Fees Engine](/en/products/midaz/fees/using-fee-engine)
* [Estimate transaction fees](/en/reference/products/midaz/v2/estimate-fee-calculation)
* [Bank Transfer configuration](/en/interfaces/ted-jd/ted-configuration)
* [Bank Transfer environment variables](/en/interfaces/ted-jd/ted-environment-variables)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.