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. |
| 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.
Trading-to-Trading transfers
InternalTransferService.CreateInternalTransfer (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, ordestinationSmartAccountAddress. - Omit
subaccountIdto send from the Main Account, or from the bound Subaccount for a Subaccount-scoped API key. - Send
amountas a REST decimal string such as"0.5", oramountE18as a ConnectRPC U128 at 18 decimals. - Use an
idempotencyKeyof 1 to 128 characters, unique per logical transfer.
{
"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.
destination with it before reusing it.Who may transfer
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.
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.
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.
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.
Idempotency and retries
idempotencyKey before the first attempt and store it with the transfer intent.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. The full list is under Internal transfers in the error codes.
To confirm the movement, find the INTERNAL_TRANSFER legs with the returned transferId in ListTransfers (GET /v1/transfers) for the source and destination
accounts, or follow them on the Ledger transfers stream. Internal transfers
are also subject to rate limits.
Field schemas are in the API reference.