# Internal transfers

Move balances between Main Accounts, Subaccounts, Funding, and Trading with InternalTransferService and the related paths, including whitelists, MFA, idempotency, and statuses.

An internal transfer moves an Asset between Polyester accounts or balance areas without leaving Polyester. No external Network, deposit address, or Zipper route is involved. Which method you use depends on where the funds are and where they go.

| From    | To      | How                                                                                                                                                                |
| ------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Trading | Trading | `CreateInternalTransfer`, described on this page.                                                                                                                  |
| Trading | Funding | Trading withdrawal with action `TO_FUNDING`. See [Withdraw from Trading](https://testnet.polyester.com/docs/developer-docs/funds-transfers/withdraw-from-trading). |
| Funding | Trading | On-chain TradingGateway deposit from the account's smart account.                                                                                                  |
| Funding | Funding | On-chain FundingAccount transfer from the account's smart account.                                                                                                 |

The Main Account and every Subaccount each have their own Funding and Trading balances, so moving funds between your own accounts uses the same paths. The Funding paths are on-chain account actions, not REST or ConnectRPC mutations. See [Funds transfer overview](https://testnet.polyester.com/docs/developer-docs/funds-transfers/overview).

## Trading-to-Trading transfers

[`InternalTransferService.CreateInternalTransfer`](https://testnet.polyester.com/docs/api-docs/connect/transfer.v1.InternalTransferService/CreateInternalTransfer) ([`POST /v1/trading/internal-transfers`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/trading/internal-transfers)) moves an available Trading balance from one account to another account's Trading balance. The destination can be your own Main Account or Subaccount, or another user's.

The request fields are in the API reference. When you build one:

- Name the destination exactly once: `destinationAccountId`, `destinationSubaccountId`, or `destinationSmartAccountAddress`.
- Omit `subaccountId` to send from the Main Account, or from the bound Subaccount for a Subaccount-scoped API key.
- Send `amount` as a REST decimal string such as `"0.5"`, or `amountE18` as a ConnectRPC U128 at 18 decimals.
- Use an `idempotencyKey` of 1 to 128 characters, unique per logical transfer.

```json
{
	"destinationSmartAccountAddress": "0x2222222222222222222222222222222222222222",
	"assetId": 1,
	"amount": "0.5",
	"idempotencyKey": "transfer-2026-07-16-0001"
}
```

A successful response returns `requestId`, `transferId`, `acceptedAtTsNs`, the echoed Asset and amount, the server-resolved `destination`, and `status`.

> **Check the destination before you send**
>
> A completed transfer cannot be canceled or reversed. Resolve another user's account from information they gave you, and compare the resolved `destination` with it before reusing it.

### Who may transfer

## Session

From the Main Account, you need a session for it. From a Subaccount, you need the `OWNER`, `ADMIN`, or `TREASURY` role, and the Subaccount policy must allow `INTERNAL_TRANSFER`.

## API key

The key's API key policy must allow `INTERNAL_TRANSFER`. A Main Account key sends only from the Main Account. A Subaccount-scoped key sends only from its bound Subaccount, and that Subaccount's policy must also allow `INTERNAL_TRANSFER`.

A disabled source Subaccount fails with `SOURCE_ACCOUNT_INACTIVE`, and a disabled destination with `DESTINATION_INACTIVE`. Sending to the same account fails with `SAME_SOURCE_DESTINATION`. See [Subaccounts and roles](https://testnet.polyester.com/docs/developer-docs/account/subaccounts-and-roles).

## Whitelists

A Trading-to-Trading transfer checks the source account's internal whitelist, which lists destination smart-account addresses. When the internal requirement is enabled, a transfer succeeds only when the destination's smart account is listed; otherwise it fails with `DESTINATION_NOT_WHITELISTED`. This includes your own Subaccounts. When the requirement is disabled, any valid destination is accepted.

Saving an address-book entry does not whitelist it. Both whitelist types, how to change them on-chain with GuardSigner approval, and how to read their state are in [Whitelists](https://testnet.polyester.com/docs/developer-docs/funds-transfers/whitelists).

## MFA and step-up

API key requests never use interactive MFA. Session requests need one of two proofs:

| Session transfer                                                                                                 | Proof                                   |
| ---------------------------------------------------------------------------------------------------------------- | --------------------------------------- |
| The Main Account owner moves funds within the same ownership tree, such as from a Subaccount to the Main Account | Recent MFA session                      |
| The Main Account owner sends to a destination explicitly listed on the internal whitelist                        | Recent MFA session                      |
| Every other session transfer, including any by a delegated member                                                | Fresh step-up token in `X-Auth-Step-Up` |

A missing recent MFA session fails with `AUTH_MFA_ELEVATION_REQUIRED`; a missing step-up fails with `AUTH_STEP_UP_REQUIRED`. A retry of an already-accepted request needs no new proof. See [Multi-factor authentication](https://testnet.polyester.com/docs/developer-docs/authentication-security/multi-factor-authentication).

## Idempotency and retries

1. Generate the `idempotencyKey` before the first attempt and store it with the transfer intent.
2. On a timeout or network error, retry with the same key and identical fields. For an API key, sign the retry with a fresh timestamp.
3. A retry of an accepted transfer returns the original response. Reusing the key with different fields fails with `IDEMPOTENCY_CONFLICT`.

Once Polyester has begun a transfer, it completes it even if your client disconnects. Retrying with the same key is always safe; sending a new key creates a second transfer.

## Statuses

| `status`                            | Meaning                                      |
| ----------------------------------- | -------------------------------------------- |
| `INTERNAL_TRANSFER_STATUS_ACCEPTED` | Durably accepted with a `transferId`.        |
| `INTERNAL_TRANSFER_STATUS_REJECTED` | Rejected before durable acceptance.          |
| `INTERNAL_TRANSFER_STATUS_FAILED`   | Failed terminally before durable acceptance. |

REST writes these values without the prefix: `ACCEPTED`, `REJECTED`, and `FAILED`.

Most refusals arrive as errors rather than a response. `INSUFFICIENT_FUNDS` means the source's available Trading balance is too low; check `available` in [Balances and holds](https://testnet.polyester.com/docs/developer-docs/account/balances-and-holds). The full list is under [Internal transfers](https://testnet.polyester.com/docs/developer-docs/error-handling/error-codes#internal-transfers) in the error codes.

To confirm the movement, find the `INTERNAL_TRANSFER` legs with the returned `transferId` in [`ListTransfers`](https://testnet.polyester.com/docs/api-docs/connect/ledger.read.v1.LedgerReadService/ListTransfers) ([`GET /v1/transfers`](https://testnet.polyester.com/docs/api-docs/rest/GET/v1/transfers)) for the source and destination accounts, or follow them on the [Ledger transfers](https://testnet.polyester.com/docs/api-docs/streams/private-ledger-transfers) stream. Internal transfers are also subject to [rate limits](https://testnet.polyester.com/docs/developer-docs/shared-concepts/rate-limits).

Field schemas are in the [API reference](https://testnet.polyester.com/docs/api-docs).
