Overview
The withdrawal flow is:- User enters an amount and destination wallet.
- Your backend creates a withdrawal quote.
- You sign the transaction via
handleSign. - The backend broadcasts the transaction.
Choose an integration
Al integrations use the same handleSign callback. See Signing for details
Packages
Installation
React (component or headless hooks):react and react-dom (>=18.2.0 <20).
Core SDK only (no React):
withdraw-react you rarely need to import from withdraw-core directly.
Core concepts
The withdrawal flow (identical across all three integrations):
Drop-in component
<MoonPayCommerceWithdraw> renders the full withdrawal UI — currency/chain selection, amount entry, quote summary, signing, submission, and status — for you. You provide the config, a wallet-signing callback, and optional lifecycle callbacks.
Quickstart
Remember to import the CSS (
@heliofi/withdraw-react/dist/withdraw-react.css) — it’s only required for the drop-in component, not for headless usage.Props
Required only when
connectedWalletAddress is provided.
Display modes
inline(default) — the widget renders directly where you mount it.button— the widget renders a trigger; clicking it opens the flow in a modal. Passtriggera string for a simple label, or a render prop to fully control the trigger UI.
Headless hooks
The headless hooks give you all the data-fetching and API plumbing — routes, balances, quotes, submit, status polling — while you own 100% of the UI. There is no state machine, no events, no step management: the hooks are thin wrappers around the core SDK’s data-fetching, and you call them imperatively and render whatever you like.Every hook below must be rendered inside
<WithdrawProvider>. No CSS import is required for headless usage.Provider setup
Wrap your UI inWithdrawProvider. It’s the only provider you need — it owns the SDK instance, an isolated Redux store (you never touch Redux), and the shared config/callbacks.
WithdrawProvider takes the same props as the drop-in component (minus the UI-only props debug / display / trigger), plus children.
The flow
Hook reference
Every data hook returns{ isLoading, isError, error } alongside the values noted below.
Building a currency/chain selector — prefer
useCurrencyOptions() + useToCurrencyIdForBlockchain() over raw route data. Use useGetRoutes() directly only if you need custom route metadata (e.g. a bespoke chain picker or minimum-amount display).
Key behaviours
sourceCurrencyIdis required for quoting — read it fromuseGetWithdraw().data.fromCurrencies[0].id.- Re-quoting. When the user edits amount/recipient, call
useUpdateWithdrawalDetail()with the same params pluswithdrawalDetailId. If the destination currency changes, create a fresh quote withuseCreateWithdrawalDetail()instead. - Signing. Branch on
quote.prepared.kindand forward the right payload tohandleSign— see Signing. (The drop-in component does this mapping for you; in headless mode you do it yourself.) - Status.
usePollStatus(id)auto-polls every 3s. UseTERMINAL_STATES.has(status.state)to detect completion/failure and stop reacting.
Full example
Wrap
trigger calls in try/catch (or read the hooks’ isError / error) for real error handling — omitted above for brevity.Signing (handleSign)
Signing is the one piece you always implement, regardless of integration style. The SDK never touches private keys — it hands you a prepared transaction and expects a signature string back.
The contract
A quote’sprepared object tells you what to sign. Its kind (a SignedTransactionKind) maps to a handleSign payload kind like this:
Drop-in vs headless. The drop-in component performs this mapping internally, so you just branch inside
handleSign on payload.kind. In headless mode you also choose the payload from prepared.kind (see toSigningPayload in the headless example).Simple flow — EVM typed data (recommended starting point)
Most EVM integrations (e.g. Polymarket, Hyperliquid) use thetyped_data path. Using wagmi:
Signing reference — other modalities
EVM raw transaction (kind: 'transaction', EVM) — send a transaction object:
kind: 'transaction', base64) — data is a base64 VersionedTransaction. Deserialize, sign with the wallet, and return the base64 signed tx; the backend broadcasts it. Using @solana/wallet-adapter-react:
kind: 'user_op', ERC-4337 + EIP-7702) — the account-abstraction path. data is { userOp, entryPoint, chain, authorization }. Recompute the userOp hash with the canonical entryPoint, sign it, sign the EIP-7702 authorization, and return both as a JSON string. Using viem:
The core SDK
@heliofi/withdraw-core is a framework-agnostic TypeScript SDK — no React, no global state. Use it directly when you’re not on React, or when you want lower-level control. It uses the global fetch (override via config for Node/custom transports).
Create an SDK instance
Quickstart — the raw quote → sign → submit loop
Method reference
Every method accepts an optional trailingAbortSignal and throws WithdrawalApiError on a non-2xx response.
createWithdrawalDetail params (CreateWithdrawalDetailParams): withdrawalConfigId, ownerAddress, recipient, amount (minimal units, string), toCurrencyId, sourceCurrencyId, and optional customerId (string, ≤255 chars). updateWithdrawalDetail takes the same minus withdrawalConfigId and customerId — customerId is set once at quote creation and is not accepted on re-quote. submitWithdrawal params (SubmitWithdrawalParams): withdrawalDetailId, token, signedTx.
Helpers
Types & error codes
Key types
SignedTransactionKind (prepared transaction kinds)
POLYMARKET_WALLET · HYPERCORE · RAW · USER_OP — see the signing mapping table.
WithdrawalState — terminal states
TERMINAL_STATES contains: COMPLETED, FAILED, FAILED_EXPIRED, EXPIRED, REFUNDED_BY_PROVIDER. Use TERMINAL_STATES.has(state) to stop polling.
Error codes
WithdrawErrorCode (React layer — surfaced via onError / hook error): INVALID_PROPS, UNSUPPORTED_VERSION, LOAD_FAILED, MISSING_ID, QUOTE_FAILED, SIGN_FAILED, SUBMIT_FAILED, STATUS_FAILED, CURRENCY_OR_BLOCKCHAIN_SELECTION_FAILED.
WithdrawalErrorCode (core SDK / backend — on WithdrawalApiError): WITHDRAW_TX_INVALID_SIGNATURE, WITHDRAW_TX_BLOCKHASH_EXPIRED, WITHDRAW_TX_NONCE_TOO_LOW, WITHDRAW_TX_USEROP_EXPIRED, WITHDRAW_TX_UNDERPRICED, WITHDRAW_BUNDLER_REJECTED, WITHDRAW_TX_BROADCAST_TIMEOUT, WITHDRAW_TX_ALREADY_KNOWN, WITHDRAW_QUOTE_EXPIRED, WITHDRAW_SANCTIONS_BLOCKED, WITHDRAW_NO_ROUTE, WITHDRAW_NOT_FOUND, WITHDRAW_INSUFFICIENT_BALANCE, WITHDRAW_DETAILS_NOT_UPDATABLE, INVALID_WALLET_ADDRESS, BALANCE_FETCH_FAILED, UNKNOWN.