A server-side payment flow built on 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.
A Dynamic 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 to connect a wallet and sign
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:
The session token is returned once when you attach a source (Step 2). Store it for the duration of the flow.
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.
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 — 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 — 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 — 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).
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
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.
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/paymentAuthorization: Bearer dyn_your_api_tokenContent-Type: application/json
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}/sourceX-Dynamic-Flow-Session-Token: dft_...Content-Type: application/json
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.
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%)
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 for EVM, Solana, Sui, and Bitcoin examples. It returns a txHash you’ll report in Step 6.
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.
Report the transaction hash back to the API once your wallet returns it.
POST /sdk/{environmentId}/flow/{flowId}/broadcastX-Dynamic-Flow-Session-Token: dft_...Content-Type: application/json
{ "txHash": "0xabc123def456..."}
Response (200): Transaction with executionState: "broadcasted".
Point of no return. After this call, the transaction cannot be cancelled. The backend begins monitoring the blockchain and orchestrating settlement.
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.
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}/sourceX-Dynamic-Flow-Session-Token: dft_...Content-Type: application/json
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.
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}/quoteX-Dynamic-Flow-Session-Token: dft_...Content-Type: application/json
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%)
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 for EVM, Solana, Sui, and Bitcoin examples. It returns a txHash you’ll report in Step 6.
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.
Report the transaction hash back to the API once your wallet returns it.
POST /sdk/{environmentId}/flow/{flowId}/broadcastX-Dynamic-Flow-Session-Token: dft_...Content-Type: application/json
{ "txHash": "0xabc123def456..."}
Response (200): Transaction with executionState: "broadcasted".
Point of no return. After this call, the transaction cannot be cancelled. The backend begins monitoring the blockchain and orchestrating settlement.
Each event fires on every state transition for that axis — check data.newState to decide what action to take. See the payment webhook section 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
webhook-handler.ts
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);});
Withdrawals and money-out are supported through this HTTP API only. The Fireblocks Flow JavaScript SDK guide covers payment and deposit flows, not platform-to-user payouts.
A withdrawal sends funds from your treasury, vault, or server walletto 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 on the overview for the product model.
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.
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.
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), but credit the user’s balance or close the withdrawal request instead of treating it as an inbound deposit.
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:
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 for platform-specific guides. The examples below show browser wallet (viem/wagmi) and Node SDK patterns.
In each example, prepared is the response from Step 4.
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.
sign-evm.ts
import { parseAbi } from 'viem';const { signingPayload } = prepared.quote;// Handle ERC-20 approval if neededif (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 transactionconst txHash = await walletClient.sendTransaction({ to: signingPayload.evmTransaction.to as `0x${string}`, data: signingPayload.evmTransaction.data as `0x${string}`, value: BigInt(signingPayload.evmTransaction.value),});
serializedTransaction is a base64-encoded versioned transaction — there are no approvals. Deserialize it, sign with your connected wallet (or keypair), and broadcast. The example below uses @solana/web3.js.
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.
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 → VersionedTransactionconst vtx = VersionedTransaction.deserialize( Buffer.from(prepared.quote.signingPayload.serializedTransaction, 'base64'));// Sign — returns base58 signature (64 bytes), NOT the full signed txconst signatureBase58 = await client.signTransaction({ walletMetadata, transaction: vtx,});// Decode signature and add it back to the transactionconst sigBytes = decodeBase58(signatureBase58);const signedVtx = addSignatureToTransaction({ transaction: vtx, signature: sigBytes, signerPublicKey: new PublicKey(accountAddress),});// Broadcast — skipPreflight avoids false failures from stale oracle simulationsconst 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');
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.
Sui returns the same serializedTransaction field as Solana — a base64-encoded transaction. Deserialize and sign it with your Sui keypair via @mysten/sui, then report the resulting digest as txHash.
Bitcoin returns a psbt field — a base64-encoded unsigned PSBT. Sign each input with your wallet, finalize, extract the raw transaction, broadcast it, and report the resulting txid as txHash.
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 to provide it.
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), attach a Solana source (fromChainName: "SOL", fromChainId: "101"), and quote with a Solana fromTokenAddress (mint, or 11111111111111111111111111111111 for native SOL).
Fireblocks Flow supports the following chains. Use these values for chainName, chainId, and token addresses in your settlement config and source attachment.
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.