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

# Fireblocks Flow API guide

> Implement payment, deposit, and withdrawal flows with raw HTTP calls to the Fireblocks Flow endpoints.

<Note>
  This is an enterprise-only feature. Please [contact us](https://www.dynamic.xyz/book-a-call) to enable.
</Note>

## What We're Building

A server-side payment flow built on [Fireblocks Flow](/overview/fireblocks-flow). Fireblocks Flow lets you accept a crypto payment from any supported chain, wallet, or exchange and have it settled in a specific token on a specific chain. This guide uses the Flow API directly. By the end of this guide you will be able to:

* Create a flow from your backend, specifying the mode, amount, settlement, and destination
* Walk through the full flow: attach source, get quote, sign, broadcast
* Send withdrawals from your treasury or vault to end-user wallets (API only)
* Poll or receive webhooks for settlement completion
* Handle errors and edge cases

This guide uses raw HTTP calls, making it suitable for backend services, AI agents, cron jobs, or any language with `fetch`. For an overview of what Fireblocks Flow can do — funding sources, settlement currencies, compliance, and webhooks — see the [Fireblocks Flow overview](/overview/fireblocks-flow).

## Prerequisites

* A [Dynamic](https://app.dynamic.xyz) environment with Fireblocks Flow enabled
* An environment API token (`dyn_...`) with `flow.write` scope from **Developer > API Tokens** in the dashboard
* A connected wallet that can sign transactions on the chains you support. If your app doesn't already handle wallet connection, use the [Dynamic JavaScript SDK](/javascript/reference/quickstart) to connect a wallet and sign
* Node.js 18+ (or any runtime with `fetch`)

## Base URL

```
https://app.dynamicauth.com/api/v0
```

## Overview

The flow is a state machine with seven sequential steps. The same seven steps are used in **payment**, **deposit**, and **withdrawal** flows — only the mode, how the amount is interpreted, and who acts as source vs destination change between them. This guide walks through all three flows end-to-end:

```
1. Create flow          (from your backend — sets mode, amount, settlement, and destination)
2. Attach source        (declare which wallet/chain the sender is paying from)
3. Get quote            (get swap route, fees, and estimated time)
4. Prepare signing      (lock in the quote, get the signing payload)
5. Sign and broadcast   (sender's wallet signs and submits to the network)
6. Notify backend       (report the tx hash so Dynamic can watch the chain)
7. Wait for settlement  (poll or receive webhooks until funds land)
```

Each call advances the state — calling endpoints out of order returns `409`.

Jump to the flow that matches your use case:

* [Payment flow](#payment-flow) — receiver fixes the amount
* [Deposit flow](#deposit-flow) — sender chooses the amount
* [Withdrawal flow](#withdrawal-flow) — platform sends funds to an end-user wallet (API only)

## Authentication

| Context                                                    | Header                                                    |
| :--------------------------------------------------------- | :-------------------------------------------------------- |
| Flow creation (from your backend)                          | `Authorization: Bearer dyn_...` (with `flow.write` scope) |
| Flow mutations (source, quote, prepare, broadcast, cancel) | `X-Dynamic-Flow-Session-Token: dft_...`                   |
| Flow reads (polling)                                       | None required                                             |

The session token is returned **once** when you attach a source (Step 2). Store it for the duration of the flow.

<Warning>
  **Do not send `Authorization: Bearer` on SDK endpoints.** SDK endpoints (`/sdk/{environmentId}/...`) only accept `X-Dynamic-Flow-Session-Token`. Sending a Bearer header on them returns `401 Unauthorized`. Only the flow creation endpoint (`/server/{environmentId}/flow/{mode}`) requires the Bearer token.
</Warning>

***

## Choose Your Flow

This guide covers three use cases. The seven steps are the same in each — what changes is the `mode` you pass in Step 1, how the amount is interpreted, and which wallet is source vs destination. Pick the flow that matches your use case:

* **[Payment flow](#payment-flow)** — `mode: "payment"`. The **receiver** fixes the amount on each transaction (e.g., a \$25 invoice). Use for invoices, e-commerce checkouts, or paid services.
* **[Deposit flow](#deposit-flow)** — `mode: "deposit"`. The **sender** chooses how much to send (e.g., a \$100 top-up). Use for funding flows, on-ramps, or open-ended deposits.
* **[Withdrawal flow](#withdrawal-flow)** — `mode: "withdraw"`. Your platform fixes the payout amount and signs from a treasury or vault; the end user receives funds at an address you pass per flow. **HTTP API only** — not available through the JavaScript SDK.

Each flow below is self-contained and walks through all seven steps end-to-end (the withdrawal flow references the deposit flow for shared steps).

### Shared concepts

These apply to all flows:

| Concept                          | Description                                                                        |
| :------------------------------- | :--------------------------------------------------------------------------------- |
| `settlementConfig.strategy`      | How the best quote is selected: `"cheapest"`, `"fastest"`, or `"preferred_order"`  |
| `settlementConfig.settlements`   | Token/chain pairs you want to receive. Each needs a matching destination           |
| `destinationConfig.destinations` | Wallet addresses where funds land. `chainName` must match a settlement entry       |
| `enableOrchestration`            | Optional. Default `true`. When `false`, skips cross-chain settlement orchestration |

| Strategy            | Behavior                                                              |
| :------------------ | :-------------------------------------------------------------------- |
| `"cheapest"`        | Selects the route with the lowest total cost (gas + fees)             |
| `"fastest"`         | Selects the route with the fewest steps                               |
| `"preferred_order"` | Returns the first available quote in the order settlements are listed |

<Note>
  **Security model:** The API key (`dyn_...` with `flow.write` scope) is required only to create a flow. Creation is where amount, currency, destination, and settlement are fixed — so gating it behind the API key means a browser client has no endpoint that accepts those fields. Everything after creation is driven by a capability session token (`dft_...`) minted at source attachment.
</Note>

***

## Payment Flow

A complete walkthrough for `mode: "payment"`, where the **receiver** fixes the amount the sender must pay.

### Step 1 (Payment): Create a Flow

Create a flow from your backend. This is a server-side call authenticated by your API token with `flow.write` scope. The body specifies the amount, currency, settlement tokens, and destination wallets — these are fixed at creation time and cannot be changed later.

```
POST /server/{environmentId}/flow/payment
Authorization: Bearer dyn_your_api_token
Content-Type: application/json
```

```json theme={"system"}
{
  "amount": "25.00",
  "currency": "USD",
  "settlementConfig": {
    "strategy": "cheapest",
    "settlements": [
      {
        "chainName": "EVM",
        "chainId": "8453",
        "tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
        "symbol": "USDC",
        "tokenDecimals": 6
      },
      {
        "chainName": "SOL",
        "chainId": "101",
        "tokenAddress": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
        "symbol": "USDC",
        "tokenDecimals": 6
      }
    ]
  },
  "destinationConfig": {
    "destinations": [
      {
        "chainName": "EVM",
        "type": "address",
        "identifier": "0xYourEVMDestinationWallet"
      },
      {
        "chainName": "SOL",
        "type": "address",
        "identifier": "YourSOLDestinationWallet"
      }
    ]
  },
  "memo": {
    "orderId": "order_abc123"
  }
}
```

| Field                            | Description                                                                    |
| :------------------------------- | :----------------------------------------------------------------------------- |
| `amount`                         | The amount the receiver collects, as a string (e.g., `"25.00"`)                |
| `currency`                       | Fiat currency code the amount is denominated in (e.g., `"USD"`)                |
| `settlementConfig.strategy`      | Quote selection strategy: `"cheapest"`, `"fastest"`, or `"preferred_order"`    |
| `settlementConfig.settlements`   | Token/chain pairs you want to receive                                          |
| `destinationConfig.destinations` | Wallet addresses where settled funds land. `chainName` must match a settlement |
| `memo`                           | Optional. Arbitrary JSON metadata for your own correlation                     |
| `expiresIn`                      | Optional. TTL in seconds. Default: `86400` (24 hours)                          |

**Response (201):**

```json theme={"system"}
{
  "flow": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "amount": "25.00",
    "currency": "USD",
    "executionState": "initiated",
    "settlementState": "none"
  }
}
```

<Warning>
  Store `flow.id` — this is your `flowId` for all subsequent steps. Pass it to your frontend to drive the rest of the flow.
</Warning>

### Step 2 (Payment): Attach Source

Declare which wallet and chain the payer is paying from. This step returns a session token (`dft_...`) that authenticates all subsequent calls, and starts risk screening asynchronously.

```
POST /sdk/{environmentId}/flow/{flowId}/source
X-Dynamic-Flow-Session-Token: dft_...
Content-Type: application/json
```

```json theme={"system"}
{
  "sourceType": "wallet",
  "fromAddress": "0xYourWalletAddress",
  "fromChainId": "8453",
  "fromChainName": "EVM"
}
```

| Field           | Description                                                                                                        |
| :-------------- | :----------------------------------------------------------------------------------------------------------------- |
| `sourceType`    | `"wallet"`, `"exchange"`, or `"deposit_address"`                                                                   |
| `fromAddress`   | Source wallet address. Required for `wallet`.                                                                      |
| `fromChainId`   | Chain ID (e.g., `"1"` = Ethereum/BTC, `"8453"` = Base, `"101"` = SOL). Required for `wallet` and `deposit_address` |
| `fromChainName` | Chain family: `"EVM"`, `"SOL"`, `"BTC"`, `"SUI"`, `"TRON"`. Required for `wallet` and `deposit_address`            |

For a Solana payer, use the Solana chain values and address format:

```json theme={"system"}
{
  "sourceType": "wallet",
  "fromAddress": "YourSolanaWalletAddress",
  "fromChainId": "101",
  "fromChainName": "SOL"
}
```

**Response (200):**

```json theme={"system"}
{
  "sessionToken": "dft_a1b2c3d4e5f6...",
  "flow": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "executionState": "source_attached"
  }
}
```

Store the `sessionToken` — it is returned once and authenticates all subsequent calls for this flow.

**Error (403):** Blocked by sanctions screening — cancel and retry with a different source.

### Step 3 (Payment): Get a Quote

Specify which token the sender is paying with. The API finds the best route to the flow's settlement token(s) for the receiver's requested amount.

```
POST /sdk/{environmentId}/flow/{flowId}/quote
X-Dynamic-Flow-Session-Token: dft_...
Content-Type: application/json
```

```json theme={"system"}
{
  "fromTokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
}
```

| Field              | Description                                                                                                                                                                 |
| :----------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fromTokenAddress` | Token contract address (EVM) or mint (Solana). Use `0x0000000000000000000000000000000000000000` for EVM native tokens, or `11111111111111111111111111111111` for native SOL |
| `slippage`         | Optional. Slippage tolerance as a decimal (e.g., `0.005` for 0.5%)                                                                                                          |

**Response (200):**

```json theme={"system"}
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "executionState": "quoted",
  "quoteVersion": 1,
  "quote": {
    "version": 1,
    "fromAmount": "25.50",
    "toAmount": "25.00",
    "estimatedTimeSec": 120,
    "fees": {
      "totalFeeUsd": "0.50",
      "gasEstimate": {
        "usdValue": "0.30",
        "nativeValue": "0.00012",
        "nativeSymbol": "ETH"
      }
    },
    "createdAt": "2025-03-23T10:01:00Z",
    "expiresAt": "2025-03-23T10:02:00Z"
  }
}
```

In payment mode, `toAmount` matches the receiver's requested amount and `fromAmount` is what the sender must pay (including swap costs and fees).

<Note>
  Quotes expire in **60 seconds**. If it expires before you call `/prepare`, request a new quote. The `quoteVersion` increments with each new quote.
</Note>

For same-chain, same-token payments (no swap needed), the API builds a direct transfer payload — no routing required.

### Step 4 (Payment): Prepare Signing

Locks in the quote and returns the signing payload. You can optionally request on-chain balance assertions.

```
POST /sdk/{environmentId}/flow/{flowId}/prepare
X-Dynamic-Flow-Session-Token: dft_...
Content-Type: application/json
```

```json theme={"system"}
{
  "assertBalanceForGasCost": true,
  "assertBalanceForTransferAmount": true
}
```

| Field                            | Default | Description                                                                  |
| :------------------------------- | :------ | :--------------------------------------------------------------------------- |
| `assertBalanceForGasCost`        | `false` | Verify the wallet has enough for gas. Returns `422` if insufficient          |
| `assertBalanceForTransferAmount` | `false` | Verify the wallet has enough for the transfer. Returns `422` if insufficient |

**Response (200):** Transaction with `executionState: "signing"` and `quote.signingPayload`:

```json theme={"system"}
{
  "executionState": "signing",
  "quote": {
    "signingPayload": {
      "chainName": "EVM",
      "chainId": "8453",
      "evmTransaction": {
        "to": "0xContractAddress",
        "data": "0xCalldata...",
        "value": "0x0",
        "gasLimit": "0x5208"
      },
      "evmApproval": {
        "tokenAddress": "0xTokenAddress",
        "spenderAddress": "0xSpenderAddress",
        "amount": "25500000"
      }
    }
  }
}
```

The payload structure depends on the chain:

| Chain        | Fields                                                     | Notes                                                            |
| :----------- | :--------------------------------------------------------- | :--------------------------------------------------------------- |
| EVM          | `evmTransaction` (`to`, `data`, `value`, `gasLimit`)       | Standard EVM transaction                                         |
| EVM (ERC-20) | `evmApproval` (`tokenAddress`, `spenderAddress`, `amount`) | Send approval tx first if present                                |
| SOL, SUI     | `serializedTransaction`                                    | Base64-encoded serialized transaction                            |
| BTC          | `psbt`                                                     | Base64-encoded unsigned PSBT                                     |
| TRON         | `tronTransaction` (`rawDataHex`, `to`, `value`)            | Hex-encoded raw transaction data; sign using TronWeb or TronLink |

**Possible errors:**

* `422` — Quote expired: go back to Step 3
* `422` — Risk not cleared: poll `GET /flow/{flowId}` until `riskState` is `"cleared"`, then retry
* `422` — Insufficient balance: response includes `required` and `available` amounts

### Step 5 (Payment): Sign and Broadcast On-Chain

Use `prepared.quote.signingPayload` to sign and submit the transaction with whatever wallet your sender has connected. The payload — and the code to sign it — depends on the source chain. See [Signing the transaction by chain](#signing-the-transaction-by-chain) for EVM, Solana, Sui, and Bitcoin examples. It returns a `txHash` you'll report in Step 6.

### Step 6 (Payment): Notify Backend of Broadcast

<Note>
  This endpoint **does not broadcast the transaction on-chain.** That already happened in Step 5 — when the wallet signed and submitted the transaction, the network received it. This call is your client notifying Dynamic's backend that the broadcast happened and handing over the resulting `txHash`, so the backend can start watching the chain for confirmation and orchestrating settlement.
</Note>

Report the transaction hash back to the API once your wallet returns it.

```
POST /sdk/{environmentId}/flow/{flowId}/broadcast
X-Dynamic-Flow-Session-Token: dft_...
Content-Type: application/json
```

```json theme={"system"}
{
  "txHash": "0xabc123def456..."
}
```

**Response (200):** Transaction with `executionState: "broadcasted"`.

<Warning>
  **Point of no return.** After this call, the transaction cannot be cancelled. The backend begins monitoring the blockchain and orchestrating settlement.
</Warning>

### Step 7 (Payment): Wait for Settlement

After broadcast, the backend handles cross-chain settlement automatically. Monitor progress via polling or webhooks.

#### Option A: Polling

```
GET /sdk/{environmentId}/flow/{flowId}
```

No authentication required. Poll every 3 seconds.

**Stop when you see a terminal state:**

| Condition                         | Meaning                                       |
| :-------------------------------- | :-------------------------------------------- |
| `settlementState === "completed"` | Funds delivered to the receiver's destination |
| `settlementState === "failed"`    | Settlement failed                             |
| `executionState === "failed"`     | Execution failed                              |
| `executionState === "cancelled"`  | Transaction cancelled                         |
| `executionState === "expired"`    | Session timed out                             |

Settlement progresses through: `none` → `routing` → `bridging` → `swapping` → `settling` → `completed`. Same-chain, same-token payments jump directly to `completed`.

#### Option B: Webhooks (Recommended)

Set up a webhook to receive events as the transaction progresses:

```
POST /environments/{environmentId}/webhooks
Authorization: Bearer dyn_your_api_token
Content-Type: application/json
```

```json theme={"system"}
{
  "url": "https://your-api.example.com/webhooks/flow",
  "events": [
    "flow.execution.updated",
    "flow.settlement.updated",
    "flow.risk.updated"
  ],
  "isEnabled": true
}
```

Each event fires on every state transition for that axis. The payload tells you what changed:

```json title="Example: flow.settlement.updated payload" theme={"system"}
{
  "eventId": "...",
  "eventName": "flow.settlement.updated",
  "environmentId": "...",
  "environmentName": "live",
  "timestamp": "2026-06-15T12:00:00.000Z",
  "data": {
    "axis": "settlement",
    "flowId": "fl_abc123",
    "previousState": "settling",
    "newState": "completed",
    "timestamp": "2026-06-15T12:00:00.000Z"
  }
}
```

**Key states to handle:**

| Event                     | `data.newState` | Action                              |
| :------------------------ | :-------------- | :---------------------------------- |
| `flow.settlement.updated` | `completed`     | Payment is done — fulfill the order |
| `flow.settlement.updated` | `failed`        | Settlement failed — investigate     |
| `flow.execution.updated`  | `failed`        | Execution failed — investigate      |

```typescript title="webhook-handler.ts" theme={"system"}
import express from 'express';

const app = express();
app.use(express.json());

app.post('/webhooks/flow', (req, res) => {
  const { eventName, data } = req.body;

  if (
    eventName === 'flow.settlement.updated' &&
    data.newState === 'completed'
  ) {
    fulfillOrder(data.flowId);
  }

  if (data.newState === 'failed') {
    handleFailure(data.flowId, eventName, data.previousState);
  }

  // Respond quickly — process async
  res.sendStatus(200);
});
```

***

## Deposit Flow

A complete walkthrough for `mode: "deposit"`, where the **sender** chooses how much to send. The seven steps mirror the payment flow — what changes is the `mode` and how the amount is interpreted.

### Step 1 (Deposit): Create a Flow

Create a deposit flow from your backend. The body specifies the deposit amount, settlement tokens, and destination wallets.

```
POST /server/{environmentId}/flow/deposit
Authorization: Bearer dyn_your_api_token
Content-Type: application/json
```

```json theme={"system"}
{
  "amount": "100.00",
  "currency": "USD",
  "settlementConfig": {
    "strategy": "cheapest",
    "settlements": [
      {
        "chainName": "EVM",
        "chainId": "8453",
        "tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
        "symbol": "USDC",
        "tokenDecimals": 6
      },
      {
        "chainName": "SOL",
        "chainId": "101",
        "tokenAddress": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
        "symbol": "USDC",
        "tokenDecimals": 6
      }
    ]
  },
  "destinationConfig": {
    "destinations": [
      {
        "chainName": "EVM",
        "type": "address",
        "identifier": "0xYourEVMDestinationWallet"
      },
      {
        "chainName": "SOL",
        "type": "address",
        "identifier": "YourSOLDestinationWallet"
      }
    ]
  },
  "memo": {
    "userId": "user_abc123",
    "purpose": "account_top_up"
  }
}
```

| Field                            | Description                                                                    |
| :------------------------------- | :----------------------------------------------------------------------------- |
| `amount`                         | The amount the sender wants to deposit, as a string (e.g., `"100.00"`)         |
| `currency`                       | Currency code the amount is denominated in (e.g., `"USD"`)                     |
| `settlementConfig.strategy`      | Quote selection strategy: `"cheapest"`, `"fastest"`, or `"preferred_order"`    |
| `settlementConfig.settlements`   | Token/chain pairs you want to receive                                          |
| `destinationConfig.destinations` | Wallet addresses where settled funds land. `chainName` must match a settlement |
| `memo`                           | Optional. Arbitrary JSON metadata for your own correlation (e.g., user ID)     |
| `expiresIn`                      | Optional. TTL in seconds. Default: `86400` (24 hours)                          |

**Response (201):**

```json theme={"system"}
{
  "flow": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "amount": "100.00",
    "currency": "USD",
    "executionState": "initiated",
    "settlementState": "none"
  }
}
```

<Warning>
  Store `flow.id` — this is your `flowId` for all subsequent steps. Pass it to your frontend to drive the rest of the flow.
</Warning>

### Step 2 (Deposit): Attach Source

Declare which wallet and chain the depositor is funding from. This step returns a session token (`dft_...`) that authenticates all subsequent calls, and starts risk screening asynchronously.

```
POST /sdk/{environmentId}/flow/{flowId}/source
X-Dynamic-Flow-Session-Token: dft_...
Content-Type: application/json
```

```json theme={"system"}
{
  "sourceType": "wallet",
  "fromAddress": "0xYourWalletAddress",
  "fromChainId": "8453",
  "fromChainName": "EVM"
}
```

| Field           | Description                                                                                                        |
| :-------------- | :----------------------------------------------------------------------------------------------------------------- |
| `sourceType`    | `"wallet"`, `"exchange"`, or `"deposit_address"`                                                                   |
| `fromAddress`   | Depositor's wallet address. Required for `wallet`.                                                                 |
| `fromChainId`   | Chain ID (e.g., `"1"` = Ethereum/BTC, `"8453"` = Base, `"101"` = SOL). Required for `wallet` and `deposit_address` |
| `fromChainName` | Chain family: `"EVM"`, `"SOL"`, `"BTC"`, `"SUI"`, `"TRON"`. Required for `wallet` and `deposit_address`            |

For a Solana depositor, use the Solana chain values and address format:

```json theme={"system"}
{
  "sourceType": "wallet",
  "fromAddress": "YourSolanaWalletAddress",
  "fromChainId": "101",
  "fromChainName": "SOL"
}
```

**Response (200):**

```json theme={"system"}
{
  "sessionToken": "dft_a1b2c3d4e5f6...",
  "flow": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "executionState": "source_attached"
  }
}
```

Store the `sessionToken` — it is returned once and authenticates all subsequent calls for this flow.

**Error (403):** Blocked by sanctions screening — cancel and retry with a different source.

### Step 3 (Deposit): Get a Quote

Specify which token the depositor is funding with. The API finds the best route from that token to the flow's settlement token(s) for the deposit amount.

```
POST /sdk/{environmentId}/flow/{flowId}/quote
X-Dynamic-Flow-Session-Token: dft_...
Content-Type: application/json
```

```json theme={"system"}
{
  "fromTokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
}
```

| Field              | Description                                                                                                                                                                                          |
| :----------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `fromTokenAddress` | Token contract address (EVM) or mint (Solana) the depositor is sending. Use `0x0000000000000000000000000000000000000000` for EVM native tokens, or `11111111111111111111111111111111` for native SOL |
| `slippage`         | Optional. Slippage tolerance as a decimal (e.g., `0.005` for 0.5%)                                                                                                                                   |

**Response (200):**

```json theme={"system"}
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "executionState": "quoted",
  "quoteVersion": 1,
  "quote": {
    "version": 1,
    "fromAmount": "100.50",
    "toAmount": "100.00",
    "estimatedTimeSec": 120,
    "fees": {
      "totalFeeUsd": "0.50",
      "gasEstimate": {
        "usdValue": "0.30",
        "nativeValue": "0.00012",
        "nativeSymbol": "ETH"
      }
    },
    "createdAt": "2025-03-23T10:01:00Z",
    "expiresAt": "2025-03-23T10:02:00Z"
  }
}
```

`fromAmount` is what the depositor's wallet will be charged in the source token; `toAmount` is what lands at the destination after swap and fees.

<Note>
  Quotes expire in **60 seconds**. If it expires before you call `/prepare`, request a new quote. The `quoteVersion` increments with each new quote.
</Note>

For same-chain, same-token deposits (no swap needed), the API builds a direct transfer payload — no routing required.

### Step 4 (Deposit): Prepare Signing

Locks in the quote and returns the signing payload. You can optionally request on-chain balance assertions.

```
POST /sdk/{environmentId}/flow/{flowId}/prepare
X-Dynamic-Flow-Session-Token: dft_...
Content-Type: application/json
```

```json theme={"system"}
{
  "assertBalanceForGasCost": true,
  "assertBalanceForTransferAmount": true
}
```

| Field                            | Default | Description                                                                  |
| :------------------------------- | :------ | :--------------------------------------------------------------------------- |
| `assertBalanceForGasCost`        | `false` | Verify the wallet has enough for gas. Returns `422` if insufficient          |
| `assertBalanceForTransferAmount` | `false` | Verify the wallet has enough for the transfer. Returns `422` if insufficient |

**Response (200):** Transaction with `executionState: "signing"` and `quote.signingPayload`:

```json theme={"system"}
{
  "executionState": "signing",
  "quote": {
    "signingPayload": {
      "chainName": "EVM",
      "chainId": "8453",
      "evmTransaction": {
        "to": "0xContractAddress",
        "data": "0xCalldata...",
        "value": "0x0",
        "gasLimit": "0x5208"
      },
      "evmApproval": {
        "tokenAddress": "0xTokenAddress",
        "spenderAddress": "0xSpenderAddress",
        "amount": "100500000"
      }
    }
  }
}
```

The payload structure depends on the chain:

| Chain        | Fields                                                     | Notes                                                            |
| :----------- | :--------------------------------------------------------- | :--------------------------------------------------------------- |
| EVM          | `evmTransaction` (`to`, `data`, `value`, `gasLimit`)       | Standard EVM transaction                                         |
| EVM (ERC-20) | `evmApproval` (`tokenAddress`, `spenderAddress`, `amount`) | Send approval tx first if present                                |
| SOL, SUI     | `serializedTransaction`                                    | Base64-encoded serialized transaction                            |
| BTC          | `psbt`                                                     | Base64-encoded unsigned PSBT                                     |
| TRON         | `tronTransaction` (`rawDataHex`, `to`, `value`)            | Hex-encoded raw transaction data; sign using TronWeb or TronLink |

**Possible errors:**

* `422` — Quote expired: go back to Step 3
* `422` — Risk not cleared: poll `GET /flow/{flowId}` until `riskState` is `"cleared"`, then retry
* `422` — Insufficient balance: response includes `required` and `available` amounts

### Step 5 (Deposit): Sign and Broadcast On-Chain

Use `prepared.quote.signingPayload` to sign and submit the transaction with whatever wallet the depositor has connected. The payload — and the code to sign it — depends on the source chain. See [Signing the transaction by chain](#signing-the-transaction-by-chain) for EVM, Solana, Sui, and Bitcoin examples. It returns a `txHash` you'll report in Step 6.

### Step 6 (Deposit): Notify Backend of Broadcast

<Note>
  This endpoint **does not broadcast the transaction on-chain.** That already happened in Step 5 — when the wallet signed and submitted the transaction, the network received it. This call is your client notifying Dynamic's backend that the broadcast happened and handing over the resulting `txHash`, so the backend can start watching the chain for confirmation and orchestrating settlement.
</Note>

Report the transaction hash back to the API once your wallet returns it.

```
POST /sdk/{environmentId}/flow/{flowId}/broadcast
X-Dynamic-Flow-Session-Token: dft_...
Content-Type: application/json
```

```json theme={"system"}
{
  "txHash": "0xabc123def456..."
}
```

**Response (200):** Transaction with `executionState: "broadcasted"`.

<Warning>
  **Point of no return.** After this call, the transaction cannot be cancelled. The backend begins monitoring the blockchain and orchestrating settlement.
</Warning>

### Step 7 (Deposit): Wait for Settlement

After broadcast, the backend handles cross-chain settlement automatically. Monitor progress via polling or webhooks.

#### Option A: Polling

```
GET /sdk/{environmentId}/flow/{flowId}
```

No authentication required. Poll every 3 seconds.

**Stop when you see a terminal state:**

| Condition                         | Meaning                              |
| :-------------------------------- | :----------------------------------- |
| `settlementState === "completed"` | Deposit delivered to the destination |
| `settlementState === "failed"`    | Settlement failed                    |
| `executionState === "failed"`     | Execution failed                     |
| `executionState === "cancelled"`  | Transaction cancelled                |
| `executionState === "expired"`    | Session timed out                    |

Settlement progresses through: `none` → `routing` → `bridging` → `swapping` → `settling` → `completed`. Same-chain, same-token deposits jump directly to `completed`.

#### Option B: Webhooks (Recommended)

Set up a webhook to receive events as the transaction progresses:

```
POST /environments/{environmentId}/webhooks
Authorization: Bearer dyn_your_api_token
Content-Type: application/json
```

```json theme={"system"}
{
  "url": "https://your-api.example.com/webhooks/flow",
  "events": [
    "flow.execution.updated",
    "flow.settlement.updated",
    "flow.risk.updated"
  ],
  "isEnabled": true
}
```

Each event fires on every state transition for that axis — check `data.newState` to decide what action to take. See the [payment webhook section](#option-b-webhooks-recommended) for the full payload shape.

**Key states to handle:**

| Event                     | `data.newState` | Action                                      |
| :------------------------ | :-------------- | :------------------------------------------ |
| `flow.settlement.updated` | `completed`     | Deposit is done — credit the user's account |
| `flow.settlement.updated` | `failed`        | Settlement failed — investigate             |
| `flow.execution.updated`  | `failed`        | Execution failed — investigate              |

```typescript title="webhook-handler.ts" theme={"system"}
import express from 'express';

const app = express();
app.use(express.json());

app.post('/webhooks/flow', (req, res) => {
  const { eventName, data } = req.body;

  if (
    eventName === 'flow.settlement.updated' &&
    data.newState === 'completed'
  ) {
    creditUserAccount(data.flowId);
  }

  if (data.newState === 'failed') {
    handleFailure(data.flowId, eventName, data.previousState);
  }

  // Respond quickly — process async
  res.sendStatus(200);
});
```

***

## Deposit Address Flow

A deposit address flow generates a unique address per transaction. The user sends funds to that address directly — no wallet connection or on-chain signing is required on your end. The inbound transfer is detected automatically and the flow advances.

Use this when your users are paying from a CEX, a cold wallet, or any context where connecting a Web3 wallet is not practical. The deposit address can also be rendered as a QR code for users to scan from a mobile wallet.

**Supported chains:** BTC, SOL, EVM, and TRON.

### How it differs from the wallet flow

| Step                            | Wallet flow                               | Deposit address flow                                                                                                                         |
| :------------------------------ | :---------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------- |
| 3. Attach source                | `sourceType: "wallet"` with `fromAddress` | `sourceType: "deposit_address"` with `fromChainId` + `fromChainName`; `refundAddress` optional (where to return funds if the transfer fails) |
| 4. Get quote                    | Returns swap route                        | Returns swap route **and** `quote.depositAddress` — the address the user sends to                                                            |
| 5–7. Prepare / sign / broadcast | Required                                  | **Skipped** — user sends externally                                                                                                          |
| 8. Wait for settlement          | Poll `executionState`                     | Poll `executionState` until it leaves `"quoted"`                                                                                             |

### Step 3 (Deposit address): Attach Source

```
POST /sdk/{environmentId}/flow/{flowId}/source
X-Dynamic-Flow-Session-Token: dft_...
Content-Type: application/json
```

```json theme={"system"}
{
  "sourceType": "deposit_address",
  "fromChainId": "1",
  "fromChainName": "BTC"
}
```

Pass `refundAddress` to specify where to return funds if the transfer fails. Omit to refund to the original sender.

```json theme={"system"}
{
  "sourceType": "deposit_address",
  "fromChainId": "1",
  "fromChainName": "BTC",
  "refundAddress": "bc1q..."
}
```

**Response (200):** Flow with `executionState: "source_attached"`.

### Step 4 (Deposit address): Get a Quote

```
POST /sdk/{environmentId}/flow/{flowId}/quote
X-Dynamic-Flow-Session-Token: dft_...
Content-Type: application/json
```

```json theme={"system"}
{
  "fromTokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
}
```

Omit `fromTokenAddress` for native tokens (BTC, SOL, or EVM native).

**Response (200):**

```json theme={"system"}
{
  "executionState": "quoted",
  "quote": {
    "depositAddress": "bc1qxy2kgdygjrsqtzq2n0yrf249...",
    "fromAmount": "100000000",
    "toAmount": "99500000",
    "fees": { "totalFeeUsd": "1.20" }
  }
}
```

Show `quote.depositAddress` to the user. They send `quote.fromAmount` (in the token's base unit) to that address from any wallet or exchange.

### Step 5 (Deposit address): Wait for transfer detection

Steps 5–7 (prepare, sign, broadcast) do not exist for deposit address flows. Once the user sends funds, the transfer is detected automatically and the flow advances.

Poll `GET /sdk/{environmentId}/transactions/{transactionId}` every few seconds until `executionState` leaves `"quoted"`:

| `executionState`     | Meaning                                    |
| :------------------- | :----------------------------------------- |
| `"quoted"`           | Waiting for the user to send funds         |
| `"source_confirmed"` | Transfer detected — settlement in progress |
| `"failed"`           | Transfer failed or deposit address expired |
| `"cancelled"`        | Flow was cancelled                         |

Deposit addresses expire after 48 hours. If the flow reaches `"failed"` with no funds received, create a new transaction.

***

## Withdrawal Flow

<Info>
  Withdrawals and money-out are supported through this HTTP API only. The [Fireblocks Flow JavaScript SDK guide](/overview/fireblocks-flow-js-sdk) covers payment and deposit flows, not platform-to-user payouts.
</Info>

A withdrawal sends funds **from** your treasury, vault, or [server wallet](/overview/wallets/overview) **to** an end-user wallet, optionally converting across chains and tokens along the way. It uses the same seven steps as the other flows, with `mode: "withdraw"` and reversed roles:

| Role                | Deposit (money in)               | Withdrawal (money out)                         |
| :------------------ | :------------------------------- | :--------------------------------------------- |
| Source              | End user's wallet or exchange    | Your treasury, vault, or server wallet         |
| Destination         | Your merchant or treasury wallet | End user's wallet (set in `destinationConfig`) |
| Amount              | Sender chooses                   | Your platform chooses                          |
| Signing (Steps 4–5) | End user's connected wallet      | Your custody or server wallet                  |

Each withdrawal flow specifies the end user's destination directly in the flow creation body. See [Money in and money out](/overview/fireblocks-flow#money-in-and-money-out) on the overview for the product model.

<Warning>
  Your application must sign Steps 4–5 with a wallet you control (Fireblocks vault, server wallet, or similar). End users do not sign withdrawal transactions in their browser the way they do for deposits.
</Warning>

### Step 1 (Withdrawal): Create a Flow

Create a withdrawal flow from your backend with the payout amount and the end user's destination wallet.

```
POST /server/{environmentId}/flow/withdraw
Authorization: Bearer dyn_your_api_token
Content-Type: application/json
```

```json theme={"system"}
{
  "amount": "100.00",
  "currency": "USD",
  "settlementConfig": {
    "strategy": "cheapest",
    "settlements": [
      {
        "chainName": "EVM",
        "chainId": "1",
        "tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "symbol": "USDC",
        "tokenDecimals": 6
      }
    ]
  },
  "destinationConfig": {
    "destinations": [
      {
        "chainName": "EVM",
        "type": "address",
        "identifier": "0x742d35Cc6634C0532925a3b844Bc9e7595f7aBCD"
      }
    ]
  },
  "memo": {
    "withdrawalId": "wd_abc123",
    "userId": "user_456"
  }
}
```

| Field                            | Description                                                                 |
| :------------------------------- | :-------------------------------------------------------------------------- |
| `amount`                         | The payout amount your platform sends, as a string (e.g., `"100.00"`)       |
| `currency`                       | Fiat currency code the amount is denominated in (e.g., `"USD"`)             |
| `settlementConfig.strategy`      | Quote selection strategy: `"cheapest"`, `"fastest"`, or `"preferred_order"` |
| `settlementConfig.settlements`   | Token/chain pairs the end user will receive                                 |
| `destinationConfig.destinations` | End-user wallet(s) where settled funds land                                 |
| `memo`                           | Optional. Arbitrary JSON metadata (e.g., withdrawal or user ID)             |
| `expiresIn`                      | Optional. TTL in seconds. Default: `3600` (1 hour)                          |

**Response (201):** Same shape as the [deposit flow Step 1](#step-1-deposit-create-a-flow). Store `flow.id` as your `flowId`.

### Step 2 (Withdrawal): Attach Source

Attach your treasury, vault, or server wallet as the funding source — not the end user's wallet.

```
POST /sdk/{environmentId}/flow/{flowId}/source
X-Dynamic-Flow-Session-Token: dft_...
Content-Type: application/json
```

```json theme={"system"}
{
  "sourceType": "wallet",
  "fromAddress": "0xYourTreasuryOrVaultAddress",
  "fromChainId": "8453",
  "fromChainName": "EVM"
}
```

Use the chain and token your treasury holds (e.g., USDC on Base). The quote step routes from this source to the settlement token and destination you set in Step 1.

**Response (200):** Returns the session token and flow with `executionState: "source_attached"`. Store the `sessionToken` — it authenticates all subsequent calls.

**Error (403):** Blocked by sanctions screening — cancel and retry with a different source.

### Steps 3–7 (Withdrawal): Quote Through Settlement

Steps 3–7 use the same endpoints as the deposit flow. Differences are **who signs** and **what you do on completion**:

| Step                   | Deposit flow reference                                          | Withdrawal notes                                                              |
| :--------------------- | :-------------------------------------------------------------- | :---------------------------------------------------------------------------- |
| 3. Get quote           | [Step 3 (Deposit)](#step-3-deposit-get-a-quote)                 | Pass the token your treasury pays with in `fromTokenAddress`                  |
| 4. Prepare             | [Step 4 (Deposit)](#step-4-deposit-prepare-signing)             | Balance assertions apply to your treasury wallet                              |
| 5. Sign and broadcast  | [Step 5 (Deposit)](#step-5-deposit-sign-and-broadcast-on-chain) | Sign with your custody or server wallet, not the end user's browser wallet    |
| 6. Notify backend      | [Step 6 (Deposit)](#step-6-deposit-notify-backend-of-broadcast) | Report the `txHash` after your platform broadcasts                            |
| 7. Wait for settlement | [Step 7 (Deposit)](#step-7-deposit-wait-for-settlement)         | On `settlement.state.completed`, mark the withdrawal fulfilled in your system |

**Example — Step 3 quote** (treasury pays with USDC on Base; user receives USDC on Ethereum):

```
POST /sdk/{environmentId}/flow/{flowId}/quote
X-Dynamic-Flow-Session-Token: dft_...
Content-Type: application/json
```

```json theme={"system"}
{
  "fromTokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"
}
```

On completion, `settlementState === "completed"` means funds reached the destination from Step 1. Handle `flow.settlement.updated` webhooks (where `data.newState === "completed"`) the same way as in [Step 7 (Deposit)](#step-7-deposit-wait-for-settlement), but credit the user's balance or close the withdrawal request instead of treating it as an inbound deposit.

***

## Signing the Transaction by Chain

Step 5 of every flow signs and broadcasts `prepared.quote.signingPayload`, then reports the resulting `txHash` in Step 6. The flow itself is identical across chains — only this signing step differs. The payload field depends on the source chain:

| Chain   | Payload field                                 | Sign with                                                           |
| :------ | :-------------------------------------------- | :------------------------------------------------------------------ |
| EVM     | `evmTransaction` (+ optional `evmApproval`)   | [viem](https://viem.sh), wagmi, ethers, or Dynamic's primary wallet |
| Solana  | `serializedTransaction` (base64 versioned tx) | [`@solana/web3.js`](https://solana.com/docs/clients/javascript)     |
| Sui     | `serializedTransaction` (base64)              | [`@mysten/sui`](https://sdk.mystenlabs.com/typescript)              |
| Bitcoin | `psbt` (base64 unsigned PSBT)                 | any PSBT signer (e.g. `bitcoinjs-lib`)                              |

<Note>
  **Any Dynamic SDK can be used for signing** — not just the browser JS SDK. React, React Native, Flutter, Swift, Kotlin, Unity, and the **Node.js SDK** (server-side MPC wallets) all surface the same signing primitives. See [docs.dynamic.xyz](https://docs.dynamic.xyz) for platform-specific guides. The examples below show browser wallet (viem/wagmi) and Node SDK patterns.
</Note>

In each example, `prepared` is the response from Step 4.

### EVM

Uses an already-connected EVM wallet client (`walletClient`) — for example the one returned by `useWalletClient()` in wagmi or by Dynamic's primary wallet connector. If `evmApproval` is present, send the approval transaction first, then the main one.

```typescript title="sign-evm.ts" theme={"system"}
import { parseAbi } from 'viem';

const { signingPayload } = prepared.quote;

// Handle ERC-20 approval if needed
if (signingPayload.evmApproval) {
  const { tokenAddress, spenderAddress, amount } = signingPayload.evmApproval;
  const approvalHash = await walletClient.writeContract({
    address: tokenAddress as `0x${string}`,
    abi: parseAbi(['function approve(address, uint256) returns (bool)']),
    functionName: 'approve',
    args: [spenderAddress as `0x${string}`, BigInt(amount)],
  });
  await walletClient.waitForTransactionReceipt({ hash: approvalHash });
}

// Send the main transaction
const txHash = await walletClient.sendTransaction({
  to: signingPayload.evmTransaction.to as `0x${string}`,
  data: signingPayload.evmTransaction.data as `0x${string}`,
  value: BigInt(signingPayload.evmTransaction.value),
});
```

#### EVM — Node SDK (server-side MPC wallet)

```typescript title="sign-evm-node.ts" theme={"system"}
import { DynamicEvmWalletClient } from '@dynamic-labs-wallet/node-evm';

const client = new DynamicEvmWalletClient({ environmentId });
await client.authenticateApiToken(apiToken);

const walletClient = await client.getWalletClient({
  accountAddress,
  chainId: 8453, // Base
  rpcUrl: 'https://mainnet.base.org',
});

const { evmTransaction: tx, evmApproval } = prepared.quote.signingPayload;

if (evmApproval) {
  // send approval tx first (see browser example above for ABI)
}

const txHash = await walletClient.sendTransaction({
  to:    tx.to as `0x${string}`,
  data:  tx.data as `0x${string}`,
  value: BigInt(tx.value || '0'),
  gas:   tx.gasLimit ? BigInt(tx.gasLimit) : undefined,
  chainId: 8453,
});
```

### Solana

`serializedTransaction` is a base64-encoded [versioned transaction](https://solana.com/docs/core/transactions/versions) — there are no approvals. Deserialize it, sign with your connected wallet (or keypair), and broadcast. The example below uses [`@solana/web3.js`](https://solana.com/docs/clients/javascript).

```typescript title="sign-solana.ts" theme={"system"}
import {
  Connection,
  Keypair,
  VersionedTransaction,
} from '@solana/web3.js';

const connection = new Connection(
  process.env.SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com',
  'confirmed'
);
const signer = Keypair.fromSecretKey(
  Uint8Array.from(JSON.parse(process.env.SOLANA_SECRET_KEY as string))
);

const tx = VersionedTransaction.deserialize(
  Buffer.from(prepared.quote.signingPayload.serializedTransaction, 'base64')
);
tx.sign([signer]);

const txHash = await connection.sendRawTransaction(tx.serialize());
await connection.confirmTransaction(txHash, 'confirmed');
```

<Warning>
  **`signTransaction` returns the signature only, not the full signed transaction.** The Node SDK's `DynamicSvmWalletClient.signTransaction` returns the raw Ed25519 signature as a base58 string (88 chars / 64 bytes) — NOT a re-serialized transaction. You must decode it and add it back to the transaction before broadcasting.
</Warning>

#### Solana — Node SDK (server-side MPC wallet)

```typescript title="sign-sol-node.ts" theme={"system"}
import { DynamicSvmWalletClient, decodeBase58, addSignatureToTransaction } from '@dynamic-labs-wallet/node-svm';
import { VersionedTransaction, Connection, PublicKey } from '@solana/web3.js';

const client = new DynamicSvmWalletClient({ environmentId });
await client.authenticateApiToken(apiToken);

// Decode the base64 payload from prepare → VersionedTransaction
const vtx = VersionedTransaction.deserialize(
  Buffer.from(prepared.quote.signingPayload.serializedTransaction, 'base64')
);

// Sign — returns base58 signature (64 bytes), NOT the full signed tx
const signatureBase58 = await client.signTransaction({
  walletMetadata,
  transaction: vtx,
});

// Decode signature and add it back to the transaction
const sigBytes = decodeBase58(signatureBase58);
const signedVtx = addSignatureToTransaction({
  transaction: vtx,
  signature: sigBytes,
  signerPublicKey: new PublicKey(accountAddress),
});

// Broadcast — skipPreflight avoids false failures from stale oracle simulations
const connection = new Connection(
  process.env.SOLANA_RPC_URL ?? 'https://api.mainnet-beta.solana.com',
  'confirmed'
);
const txHash = await connection.sendRawTransaction(signedVtx.serialize(), {
  skipPreflight: true,
});
await connection.confirmTransaction(txHash, 'confirmed');
```

<Warning>
  **Solana routing and oracle errors.** The default cheapest route for SOL→USDC may use the Titan protocol, which requires fresh Pyth oracle data embedded in the transaction. If you see `PythOracleOutdated` errors, pass `slippage: 0.5` (or higher) in Step 4's quote request to route via dflow instead. Alternatively, settle cross-chain to Base (SOL → USDC on Base via MayanFinance) which has no oracle dependency.
</Warning>

### Sui

Sui returns the same `serializedTransaction` field as Solana — a base64-encoded transaction. Deserialize and sign it with your Sui keypair via [`@mysten/sui`](https://sdk.mystenlabs.com/typescript), then report the resulting digest as `txHash`.

### Bitcoin

Bitcoin returns a `psbt` field — a base64-encoded unsigned [PSBT](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki). Sign each input with your wallet, finalize, extract the raw transaction, broadcast it, and report the resulting txid as `txHash`.

***

## Cancelling a Transaction

Cancel any time before broadcast (states: `initiated`, `source_attached`, `quoted`, `signing`):

```
POST /sdk/{environmentId}/flow/{flowId}/cancel
X-Dynamic-Flow-Session-Token: dft_...
```

Returns the transaction with `executionState: "cancelled"`. Once cancelled, create a new transaction to retry.

***

## Complete Example

A self-contained TypeScript script that creates a flow from the backend, executes the payment lifecycle, and polls for settlement. It assumes you already have a connected wallet client — for example one returned by Dynamic's JS SDK, wagmi's `useWalletClient()`, or any viem `WalletClient`. If your app doesn't have wallet connection yet, use the [Dynamic JavaScript SDK](/javascript/reference/quickstart) to provide it.

<Note>
  This example pays from an **EVM** wallet. To pay from **Solana**, keep Steps 1–4 and 6–7 identical — only swap the Step 5 signing block for the Solana variant ([sign-solana.ts](#solana)), attach a Solana source (`fromChainName: "SOL"`, `fromChainId: "101"`), and quote with a Solana `fromTokenAddress` (mint, or `11111111111111111111111111111111` for native SOL).
</Note>

```typescript title="flow-example.ts" theme={"system"}
import { parseAbi, type WalletClient } from 'viem';

// --- Config ---
const API = 'https://app.dynamicauth.com/api/v0';
const ENV_ID = 'your-environment-id';
const API_TOKEN = 'dyn_your_api_token'; // must have flow.write scope

// Bring your own connected wallet client (viem, wagmi, Dynamic primary wallet, etc.)
declare const walletClient: WalletClient & {
  waitForTransactionReceipt: (args: { hash: `0x${string}` }) => Promise<unknown>;
};
const account = walletClient.account!;

// --- Helpers ---
async function api(path: string, options: RequestInit = {}) {
  const res = await fetch(`${API}${path}`, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...options.headers,
    },
  });
  if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
  return res.json();
}

// --- Step 1: Create flow (server-side) ---
async function createFlow(amount: string): Promise<string> {
  const { flow } = await api(`/server/${ENV_ID}/flow/payment`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_TOKEN}` },
    body: JSON.stringify({
      amount,
      currency: 'USD',
      settlementConfig: {
        strategy: 'cheapest',
        settlements: [
          {
            chainName: 'EVM',
            chainId: '8453',
            symbol: 'USDC',
            tokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
            tokenDecimals: 6,
          },
        ],
      },
      destinationConfig: {
        destinations: [
          {
            chainName: 'EVM',
            type: 'address',
            identifier: '0xYourDestinationWallet',
          },
        ],
      },
      memo: { orderId: 'order_abc123' },
    }),
  });
  console.log('Flow created:', flow.id);
  return flow.id;
}

// --- Steps 2–7: Client-side flow lifecycle ---
async function executeFlow(flowId: string) {
  // Step 2: Attach source (returns session token)
  const { sessionToken } = await api(`/sdk/${ENV_ID}/flow/${flowId}/source`, {
    method: 'POST',
    body: JSON.stringify({
      sourceType: 'wallet',
      fromAddress: account.address,
      fromChainId: '8453',
      fromChainName: 'EVM',
    }),
  });
  const session = { 'X-Dynamic-Flow-Session-Token': sessionToken };

  // Step 3: Get quote (paying with USDC on Base)
  const quoted = await api(`/sdk/${ENV_ID}/flow/${flowId}/quote`, {
    method: 'POST',
    headers: session,
    body: JSON.stringify({
      fromTokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
    }),
  });
  console.log(
    `Quote: send ${quoted.quote.fromAmount}, receive ${quoted.quote.toAmount}, fees: $${quoted.quote.fees?.totalFeeUsd}`
  );

  // Step 4: Prepare signing (with balance checks)
  const prepared = await api(`/sdk/${ENV_ID}/flow/${flowId}/prepare`, {
    method: 'POST',
    headers: session,
    body: JSON.stringify({
      assertBalanceForGasCost: true,
      assertBalanceForTransferAmount: true,
    }),
  });

  // Step 5: Sign and broadcast
  const { signingPayload } = prepared.quote;

  if (signingPayload.evmApproval) {
    const approvalHash = await walletClient.writeContract({
      address: signingPayload.evmApproval.tokenAddress as `0x${string}`,
      abi: parseAbi(['function approve(address, uint256) returns (bool)']),
      functionName: 'approve',
      args: [
        signingPayload.evmApproval.spenderAddress as `0x${string}`,
        BigInt(signingPayload.evmApproval.amount),
      ],
    });
    await walletClient.waitForTransactionReceipt({ hash: approvalHash });
    console.log('Token approved');
  }

  const txHash = await walletClient.sendTransaction({
    to: signingPayload.evmTransaction.to as `0x${string}`,
    data: signingPayload.evmTransaction.data as `0x${string}`,
    value: BigInt(signingPayload.evmTransaction.value),
  });
  console.log('Broadcasted:', txHash);

  // Step 6: Record broadcast
  await api(`/sdk/${ENV_ID}/flow/${flowId}/broadcast`, {
    method: 'POST',
    headers: session,
    body: JSON.stringify({ txHash }),
  });

  // Step 7: Poll for settlement
  while (true) {
    const flow = await api(`/sdk/${ENV_ID}/flow/${flowId}`);

    if (flow.settlementState === 'completed') {
      console.log('Payment settled!');
      return flow;
    }

    if (
      flow.settlementState === 'failed' ||
      ['cancelled', 'expired', 'failed'].includes(flow.executionState)
    ) {
      throw new Error(
        `Payment failed: execution=${flow.executionState}, settlement=${flow.settlementState}`
      );
    }

    console.log(
      `Waiting... execution=${flow.executionState}, settlement=${flow.settlementState}`
    );
    await new Promise((r) => setTimeout(r, 3000));
  }
}

// --- Run ---
const flowId = await createFlow('25.00');
await executeFlow(flowId);
```

***

## Supported Chains and Native Tokens

Fireblocks Flow supports the following chains. Use these values for `chainName`, `chainId`, and token addresses in your settlement config and source attachment.

### Chain Reference

| Chain   | `chainName` | `chainId`                                                                                                                             | Networks               |
| :------ | :---------- | :------------------------------------------------------------------------------------------------------------------------------------ | :--------------------- |
| EVM     | `"EVM"`     | Standard EVM chain ID (e.g., `"1"` for Ethereum, `"137"` for Polygon, `"8453"` for Base, `"42161"` for Arbitrum, `"10"` for Optimism) | All major EVM networks |
| Solana  | `"SOL"`     | `"101"`                                                                                                                               | Solana mainnet         |
| Bitcoin | `"BTC"`     | `"1"`                                                                                                                                 | Bitcoin mainnet        |
| Sui     | `"SUI"`     | `"501"`                                                                                                                               | Sui mainnet            |
| TRON    | `"TRON"`    | `"728126428"`                                                                                                                         | TRON mainnet           |

**Except for the EVM chains listed below, only mainnet is supported:**

| Name                     | `ID`         |
| :----------------------- | :----------- |
| Base Sepolia Testnet     | `"84532"`    |
| Arbitrum Sepolia Testnet | `"421614"`   |
| Arc Testnet              | `"5042002"`  |
| OP Sepolia Testnet       | `"11155420"` |

### Native Token Addresses

For native tokens (ETH, SOL, BTC, SUI, TRX), use any of the accepted addresses below in token address fields:

| Chain   | Accepted native token addresses                                                                                    |
| :------ | :----------------------------------------------------------------------------------------------------------------- |
| EVM     | `0x0000000000000000000000000000000000000000` (zero address) or `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee`        |
| Solana  | `11111111111111111111111111111111` (System Program) or `So11111111111111111111111111111111111111112` (Wrapped SOL) |
| Bitcoin | `11111111111111111111111111111111` or `bitcoin`                                                                    |
| Sui     | `0x2::sui::SUI` or `0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI`                  |
| TRON    | `T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb` (TRX)                                                                         |

For non-native tokens, use the token's contract address on that chain (e.g., `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` for USDC on Base).

***

## Error Reference

### By status code

| Status | Cause                               | Recovery                                              |
| :----- | :---------------------------------- | :---------------------------------------------------- |
| `400`  | Invalid request body                | Check field formats                                   |
| `401`  | Missing or invalid session token    | Verify `X-Dynamic-Flow-Session-Token` header          |
| `403`  | Risk screening blocked              | Cancel and retry with a different source              |
| `404`  | Resource not found                  | Verify flow ID                                        |
| `409`  | State conflict or duplicate tx hash | Check `executionState` and call the correct next step |
| `422`  | Quote expired                       | Re-quote (Step 3) and retry prepare                   |
| `422`  | Insufficient balance                | Source wallet doesn't have enough funds               |
| `422`  | Risk not cleared                    | Poll until `riskState` is `"cleared"`, then retry     |

### By step

<Accordion title="Step 1 — Create flow">
  | Status | Error                                                                | Cause                                                                                | Fix                                                                                                 |
  | :----- | :------------------------------------------------------------------- | :----------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- |
  | `400`  | `Invalid flow mode. Expected one of: payment, deposit, withdraw`     | Invalid `mode` in the URL path.                                                      | Use `/flow/payment`, `/flow/deposit`, or `/flow/withdraw`.                                          |
  | `401`  | `Unauthorized`                                                       | Missing or invalid API token, or the token lacks `flow.write` scope.                 | Check the `Authorization: Bearer dyn_...` header and the token scope in **Developer > API Tokens**. |
  | `422`  | `Each chain should only have one destination config`                 | Duplicate chain entries in `destinationConfig.destinations`.                         | Remove duplicates so each chain appears once.                                                       |
  | `422`  | `Settlement for token SYMBOL: ...`                                   | A settlement chain is not supported, or the token address is invalid for that chain. | Verify the chain/token pair. Use a valid EVM address for EVM chains, a Solana mint for SOL, etc.    |
  | `422`  | `All settlement chains must have a destination config. Missing: ...` | A chain in `settlementConfig` has no matching entry in `destinationConfig`.          | Add a destination entry for every settlement chain.                                                 |
  | `403`  | Sanctions block                                                      | A destination address is sanctioned.                                                 | Use a different destination address.                                                                |
</Accordion>

<Accordion title="Step 2 — Attach source">
  | Status | Error                                                                                | Cause                                                                                  | Fix                                                                                                |
  | :----- | :----------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------- |
  | `400`  | `fromAddress, fromChainId, and fromChainName are required for wallet sources`        | Missing source fields when `sourceType` is `"wallet"`.                                 | Include all three fields in the request body.                                                      |
  | `400`  | `fromChainId and fromChainName are required for deposit_address sources`             | Missing chain fields when `sourceType` is `"deposit_address"`.                         | Include `fromChainId` and `fromChainName`.                                                         |
  | `400`  | `fromAddress is not accepted for deposit_address sources; use refundAddress instead` | `fromAddress` was passed with `sourceType: "deposit_address"`.                         | Remove `fromAddress` and pass `refundAddress` if you need a refund destination.                    |
  | `400`  | `refundAddress is only valid for deposit_address sources`                            | `refundAddress` was passed with `sourceType: "wallet"` or `"exchange"`.                | Remove `refundAddress` from non-deposit-address source requests.                                   |
  | `400`  | Chain not supported                                                                  | The source chain/chainId is not supported for swaps.                                   | Use a supported chain (see [Supported Chains](/overview/fireblocks-flow-js-sdk#supported-chains)). |
  | `403`  | `Flow is blocked by sanctions`                                                       | The source address failed sanctions screening (wallet sources only).                   | Use a different source wallet.                                                                     |
  | `404`  | `Flow not found`                                                                     | Invalid `flowId`, deleted flow, or wrong environment ID.                               | Verify the flow ID and environment.                                                                |
  | `409`  | State transition error                                                               | The flow has already been broadcasted — sources can only be attached before broadcast. | Check `executionState` with a GET request. Start a new flow if needed.                             |
</Accordion>

<Accordion title="Step 3 — Get quote">
  | Status | Error                                                                               | Cause                                                                                              | Fix                                                                          |
  | :----- | :---------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------- |
  | `400`  | `fromChainName is required` / `fromChainId is required` / `fromAddress is required` | No source attached yet.                                                                            | Call attach source (Step 2) first.                                           |
  | `400`  | `fromTokenAddress is required and could not be resolved for the specified chain`    | The token could not be determined automatically.                                                   | Pass `fromTokenAddress` explicitly in the request body.                      |
  | `403`  | `Flow is blocked by risk screening`                                                 | The source was flagged by risk screening.                                                          | Cancel the flow and retry with a different source.                           |
  | `409`  | State transition error                                                              | The flow is not in a quotable state (e.g. already signed).                                         | You can re-quote from `source_attached` or `quoted`. Check `executionState`. |
  | `422`  | `No quotes available for any settlement token.`                                     | No swap provider returned a valid quote. The response includes a structured `quoteFailures` array. | Inspect the `quoteFailures` array. See below.                                |
  | `422`  | `Token price is not available for conversion (chain: ..., token: ...)`              | The swap provider cannot price the source token.                                                   | Try a different source token.                                                |

  **Quote failure response format:**

  When no quotes are available, the 422 response includes a `quoteFailures` array with per-settlement failure details:

  ```json theme={"system"}
  {
    "error": "No quotes available for any settlement token.",
    "quoteFailures": [
      {
        "chainId": "728126428",
        "chainName": "TRON",
        "reason": "unknown",
        "symbol": "USDT"
      },
      {
        "chainId": "8453",
        "chainName": "EVM",
        "reason": "amount_below_minimum",
        "symbol": "USDC"
      }
    ]
  }
  ```

  Each entry tells you which settlement token failed (`symbol` on `chainName`/`chainId`) and why (`reason`).

  **Quote failure reasons:**

  | Reason code               | Meaning                                                                                                           | Fix                                                            |
  | :------------------------ | :---------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------- |
  | `amount_below_minimum`    | The flow amount is too low for this token/route.                                                                  | Increase the amount or use a different source token.           |
  | `amount_above_maximum`    | The flow amount exceeds the route's maximum.                                                                      | Decrease the amount or split across multiple flows.            |
  | `insufficient_liquidity`  | Not enough liquidity on the swap route.                                                                           | Try a different source token or chain, or reduce the amount.   |
  | `unsupported_chain_pair`  | No route exists between the source and settlement chains.                                                         | Use a different source or settlement chain.                    |
  | `token_not_found`         | The source or settlement token is not recognized.                                                                 | Verify the token address is correct.                           |
  | `token_price_unavailable` | The swap provider cannot price the token for USD conversion.                                                      | Try a different source token.                                  |
  | `slippage_too_tight`      | The slippage tolerance is too narrow for current market conditions.                                               | Increase `slippage` in the request body (e.g. `0.5` for 0.5%). |
  | `provider_rate_limited`   | The swap provider rate-limited the request.                                                                       | Wait and retry.                                                |
  | `provider_unavailable`    | The swap provider is temporarily down.                                                                            | Retry after a short delay.                                     |
  | `unknown`                 | The swap provider returned an unclassified error. Often means the amount is too low for conversion on that route. | Try increasing the amount or using a different source token.   |
</Accordion>

<Accordion title="Step 4 — Prepare signing">
  | Status | Error                                                   | Cause                                                                                                   | Fix                                                                |
  | :----- | :------------------------------------------------------ | :------------------------------------------------------------------------------------------------------ | :----------------------------------------------------------------- |
  | `409`  | State transition error                                  | The flow is not in the `quoted` state.                                                                  | Ensure a quote was fetched (Step 3) before calling prepare.        |
  | `422`  | `Quote has expired; request a new quote before signing` | The quote is older than 60 seconds.                                                                     | Call get quote (Step 3) again, then retry prepare.                 |
  | `422`  | `Flow risk screening has not been cleared`              | Risk screening is still pending or the flow was flagged.                                                | Wait for `riskState` to become `"cleared"`, or cancel the flow.    |
  | `422`  | `Insufficient balance ...`                              | The wallet does not have enough tokens or gas. The message includes `required` and `available` amounts. | Fund the wallet with enough tokens/gas, or reduce the flow amount. |
  | `409`  | `Flow was modified by another request`                  | A concurrent request changed the flow.                                                                  | Re-fetch the flow and retry from the current state.                |
</Accordion>

<Accordion title="Step 5 — Record broadcast">
  | Status | Error                                         | Cause                                                            | Fix                                                 |
  | :----- | :-------------------------------------------- | :--------------------------------------------------------------- | :-------------------------------------------------- |
  | `400`  | `txHash is required for non-exchange sources` | Wallet-sourced flows must include the on-chain transaction hash. | Include `txHash` in the request body.               |
  | `404`  | `Flow not found`                              | Invalid flow ID or already deleted.                              | Verify the flow ID.                                 |
  | `409`  | `Transaction hash already recorded`           | The same `txHash` was already submitted.                         | The broadcast already succeeded — no action needed. |
  | `409`  | State transition error                        | The flow is not in the `signing` state.                          | Check `executionState` before calling broadcast.    |
  | `409`  | `Flow was modified by another request`        | Concurrent modification.                                         | Re-fetch the flow and retry.                        |
</Accordion>

<Accordion title="Cancel">
  | Status | Error                  | Cause                                                  | Fix                                           |
  | :----- | :--------------------- | :----------------------------------------------------- | :-------------------------------------------- |
  | `404`  | `Flow not found`       | The flow does not exist or was already deleted.        | Verify the flow ID.                           |
  | `409`  | State transition error | The flow has been broadcasted and cannot be cancelled. | Flows can only be cancelled before broadcast. |
</Accordion>

***

## Tips

* **Balance assertions:** Enable both `assertBalanceForGasCost` and `assertBalanceForTransferAmount` in prepare to catch insufficient balance before signing.
* **Quote evaluation:** Check `quote.fees.totalFeeUsd` and `quote.estimatedTimeSec` programmatically before proceeding. Cancel and retry with a different token/chain if fees are too high.
* **Quote expiry:** Quotes last 60 seconds. Sign promptly. You can re-quote multiple times — `quoteVersion` increments with each new quote.
* **Idempotency:** Use the `memo` field to store your own idempotency keys (e.g., `{ "orderId": "order_abc123" }`).
* **Error recovery:** If your process crashes mid-flow, call `GET /flow/{flowId}` to check the current `executionState` and resume from the correct step.
* **Session token lifetime:** Matches the flow's `expiresIn` (default 1 hour).
* **Balance API — required params:** Pass `includeNative=true` to include ETH/SOL in the response (omitted by default). For Solana, `networkId=101` is also required — without it the endpoint returns `[]` even for funded wallets. For EVM, use the chain ID (e.g. `networkId=8453` for Base). The indexer can lag 60–90 seconds after a deposit; if a balance check returns empty immediately after funding, wait a moment and retry.
* **Solana routing:** SOL→USDC routes vary by market conditions. The Titan protocol (Pyth oracle–dependent) is common and usually works, but can fail with `PythOracleOutdated` if oracle prices are stale. If you hit this, try adjusting `slippage` (e.g. `0.5`) to get an alternative route such as dflow. For agents, cross-chain settlement to Base (SOL → USDC on Base via MayanFinance FastMCTP) avoids oracle dependencies entirely and has zero bridge fees.
* **Solana simulation:** Pre-built Solana transactions from the flow API may fail RPC simulation with oracle errors even though they succeed on validators. Use `skipPreflight: true` when calling `sendRawTransaction` to bypass simulation.

## Quick Reference

| Step             | Method | Endpoint                               | Auth                         |
| :--------------- | :----- | :------------------------------------- | :--------------------------- |
| Create flow      | `POST` | `/server/{envId}/flow/{mode}`          | API token (`flow.write`)     |
| Attach source    | `POST` | `/sdk/{envId}/flow/{flowId}/source`    | None (returns session token) |
| Get quote        | `POST` | `/sdk/{envId}/flow/{flowId}/quote`     | Session token                |
| Prepare signing  | `POST` | `/sdk/{envId}/flow/{flowId}/prepare`   | Session token                |
| Record broadcast | `POST` | `/sdk/{envId}/flow/{flowId}/broadcast` | Session token                |
| Cancel           | `POST` | `/sdk/{envId}/flow/{flowId}/cancel`    | Session token                |
| Poll status      | `GET`  | `/sdk/{envId}/flow/{flowId}`           | None                         |

## Related

* [Fireblocks Flow overview](/overview/fireblocks-flow) — Product capabilities, modes, withdrawals, and webhooks
* [Token swaps via API](/recipes/integrations/swaps/swap-api) — Standalone token swaps without flow orchestration
* [Fireblocks Flow JavaScript SDK guide](/overview/fireblocks-flow-js-sdk) — End-to-end flow with the Dynamic JavaScript SDK
* [`attachFlowSource`](/javascript/reference/client/attach-flow-source) — SDK source attachment
* [`submitFlowTransaction`](/javascript/reference/client/submit-flow-transaction) — SDK submit (prepare + sign + broadcast)
