> ## Documentation Index
> Fetch the complete documentation index at: https://dev.commerce.moonpay.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Withdrawals

> Let users withdraw funds to any supported destination chain

A withdrawal allows a customer to transfer funds from a supported wallet to a user-selected destination wallet on a supported blockchain.

There are 3 ways to integrate withdrawals:

1. [**Widget (React embed)**](https://www.npmjs.com/package/@heliofi/withdraw-react) - Drop in a pre-built component.
2. [**JS SDK**](https://www.npmjs.com/package/@heliofi/withdraw-core) - Build your own UI with headless hooks or `@heliofi/withdraw-core`.
3. **Raw API** - Headless integration, full control over everything (this guide).

## Widget & SDK

Use our npm packages to embed the withdrawal widget or build a custom UI. Setup (API keys, withdrawal configs) and the HTTP API flow are covered in this guide.

> [!NOTE]
>
> SDK documentation is hosted on npm:
>
> * [**@heliofi/withdraw-react**](https://www.npmjs.com/package/@heliofi/withdraw-react) - drop-in React component and headless hooks
> * [**@heliofi/withdraw-core**](https://www.npmjs.com/package/@heliofi/withdraw-core) - framework-agnostic TypeScript SDK

The headless integration consists of two phases:

* **Setup**: Create a withdrawal config to define the source asset and obtain a `withdrawalConfigId`.
* **Runtime**: List available routes, prepare the withdrawal, sign the transaction, submit it, and track its status.

This guide covers the withdrawal flow for supported blockchains.

# 1. Setup

## Get an API Key

Creating a `WithdrawalConfig` requires API credentials, although the runtime withdrawal flow does not.

Follow the instructions [here](https://docs.hel.io/reference/getting-started#1-create-your-account-&-get-api-keys) to generate your API key pair.

Your API key must include the `WITHDRAWS_CREATE` permission. Include your credentials on every authenticated request:

```text theme={null}
?apiKey=<PUBLIC_KEY>
Authorization: Bearer <SECRET_KEY>
```

## **Find the Source Currency ID**

A withdrawal config is keyed by currency IDs, not currency symbols. To find the correct currency ID, use the [**Get Withdrawal Currencies**](https://dev.commerce.moonpay.com/reference/currency/list-withdrawal.md) endpoint to list currencies eligible for withdrawals.

Each currency includes an `id`, `symbol`, `decimals`, `blockchain`, and `features` array. A currency can only be used as a withdrawal source if its `features` array contains `WITHDRAWAL_SOURCE`.

The following example filters the returned currencies to find the withdrawable USDC asset on the specified blockchain:

```typescript theme={null}
// e.g. withdrawable USDC asset
const sourceCurrency = currencies.find(
  (c) =>
    c.blockchain?.symbol === "<BLOCKCHAIN>" &&
    c.symbol === "USDC" &&
    c.features?.includes("WITHDRAWAL_SOURCE"),
);
```

Use `sourceCurrency.id` as the `currencyIds` value when creating a withdrawal config and as the `sourceCurrencyId` when preparing a withdrawal.

## Create the WithdrawalConfig

Create a withdrawal config using the [Create Withdrawal Config](https://docs.hel.io/reference/withdrawal-configs/create) endpoint:

The response includes an `id`, which you'll use as the `withdrawalConfigId` when preparing withdrawals. Create the config once and reuse it for subsequent withdrawals.

# 2. Amounts

Specify amounts in atomic units as a decimal-free string. Always derive the value from the source currency's `decimals` field (returned by [**Get Withdrawal Currencies**](https://dev.commerce.moonpay.com/reference/currency/list-withdrawal.md)):

```text theme={null}
atomicAmount = humanAmount × 10^decimals
```

Decimal precision varies by chain and token. Do not assume USDC always uses 6 decimals.

| Source asset | `decimals` | Human amount | Atomic `amount` |
| - | - | - | - |
| USDC on Ethereum, Solana, Base, etc. | 6 | 1.5 USDC | `"1500000"` |
| USDC on HyperCore | 8 | 1.5 USDC | `"150000000"` |
| SOL | 9 | 0.1 SOL | `"100000000"` |

There is no maximum withdrawal amount. There is a **minimum floor**, returned per route as `minimumAmountMinimalUnit` (source-token atomic units) and `minimumAmountUsd`.

The expected output can be read from `destinationAmount` / `destinationAmountMin` in the prepare response. These are firm, fee-adjusted quotes valid until `summary.expiresAt`.

# 3. List Routes

Call the [**List Withdrawal Routes**](https://docs.hel.io/reference/withdraw/routes) endpoint to retrieve the available withdrawal destinations for your withdrawal config.

Select the route whose `toCurrency` matches the destination currency the user wants to receive. Each route also includes the minimum supported withdrawal amount, which you can use to validate user input before preparing the withdrawal.

Keep the following values for the next step:

* `sourceCurrencyId` from your withdrawal config.
* `toCurrency.id` from the selected route, which you'll use as the `toCurrencyId` when preparing the withdrawal.

Optionally, you can call the [**Get Withdrawal Balances**](https://docs.hel.io/reference/withdraw/balances) endpoint to retrieve the holder's on-chain balance and display it in your application. If `maxWithdrawableAmount` is present (HyperCore), show it as the limit instead of `balance`. Prepare rejects amounts above it.

# 4. Prepare the Withdrawal

Call the [**Prepare Withdrawal**](https://docs.hel.io/reference/withdraw/prepare) endpoint with the holder's wallet address, destination address, amount (in atomic units), and currency IDs.

Display the `summary` object as your confirmation screen.

If the user changes the destination, amount, or recipient, or if the quote expires, update the quote using the [**Update Withdrawal Detail**](https://docs.hel.io/reference/withdraw-details/update) endpoint rather than calling Prepare Withdrawal again.

# 5. Sign the Transaction

Inspect `prepared.kind` on the prepare (or re-quote) response to determine what the holder must sign:

* `raw` — sign `prepared.unsignedTx` (Solana)
* `userop` — sign the ERC-4337 user operation (`prepared.userOp` and related fields)
* `hypercore_eip3009` — sign `prepared.sendAssetTypedData` and `prepared.transferAuthorizationTypedData` (HyperCore, see [HyperCore withdrawals](#hypercore-withdrawals))
* `hypercore` — sign `prepared.typedData` (HyperCore, legacy single-signature route)

Use the appropriate wallet or signing library for that chain. Do not modify the prepared payload before signing, including any existing signatures, fee payer, or transaction metadata.

# 6. Submit the Withdrawal

Call the [**Submit Withdrawal**](https://docs.hel.io/reference/withdraw/submit) endpoint using the `withdrawalDetailId` returned by the **Prepare Withdrawal** endpoint.

Pass the signed payload from the previous step as `signedTx` when submitting the withdrawal.

# 7. Poll status

Poll the [Get Withdrawal Detail](https://docs.hel.io/reference/withdraw-details/retrieve) endpoint every few seconds until the withdrawal reaches a terminal state.

Handle the following terminal statuses:

| State | Meaning |
| - | - |
| `COMPLETED` | Done. |
| `FAILED` / `FAILED_EXPIRED` / `EXPIRED` | Surface `errorReason`; the user re-quotes from prepare. |
| `REFUNDED_BY_PROVIDER` | Bridge bounced funds back. |

# HyperCore withdrawals

HyperCore uses the same endpoints as other chains. Only the balance limit, the signing step and the status fields are different.

## Max withdrawable

Show `maxWithdrawableAmount` as the limit, not `balance`. It excludes open-order holds and a 0.01 USDC buffer.

```jsonc theme={null}
GET /v1/withdraw/{withdrawalConfigId}/balances?walletAddress={customerAddress}

{ "balances": [{
    "symbol": "USDC",
    "balance": "272718400",              // 2.727184 USDC (8 decimals)
    "maxWithdrawableAmount": "178830000" // 1.7883 USDC
}] }
```

`amount` is in HyperCore USDC atomic units (8 decimals). HyperEVM USDC has 6 decimals, so use amounts with at most 6 decimals. The last two digits are truncated.

## Sign both messages

When `prepared.kind` is `hypercore_eip3009`, the prepare response contains two EIP-712 messages:

```jsonc theme={null}
"prepared": {
  "kind": "hypercore_eip3009",
  "chain": "HYPERCORE",
  "sendAssetTypedData": { "domain", "types", "primaryType", "message" },             // domain.chainId 42161
  "transferAuthorizationChain": "HYPERLIQUID",
  "transferAuthorizationTypedData": { "domain", "types", "primaryType", "message" } // domain.chainId 999
}
```

Sign each one exactly as returned (`eth_signTypedData_v4`) with the `ownerAddress` wallet. Then join the signatures: the `sendAsset` signature first, then the authorization signature without its `0x`.

```typescript theme={null}
// viem
let signedTx;
if (prepared.kind === "hypercore_eip3009") {
  const sendAssetSig = await wallet.signTypedData(prepared.sendAssetTypedData);
  const authorizationSig = await wallet.signTypedData(prepared.transferAuthorizationTypedData);
  signedTx = sendAssetSig + authorizationSig.slice(2); // 0x + 260 hex chars
} else if (prepared.kind === "hypercore") {
  signedTx = await wallet.signTypedData(prepared.typedData);
}
```

> [!NOTE]
>
> * The two domains use different chain IDs: 42161 (HyperCore signing domain) and 999 (HyperEVM USDC). Some browser wallets won't sign when the domain chain ID differs from the active network, so the customer may need to switch networks between signatures.
> * `uint256` values arrive as decimal strings. Raw JSON-RPC accepts them as they are, but viem expects `bigint`, so convert those fields first.
> * Signatures with `v = 0/1` are normalized. A signature that can't be relayed is rejected with `400` before any funds move.

Submit as usual with `{ token, signedTx }` before `summary.expiresAt` (2 minutes after prepare). Sending the same submit again is safe and returns the same result.

## Track the withdrawal

Poll [Get Withdrawal Detail](https://docs.hel.io/reference/withdraw-details/retrieve) until `state` leaves `SUBMITTED` / `BRIDGING`. When leg 1 lands, `txHash` is the HyperCore ledger hash.

```jsonc theme={null}
{ "state": "COMPLETED",
  "txHash": "0xa545…",            // HyperCore leg
  "destinationTxHash": "Z88X…",   // delivery on the destination chain
  "hyperCoreBridge": {
    "legState": "relayed",
    "hypercoreLegHash": "0xa545…",
    "hyperevmLegHash": "0x94ad…",
    "requiresReauthorization": false } }
```

## Re-sign an expired authorization

This is rare. If leg 2 isn't relayed before its authorization expires, `requiresReauthorization` becomes `true`. The USDC stays safe on the customer's own HyperEVM address. Fetch a fresh authorization, have the customer sign it, and submit the signature. The relay then continues by itself.

```jsonc theme={null}
POST /v1/withdraw-details/{withdrawalDetailId}/hypercore-bridge/transfer-authorization
→ { "withdrawalDetailId": "…", "transferAuthorizationChain": "HYPERLIQUID",
    "transferAuthorizationTypedData": { … }, "validBefore": "1792057174" }

POST /v1/withdraw-details/{withdrawalDetailId}/hypercore-bridge/transfer-authorization/submit
{ "signature": "0x…" }   // one 65-byte signature
→ withdrawal detail (same shape as GET /v1/withdraw-details/{id})
```

# Webhooks

Register a webhook via the [Create Withdrawal Webhook endpoint](https://dev.commerce.moonpay.com/reference/webhook/withdrawal/create.md) to receive withdrawal lifecycle events instead of polling.

See [Withdrawal webhooks](https://dev.commerce.moonpay.com/reference/webhook/withdrawal/overview.md) for supported events and setup, and the [Withdrawal payload reference](https://dev.commerce.moonpay.com/reference/webhook/withdrawal/payloads.md) for JSON field details. Verify signatures using [Verifying Webhook Signatures](https://dev.commerce.moonpay.com/docs/webhooks.md#verifying-webhook-signatures).
