Skip to main content
The Withdraw SDK lets platforms enable crypto withdrawals from a user’s platform balance to their own wallet. This guide covers the React component, headless React hooks, and framework-agnostic core SDK. For setup (API keys, withdrawal configs) and the HTTP API, see Withdrawals.

Overview

The withdrawal flow is:
  1. User enters an amount and destination wallet.
  2. Your backend creates a withdrawal quote.
  3. You sign the transaction via handleSign.
  4. 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):
Peer dependencies: react and react-dom (>=18.2.0 <20). Core SDK only (no React):
The React package re-exports every type and enum from the core SDK, so when you use 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

Breaking change (withdraw-react): buttonText and renderButton are removed and replaced by a single trigger prop. Pass a string where you used buttonText, or a render prop where you used renderButton. Exported type WithdrawRenderButton is now WithdrawTriggerRender (WithdrawTriggerRenderProps unchanged).
  • inline (default) — the widget renders directly where you mount it.
  • button — the widget renders a trigger; clicking it opens the flow in a modal. Pass trigger a 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 in WithdrawProvider. 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

You decide when to call each step and what to render in between. Form state (amount, recipient, currency/chain selection) is yours to manage.

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

  • sourceCurrencyId is required for quoting — read it from useGetWithdraw().data.fromCurrencies[0].id.
  • Re-quoting. When the user edits amount/recipient, call useUpdateWithdrawalDetail() with the same params plus withdrawalDetailId. If the destination currency changes, create a fresh quote with useCreateWithdrawalDetail() instead.
  • Signing. Branch on quote.prepared.kind and forward the right payload to handleSign — 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. Use TERMINAL_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’s prepared 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).
Most EVM integrations (e.g. Polymarket, Hyperliquid) use the typed_data path. Using wagmi:
That’s a complete, production-shaped signer for the common case. The rest of this section covers the other modalities — add them only for the chains you support.

Signing reference — other modalities

EVM raw transaction (kind: 'transaction', EVM) — send a transaction object:
Solana (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:
EVM UserOp (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 userOp hash must match the backend’s server-side hash exactly, or submit will fail with 401. Keep entryPointVersion and the gas-field types aligned with the values above.

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 trailing AbortSignal 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 customerIdcustomerId 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.