Stage 1 of the two-stage withdrawal flow — returns an EIP-712 typed-data envelope for the end-user to sign in their wallet.
POST /v2/portfolios/{portfolioId}/withdraw) accepts the
signed envelope and dispatches the actual onchain transfer.
Wallet addresses are exchanged as
CAIP-10 account identifiers
throughout the v2 API. recipientAccountId uses the chain-bound EVM form
eip155:<chainId>:<address> — a withdrawal always goes to one address on one
specific chain. Only EVM recipients are supported today.
x-api-key header (required)portfolios:withdrawauthorizationId (= the signed message.nonce) is the
idempotency anchor for the matching POST .../withdraw call. Stage 1
returns the full EIP-712 typed-data object at data.typedData — pass it
directly to signTypedData(data.typedData) in the user’s wallet. On stage
2 the integrator echoes back typedData.message as body.message
alongside the signature. The server reconstructs the EIP-712 hash and
verifies the signature recovers to the portfolio owner.
The authorization is valid for 10 minutes. If the user takes longer to
sign — or any balance check fails at stage 2 — the integrator must restart
the flow by calling this endpoint again.
assetId must be on the same chain as recipientAccountId. A
single authorization withdraws a set of assets from one chain to one
recipient on that chain. Multi-chain withdrawals require separate
authorizations per chain.
domain binds the signature to two things:
chainId — the EVM chain the assets and recipient live on. Required for
ERC-1271 smart-wallet verification, which re-hashes the typed-data using
the wallet’s own chainId.verifyingContract — the vault being debited on that chain. Scopes the
signature to a specific vault deployment.domain, types, primaryType, and message
verbatim to signTypedData in the user’s wallet.
recipientAccountId as a CAIP-10 string like
eip155:1:0x4444… — chain and address are visible together. They also see
portfolioId (ULID), assets (CAIP-19 asset ids + raw atomic amounts),
nonce, and expiresAt. Integrators SHOULD surface a decoded summary in
their own UI (asset symbols, human-readable amounts) so users can verify
the recipient and amounts before approving.
Common error responses:
400 API_210 INSUFFICIENT_BALANCE — live balance below amountRaw for at
least one asset.400 API_211 INVALID_RECIPIENT — zero address, vault self-transfer, or
non-EVM namespace.400 API_212 DUPLICATE_WITHDRAW_ASSET — two or more assets in assets
share the same assetId.400 API_213 WITHDRAWAL_CHAIN_MISMATCH — at least one asset is on a
different chain than recipientAccountId.400 API_215 PORTFOLIO_HAS_NO_VAULT_ON_CHAIN — portfolio has no vault
deployed on the recipient’s chain.400 when the request body is otherwise invalid (schema errors).401 when x-api-key header is missing or the key is invalid.403 when the API key lacks the portfolios:withdraw scope.404 API_200 PORTFOLIO_NOT_FOUND — portfolioId doesn’t exist or belongs
to a different tenant.500 on unexpected server errors.