> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Withdrawals SDK

> Integrate crypto withdrawals with @heliofi/withdraw-react and @heliofi/withdraw-core

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**](/docs/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

| **If you want to**                  | **Use**               |
| :---------------------------------- | :-------------------- |
| Add a ready-made React UI           | **Drop-in component** |
| Build a custom React UI             | **Headless hooks**    |
| Use another framework or vanilla JS | **Core SDK**          |

Al integrations use the same handleSign callback. See [Signing](#signing-handlesign) for details

### Packages

| Package                       | Description                                     |
| ----------------------------- | ----------------------------------------------- |
| **`@heliofi/withdraw-react`** | Drop-in component and headless hooks for React. |
| **`@heliofi/withdraw-core`**  | Framework-agnostic JavaScript/TypeScript SDK.   |

## Installation

**React (component or headless hooks):**

<CodeGroup>
  ```bash Shell theme={null}
  yarn add @heliofi/withdraw-react
  # or npm install @heliofi/withdraw-react
  # or pnpm add @heliofi/withdraw-react
  ```
</CodeGroup>

Peer dependencies: `react` and `react-dom` (`>=18.2.0 <20`).

**Core SDK only (no React):**

<CodeGroup>
  ```bash Shell theme={null}
  npm install @heliofi/withdraw-core
  ```
</CodeGroup>

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

| Term                                      | Meaning                                                                                                                                                                                                                      |
| ----------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Withdrawal config**                     | The merchant's server-side configuration (`withdrawalConfigId`). Determines the source chain/token, available routes, and signing strategy. Create one via [Create Withdrawal Config](/reference/withdrawal-configs/create). |
| **Balance**                               | The user's spendable balance on the *merchant site* (the source of funds).                                                                                                                                                   |
| **Source wallet** (`sourceWalletAddress`) | The merchant's funding/deposit wallet that actually holds and sends the crypto. Provided by your app, not the end user.                                                                                                      |
| **Destination** (`recipient`)             | The user's crypto wallet address that receives the funds.                                                                                                                                                                    |
| **Quote / withdrawal detail**             | A priced withdrawal, created by the [prepare](/reference/withdraw/prepare) call. Returns a `withdrawalDetailId`, a `prepared` transaction to sign, a fee `summary`, and a JWT `token` required for submit.                   |
| **`handleSign`**                          | Your callback. Receives the prepared transaction and returns the merchant's signature. This is where your wallet integration lives.                                                                                          |
| **Amounts**                               | All amounts are in **minimal (atomic) units** as strings — e.g. `"1000000"` for 1 USDC (6 decimals). Read `decimals` from the currency to format for display.                                                                |

**The withdrawal flow (identical across all three integrations):**

```text theme={null}
Load config  →  Get routes  →  Get balance  →  Quote (prepare)  →  Sign  →  Submit  →  Poll status
```

## 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

```tsx theme={null}
import { MoonPayCommerceWithdraw } from '@heliofi/withdraw-react';
import '@heliofi/withdraw-react/dist/withdraw-react.css';

export const Withdraw = () => (
  <MoonPayCommerceWithdraw
    withdrawalConfigId="wc_..."
    sourceWalletAddress="0xMerchantWallet"
    handleSign={async (payload) => {
      // Simplest path — EIP-712 typed data. See "Signing" for all payload kinds.
      return walletClient.signTypedData(payload.data);
    }}
    onWithdrawalComplete={(result) => console.log('Done', result)}
    onError={(error) => console.error('Failed', error.code, error.message)}
  />
);
```

<Info>
  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.
</Info>

### Props

| Prop                        | Type                                           | Required   | Description                                                                                                                                                     |                                                                                                                                                                                              |   |
| --------------------------- | ---------------------------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - |
| `withdrawalConfigId`        | `string`                                       | ✅          | ID of the merchant's withdrawal configuration.                                                                                                                  |                                                                                                                                                                                              |   |
| `sourceWalletAddress`       | `string`                                       | ✅          | Merchant's funding wallet (source of funds).                                                                                                                    |                                                                                                                                                                                              |   |
| `handleSign`                | `(payload: SigningPayload) => Promise<string>` | ✅          | Signs the prepared withdrawal, returns the signature. See [Signing](#signing-handlesign).                                                                       |                                                                                                                                                                                              |   |
| `onWithdrawalComplete`      | `(result: WithdrawResult) => void`             | —          | Fired once per completed withdrawal. Not terminal — the widget stays mounted and can run another.                                                               |                                                                                                                                                                                              |   |
| `onError`                   | `(error: WithdrawError) => void`               | —          | Fired on any error (`error.code`, `error.message`).                                                                                                             |                                                                                                                                                                                              |   |
| `onReady`                   | `() => void`                                   | —          | Fired once the flow has loaded and is ready.                                                                                                                    |                                                                                                                                                                                              |   |
| `connectedWalletAddress`    | `string`                                       | —          | Connected wallet address, for balance display.                                                                                                                  |                                                                                                                                                                                              |   |
| `connectedWalletEngineType` | `BlockchainEngineType`                         | —¹         | Engine type of the connected wallet (`'EVM'`, `'SOL'`, …).                                                                                                      |                                                                                                                                                                                              |   |
| `baseUrl`                   | `string`                                       | —          | API base URL. Defaults to `https://api.hel.io/v1`.                                                                                                              |                                                                                                                                                                                              |   |
| `customerId`                | `string`                                       | —          | Optional merchant identifier for the end user, attached to the withdrawal quote (sent on the create `prepare` call). Max 255 characters (enforced server-side). |                                                                                                                                                                                              |   |
| `display`                   | \`'inline'                                     | 'button'\` | —                                                                                                                                                               | Render inline (default) or behind a trigger button/modal.                                                                                                                                    |   |
| `trigger`                   | \`string                                       | null)\`    | —                                                                                                                                                               | Custom trigger for `display="button"` mode. A **string** relabels the standard button; a **render prop** supplies a fully custom trigger. Named `trigger` to match `@heliofi/deposit-react`. |   |
| `debug`                     | `boolean`                                      | —          | Show the built-in debug panel.                                                                                                                                  |                                                                                                                                                                                              |   |

Required only when `connectedWalletAddress` is provided.

### Display modes

<Warning>
  **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).
</Warning>

* **`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.

```tsx theme={null}
{/* String form — relabel the default button (replaces buttonText) */}
<MoonPayCommerceWithdraw
  withdrawalConfigId="wc_..."
  sourceWalletAddress="0xMerchantWallet"
  handleSign={handleSign}
  display="button"
  trigger="Withdraw funds"
/>

{/* Render-prop form — fully custom trigger (replaces renderButton) */}
<MoonPayCommerceWithdraw
  withdrawalConfigId="wc_..."
  sourceWalletAddress="0xMerchantWallet"
  handleSign={handleSign}
  display="button"
  trigger={({ open, close, isOpen }) => (
    <MyButton onClick={isOpen ? close : open}>
      {isOpen ? 'Close' : 'Withdraw funds'}
    </MyButton>
  )}
/>
```

## 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.

<Info>
  Every hook below must be rendered inside `<WithdrawProvider>`. No CSS import is required for headless usage.
</Info>

### 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.

```tsx theme={null}
import { WithdrawProvider } from '@heliofi/withdraw-react';

const App = () => (
  <WithdrawProvider
    withdrawalConfigId="wc_..."
    sourceWalletAddress="0xMerchantWallet"
    handleSign={handleSign} // your wallet signer — see Signing
    onWithdrawalComplete={(result) => console.log('Done', result)}
    onError={(error) => console.error('Failed', error.code, error.message)}
  >
    <YourCustomUI />
  </WithdrawProvider>
);
```

`WithdrawProvider` takes the same props as the drop-in component (minus the UI-only props `debug` / `display` / `trigger`), plus `children`.

### The flow

```text theme={null}
useGetWithdraw()      → load config (source currencies)
useGetRoutes()        → available destination currencies / chains
useGetBalances()      → user's spendable balance
useCreateWithdrawalDetail()  → get a quote
useUpdateWithdrawalDetail()  → re-quote after edits (optional)
handleSign()          → sign the prepared tx (from context)
useSubmitWithdraw()   → submit the signature
usePollStatus()       → poll until terminal
```

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.

| Hook                                                               | Signature → returns                                                                           | Purpose                                                                              |                                                                                                    |
| ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------- |
| `useWithdrawContext()`                                             | → `{ withdrawalConfigId, sourceWalletAddress, handleSign, onWithdrawalComplete, onError, … }` | Read config & callbacks passed to the provider.                                      |                                                                                                    |
| `useGetWithdraw()`                                                 | → `{ data: WithdrawalConfig }`                                                                | Fetch the withdrawal config. `data.fromCurrencies[0].id` is your `sourceCurrencyId`. |                                                                                                    |
| `useGetRoutes()`                                                   | → `{ availableRoutes, routesById }`                                                           | Fetch available destination routes (raw).                                            |                                                                                                    |
| `useGetBalances(walletAddress)`                                    | → `{ data: BalanceResponse }`                                                                 | Fetch the user's balance for a wallet.                                               |                                                                                                    |
| `useCurrencyOptions()`                                             | → `{ options: { value, label }[] }`                                                           | Deduplicated currency options for a selector (calls `useGetRoutes` internally).      |                                                                                                    |
| `useToCurrencyIdForBlockchain(currencySymbol?, blockchainSymbol?)` | → \`string                                                                                    | undefined\`                                                                          | Resolve a currency + chain pair to a `toCurrencyId`. Returns `undefined` if either arg is missing. |
| `useCreateWithdrawalDetail()`                                      | `.trigger(params)` → `QuoteResponse`                                                          | Create a quote (`prepare`).                                                          |                                                                                                    |
| `useUpdateWithdrawalDetail()`                                      | `.trigger(params)` → `QuoteResponse`                                                          | Re-quote an existing withdrawal detail.                                              |                                                                                                    |
| `useSubmitWithdraw()`                                              | `.trigger(params)` → `SubmitResponse`                                                         | Submit the signed transaction.                                                       |                                                                                                    |
| `usePollStatus(withdrawalDetailId)`                                | → `{ data: StatusResponse }`                                                                  | Auto-polls status **every 3 seconds** once an id is set.                             |                                                                                                    |

**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](#signing-handlesign). (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

```tsx theme={null}
import { useState } from 'react';
import {
  useWithdrawContext,
  useGetWithdraw,
  useGetBalances,
  useCurrencyOptions,
  useToCurrencyIdForBlockchain,
  useCreateWithdrawalDetail,
  useUpdateWithdrawalDetail,
  useSubmitWithdraw,
  usePollStatus,
  TERMINAL_STATES,
  SignedTransactionKind,
  BlockchainSymbol,
} from '@heliofi/withdraw-react';
import type { QuoteResponse, SigningPayload } from '@heliofi/withdraw-react';

// Map a prepared transaction to the handleSign payload (see Signing).
const toSigningPayload = (prepared: QuoteResponse['prepared']): SigningPayload => {
  switch (prepared.kind) {
    case SignedTransactionKind.POLYMARKET_WALLET:
    case SignedTransactionKind.HYPERCORE:
      return { kind: 'typed_data', data: prepared.typedData };
    case SignedTransactionKind.RAW:
      return { kind: 'transaction', data: prepared.unsignedTx };
    case SignedTransactionKind.USER_OP:
      return {
        kind: 'user_op',
        data: {
          userOp: prepared.userOp,
          entryPoint: prepared.entryPoint,
          chain: prepared.chain,
          authorization: prepared.authorization,
        },
      };
  }
};

const YourCustomUI = () => {
  const { withdrawalConfigId, sourceWalletAddress, handleSign } = useWithdrawContext();

  const { data: config } = useGetWithdraw();
  const { data: balance } = useGetBalances(sourceWalletAddress);
  const { trigger: createQuote } = useCreateWithdrawalDetail();
  const { trigger: updateQuote } = useUpdateWithdrawalDetail();
  const { trigger: submit } = useSubmitWithdraw();

  // ── Form state (yours to manage) ──
  const [currencySymbol, setCurrencySymbol] = useState<string>();
  const [blockchainSymbol, setBlockchainSymbol] = useState<BlockchainSymbol>();
  const [amount, setAmount] = useState('');       // minimal units
  const [recipient, setRecipient] = useState('');
  const [quote, setQuote] = useState<QuoteResponse>();
  const [withdrawalDetailId, setWithdrawalDetailId] = useState<string>();

  // ── 1. Build the currency/blockchain selectors ──
  const { options: currencyOptions } = useCurrencyOptions();
  const toCurrencyId = useToCurrencyIdForBlockchain(currencySymbol, blockchainSymbol);
  const sourceCurrencyId = config?.fromCurrencies[0]?.id;

  // ── 2. Get a quote ──
  const handleGetQuote = async () => {
    if (!withdrawalConfigId || !sourceWalletAddress || !toCurrencyId || !sourceCurrencyId) return;
    const result = await createQuote({
      withdrawalConfigId,
      ownerAddress: sourceWalletAddress,
      recipient,
      amount,
      toCurrencyId,
      sourceCurrencyId,
    });
    setQuote(result);
  };

  // ── 2b. Re-quote after the user edits amount/recipient ──
  const handleUpdateQuote = async () => {
    if (!quote || !toCurrencyId || !sourceCurrencyId) return;
    const result = await updateQuote({
      withdrawalDetailId: quote.withdrawalDetailId,
      ownerAddress: sourceWalletAddress,
      recipient,
      amount,
      toCurrencyId,
      sourceCurrencyId,
    });
    setQuote(result);
  };

  // ── 3. Sign and submit ──
  const handleConfirmAndSign = async () => {
    if (!quote) return;
    const signature = await handleSign(toSigningPayload(quote.prepared));
    await submit({
      withdrawalDetailId: quote.withdrawalDetailId,
      token: quote.token,       // JWT from the quote — required for submit
      signedTx: signature,
    });
    setWithdrawalDetailId(quote.withdrawalDetailId);
  };

  // ── 4. Poll status (auto-polls every 3s once the id is set) ──
  const { data: status } = usePollStatus(withdrawalDetailId);
  const isTerminal = status && TERMINAL_STATES.has(status.state);

  return <div>{/* Your UI here */}</div>;
};
```

<Info>
  Wrap `trigger` calls in `try/catch` (or read the hooks' `isError` / `error`) for real error handling — omitted above for brevity.
</Info>

## 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.

```tsx theme={null}
type SigningPayload = {
  kind: 'transaction' | 'typed_data' | 'user_op';
  data: unknown;
};

const handleSign = async (payload: SigningPayload): Promise<string> => { /* ... */ };
```

### The contract

A quote's `prepared` object tells you what to sign. Its `kind` (a `SignedTransactionKind`) maps to a `handleSign` payload `kind` like this:

| `quote.prepared.kind` | → `handleSign` payload `kind` | `data` you pass                                                       | What your wallet does                    | Return                  |
| --------------------- | ----------------------------- | --------------------------------------------------------------------- | ---------------------------------------- | ----------------------- |
| `POLYMARKET_WALLET`   | `typed_data`                  | `prepared.typedData` (EIP-712)                                        | Sign typed data                          | Signature (hex string)  |
| `HYPERCORE`           | `typed_data`                  | `prepared.typedData` (EIP-712)                                        | Sign typed data                          | Signature (hex string)  |
| `RAW`                 | `transaction`                 | `prepared.unsignedTx` (string; Solana: base64 `VersionedTransaction`) | Deserialize → sign → re-serialize        | Base64 signed tx        |
| `USER_OP`             | `user_op`                     | `{ userOp, entryPoint, chain, authorization? }`                       | Sign userOpHash + EIP-7702 authorization | JSON string (see below) |

<Info>
  **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).
</Info>

### Simple flow — EVM typed data (recommended starting point)

Most EVM integrations (e.g. Polymarket, Hyperliquid) use the `typed_data` path. Using wagmi:

```tsx theme={null}
import { signTypedData } from '@wagmi/core';
import type { SigningPayload } from '@heliofi/withdraw-react';
import { wagmiConfig } from './wagmiConfig';

const handleSign = async (payload: SigningPayload): Promise<string> => {
  if (payload.kind === 'typed_data') {
    const { domain, types, primaryType, message } = payload.data as {
      domain: Record<string, unknown>;
      types: Record<string, Array<{ name: string; type: string }>>;
      primaryType: string;
      message: Record<string, unknown>;
    };

    // wagmi/viem derives EIP712Domain itself — strip it from the types.
    const { EIP712Domain: _omit, ...messageTypes } = types;

    return signTypedData(wagmiConfig, { domain, types: messageTypes, primaryType, message });
  }

  throw new Error(`Unsupported signing kind: ${payload.kind}`);
};
```

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:

```tsx theme={null}
import { sendTransaction } from '@wagmi/core';

if (payload.kind === 'transaction') {
  const tx = payload.data as { to: `0x${string}`; value?: bigint; data?: `0x${string}` };
  return sendTransaction(wagmiConfig, tx);
}
```

**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`:

```tsx theme={null}
import { VersionedTransaction } from '@solana/web3.js';

const base64ToBytes = (b64: string) => Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
const bytesToBase64 = (bytes: Uint8Array) => btoa(String.fromCharCode(...bytes));

const handleSign = async (payload: SigningPayload): Promise<string> => {
  if (payload.kind !== 'transaction') throw new Error(`Unsupported: ${payload.kind}`);
  const tx = VersionedTransaction.deserialize(base64ToBytes(payload.data as string));
  const signed = await signTransaction(tx); // from useWallet()
  return bytesToBase64(signed.serialize());
};
```

**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:

```tsx theme={null}
import { getUserOperationHash } from 'viem/account-abstraction';
import type { Address } from 'viem';

// aa-sdk hexlifies these gas fields; viem's hash needs them as bigints.
const BIGINT_FIELDS = ['nonce', 'callGasLimit', 'verificationGasLimit', 'preVerificationGas',
  'maxFeePerGas', 'maxPriorityFeePerGas', 'paymasterVerificationGasLimit', 'paymasterPostOpGasLimit'];

const toBigints = (op: Record<string, unknown>) => {
  const out = { ...op };
  for (const f of BIGINT_FIELDS) if (typeof out[f] === 'string') out[f] = BigInt(out[f] as string);
  return out;
};

const handleSign = async (payload: SigningPayload): Promise<string> => {
  if (payload.kind !== 'user_op') throw new Error(`Unsupported: ${payload.kind}`);
  const { userOp, entryPoint, authorization } = payload.data as {
    userOp: Record<string, unknown>;
    entryPoint: Address;
    authorization: { chainId: string; address: Address; nonce: string };
  };
  const chainId = Number(BigInt(authorization.chainId));
  const nonce = Number(BigInt(authorization.nonce));

  const userOpHash = getUserOperationHash({
    chainId,
    entryPointAddress: entryPoint,
    entryPointVersion: '0.7',
    userOperation: toBigints(userOp) as never,
  });

  const signature = await account.signMessage({ message: { raw: userOpHash } });
  const signed = await account.signAuthorization({ address: authorization.address, chainId, nonce });

  return JSON.stringify({
    signature,
    authorization: {
      chainId, address: authorization.address, nonce,
      r: signed.r, s: signed.s, yParity: signed.yParity,
    },
  });
};
```

<Warning>
  The userOp hash must match the backend's server-side hash exactly, or [submit](/reference/withdraw/submit) will fail with `401`. Keep `entryPointVersion` and the gas-field types aligned with the values above.
</Warning>

## 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

```tsx theme={null}
import { WithdrawalSDK } from '@heliofi/withdraw-core';

const sdk = WithdrawalSDK.create({
  baseUrl: 'https://api.hel.io/v1',
  // headers?: extra request headers
  // fetch?:   custom fetch (e.g. for Node < 18 or instrumentation)
});
```

### Quickstart — the raw quote → sign → submit loop

```tsx theme={null}
const configId = 'wc_...';

// 1. Load config → source currency
const config = await sdk.getWithdrawalConfig(configId);
const sourceCurrencyId = config.fromCurrencies[0].id;

// 2. See where the user can withdraw to
const { routes } = await sdk.getRoutes(configId);
const toCurrencyId = routes.find((r) => r.available)!.toCurrency.id;

// 3. (Optional) check balance
const { balances } = await sdk.getBalance(configId, ownerAddress);

// 4. Quote
const quote = await sdk.createWithdrawalDetail({
  withdrawalConfigId: configId,
  ownerAddress,
  recipient,
  amount: '1000000', // minimal units
  toCurrencyId,
  sourceCurrencyId,
});

// 5. Sign the prepared tx with your wallet (branch on quote.prepared.kind — see Signing)
const signedTx = await signWithYourWallet(quote.prepared);

// 6. Submit
const submit = await sdk.submitWithdrawal({
  withdrawalDetailId: quote.withdrawalDetailId,
  token: quote.token,
  signedTx,
});

// 7. Poll until terminal
import { TERMINAL_STATES } from '@heliofi/withdraw-core';
let status = await sdk.getWithdrawalDetail(quote.withdrawalDetailId);
while (!TERMINAL_STATES.has(status.state)) {
  await new Promise((r) => setTimeout(r, 3000));
  status = await sdk.getWithdrawalDetail(quote.withdrawalDetailId);
}
```

### Method reference

Every method accepts an optional trailing `AbortSignal` and throws `WithdrawalApiError` on a non-2xx response.

| Method                                              | HTTP endpoint                                                                       | Returns                                                                         |
| --------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `getWithdrawalConfig(configId, signal?)`            | [GET /withdrawal-configs/:id/public](/reference/withdrawal-configs/retrieve-public) | `WithdrawalConfig` — source currencies, exchange rates, enabled features.       |
| `getRoutes(configId, signal?)`                      | [GET /withdraw/:id/routes](/reference/withdraw/routes)                              | `RoutesResponse` — `fromCurrencies` + available `routes`.                       |
| `getBalance(configId, walletAddress, signal?)`      | [GET /withdraw/:id/balances](/reference/withdraw/balances)                          | `BalanceResponse` — per-currency balances for the wallet.                       |
| `createWithdrawalDetail(params, signal?)`           | [POST /withdraw/:id/prepare](/reference/withdraw/prepare)                           | `QuoteResponse` — `withdrawalDetailId`, `prepared`, fee `summary`, JWT `token`. |
| `updateWithdrawalDetail(detailId, params, signal?)` | [PATCH /withdraw-details/:detailId](/reference/withdraw-details/update)             | `QuoteResponse` — a re-priced quote.                                            |
| `submitWithdrawal(params, signal?)`                 | [POST /withdraw/:detailId/submit](/reference/withdraw/submit)                       | `SubmitResponse` — `txHash`, `state`, `broadcastAck`.                           |
| `getWithdrawalDetail(detailId, signal?)`            | [GET /withdraw-details/:detailId](/reference/withdraw-details/retrieve)             | `StatusResponse` — current `state`, `txHash`, `errorReason`.                    |
| `isSupported(minSupportedVersions?)`                | —                                                                                   | `boolean` — whether this SDK version satisfies the config's minimum.            |

`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

```tsx theme={null}
import { decodeJwt, isQuoteExpired, isSupported, isBelow, SDK_VERSION } from '@heliofi/withdraw-core';

isQuoteExpired(quote.token);        // → true once the quote's JWT has expired (5s buffer)
decodeJwt(quote.token);             // → { exp, iat, withdrawalDetailId, ... }
```

## Types & error codes

### Key types

```tsx theme={null}
// Success result passed to onWithdrawalComplete.
interface WithdrawResult {
  withdrawalDetailId: string;
  status: string;
  txHash: string | null;
}

// Error passed to onError (React) — a code + human message.
interface WithdrawError {
  code: WithdrawErrorCode;
  message: string;
}

// The signing callback payload.
interface SigningPayload {
  kind: 'transaction' | 'typed_data' | 'user_op';
  data: unknown;
}

// Quote response (from prepare / update).
interface QuoteResponse {
  withdrawalDetailId: string;
  token: string;                 // JWT — required for submit
  prepared: PreparedTransaction; // branch on .kind to sign (see Signing)
  summary: QuoteSummary;         // amounts + fee breakdown
}

// Submit / status responses.
interface SubmitResponse { withdrawalDetailId: string; txHash: string | null; state: WithdrawalState; broadcastAck: BroadcastAck | null; }
interface StatusResponse { withdrawalDetailId: string; state: WithdrawalState; txHash: string | null; broadcastAck: BroadcastAck | null; errorReason: string | null; }
```

### `SignedTransactionKind` (prepared transaction kinds)

`POLYMARKET_WALLET` · `HYPERCORE` · `RAW` · `USER_OP` — see the [signing mapping table](#the-contract).

### `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`.
