This is an enterprise-only feature. Please contact us to enable.
Flow lets you send a crypto payment from any supported chain and have it settled in a specific token on a specific chain.
This guide walks through the complete flow, from creating a flow to polling for settlement completion.
Prerequisites
Before starting a flow on the client, create one from your backend. The flow creation endpoint is server-side only, authenticated by an API token with flow.write scope — this is where amount, currency, settlement, and destination are fixed.
# Create a flow from your backend (returns a flowId)
curl --request POST \
--url https://app.dynamic.xyz/api/v0/server/<YOUR_ENVIRONMENT_ID>/flow/payment \
--header 'Authorization: Bearer <YOUR_API_TOKEN>' \
--header 'Content-Type: application/json' \
--data '{
"amount": "25.00",
"currency": "USD",
"settlementConfig": {
"strategy": "cheapest",
"settlements": [
{
"chainName": "EVM",
"tokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
"chainId": "1",
"symbol": "USDC",
"tokenDecimals": 18
}
]
},
"destinationConfig": {
"destinations": [
{
"chainName": "EVM",
"type": "address",
"identifier": "0xYourSettlementAddress"
}
]
},
"memo": {
"description": "Payment for order #1234"
}
}'
The response includes flow.id — pass this as the flowId to your frontend when calling the SDK functions below.
Supported Chains
Flow supports the following chains:
TRON cross-chain swaps currently route through intent-based bridges and may incur higher gas costs relative to the transfer amount.
The SDK signs internally, so the same flow works regardless of the payer’s chain — pass a wallet account on the relevant chain. The full example below pays from EVM; to pay from Solana, attach a Solana source (fromChainName: 'SOL', fromChainId: '101') and quote with a Solana token mint (or 11111111111111111111111111111111 for native SOL). submitFlowTransaction handles the Solana versioned-transaction signing for you.
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" |
Overview
The flow follows these steps:
- Attach the source wallet with
attachFlowSource
- Quote the conversion with
getFlowQuote
- Submit the transaction with
submitFlowTransaction (prepares, signs, and broadcasts)
- Poll for completion with
getFlow
Full example
import {
attachFlowSource,
getFlowQuote,
submitFlowTransaction,
getFlow,
} from '@dynamic-labs-sdk/client';
const pay = async (flowId, walletAccount) => {
// Step 1: Attach the wallet source
await attachFlowSource({
flowId,
fromAddress: walletAccount.address,
fromChainId: '1',
fromChainName: walletAccount.chain,
sourceType: 'wallet',
});
// Step 2: Get a quote
const quoted = await getFlowQuote({
flowId,
fromTokenAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
});
console.log('Quote:', quoted.quote);
// Step 3: Submit (prepare + sign + broadcast)
await submitFlowTransaction({
flowId,
walletAccount,
});
// Step 4: Poll for completion
const final = await getFlow({ flowId });
console.log('Final state:', final.settlementState);
};
Exchange source
To accept payment from a Coinbase exchange account instead of a wallet, attach an exchange source:
import { attachFlowSource, broadcastFlow } from '@dynamic-labs-sdk/client';
// Attach exchange source — returns the flow with exchange metadata
const { flow } = await attachFlowSource({
flowId,
sourceType: 'exchange',
exchangeProvider: 'coinbase',
});
// The exchange buy URL is in flow.exchangeSource.metadata.url
// Open it in an iframe or redirect the user to complete payment on Coinbase
const exchangeUrl = flow.exchangeSource?.metadata?.url;
window.open(exchangeUrl, '_blank');
// After the user completes payment on the exchange, notify the backend
await broadcastFlow({ flowId });
Handling cancellation and errors
import { submitFlowTransaction, cancelFlow } from '@dynamic-labs-sdk/client';
try {
await submitFlowTransaction({
flowId,
walletAccount,
});
} catch (error) {
if (error.message.includes('rejected')) {
// User rejected the signing request in their wallet
await cancelFlow({ flowId });
console.log('Flow cancelled');
} else {
console.error('Flow failed:', error.message);
}
}
Error reference
Each SDK function can throw errors with an HTTP status code and a message. The table below lists common errors by step, their cause, and how to resolve them.
Create flow (backend)
| Status | Error | Cause | Fix |
|---|
400 | Invalid flow mode. Expected one of: payment, deposit, withdraw | The mode path parameter is not a valid flow mode. | Use payment, deposit, or withdraw. |
401 | Unauthorized | Missing or invalid API token, or the token does not have flow.write scope. | Verify the Authorization: Bearer dyn_... header and the token scope in the dashboard. |
422 | Each chain should only have one destination config | Duplicate chain entries in destinationConfig.destinations. | Remove duplicate chain entries 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 for every settlement chain. |
Attach source (attachFlowSource)
| Status | Error | Cause | Fix |
|---|
400 | fromAddress, fromChainId, and fromChainName are required for wallet sources | Missing source fields when sourceType is wallet. | Pass fromAddress, fromChainId, and fromChainName from the wallet account. |
400 | Chain not supported | The source chain/chainId combination is not supported for swaps. | Use a supported chain. See Supported Chains. |
403 | Flow is blocked by sanctions | The source address failed sanctions screening. | Use a different source wallet. |
404 | Flow not found | The flowId does not exist, the flow has been deleted, or the environment ID is wrong. | Verify the flowId and environment. |
409 | State transition error | The flow is not in a state that allows attaching a source (e.g. already broadcasted). | Check executionState with getFlow and start a new flow if needed. |
Get quote (getFlowQuote)
| Status | Error | Cause | Fix |
|---|
400 | fromChainName is required / fromChainId is required / fromAddress is required | Source was not attached before requesting a quote. | Call attachFlowSource first. |
400 | fromTokenAddress is required and could not be resolved for the specified chain | The token could not be determined for the source chain. | Pass fromTokenAddress explicitly in the quote request. |
403 | Flow is blocked by risk screening | The flow’s risk screening flagged the source. | Cancel the flow and retry with a different source. |
409 | State transition error | The flow is not in a valid state for quoting (e.g. already signed). | Check executionState. You can re-quote from source_attached or quoted states. |
422 | No quotes available for any settlement token. | No swap provider returned a valid quote. The response includes a structured quoteFailures array with per-settlement failure details. | Inspect the quoteFailures array. See the reason codes below. |
Quote failure response format:
When no quotes are available, the 422 response includes a quoteFailures array:
{
"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 identifies the settlement token that failed (symbol on chainName/chainId) and the failure reason.
Quote failure reasons:
| Reason | Meaning | Fix |
|---|
amount_below_minimum | The flow amount is too low for the selected token/route. | Increase the amount or choose 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, chain, or reduce the amount. |
unsupported_chain_pair | No swap route exists between the source and settlement chains. | Use a different source chain or settlement chain. |
token_not_found | The source or settlement token is not recognized by the swap provider. | Verify the token address. |
token_price_unavailable | The swap provider cannot price the token for conversion. | Try a different source token. |
slippage_too_tight | The slippage tolerance is too narrow for market conditions. | Increase slippage in the quote request (e.g. 0.5 for 0.5%). |
provider_rate_limited | The swap provider rate-limited the request. | Wait a moment 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. |
Prepare signing (prepareFlowSigning / submitFlowTransaction)
| Status | Error | Cause | Fix |
|---|
409 | State transition error | The flow is not in the quoted state. | Ensure a quote was fetched first. |
422 | Quote has expired; request a new quote before signing | The quote is older than 60 seconds. | Call getFlowQuote again, then retry. |
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. |
422 | Insufficient balance ... | The wallet does not have enough tokens or gas to cover the transaction. Includes required and available amounts. | Fund the wallet or choose a smaller amount. |
409 | Flow was modified by another request | A concurrent request changed the flow state. | Re-fetch the flow and retry from the current state. |
Broadcast (broadcastFlow)
| Status | Error | Cause | Fix |
|---|
400 | txHash is required for non-exchange sources | Wallet sources must include the transaction hash. | This is handled automatically by submitFlowTransaction. If calling broadcastFlow directly, include txHash. |
409 | Transaction hash already recorded | The same txHash was submitted twice. | The broadcast already succeeded — no action needed. |
409 | State transition error | The flow is not in the signing state. | Check executionState before calling broadcast. |
Cancel (cancelFlow)
| Status | Error | Cause | Fix |
|---|
404 | Flow not found | The flow does not exist or was already deleted. | Verify the flowId. |
409 | State transition error | The flow has already been broadcasted and cannot be cancelled. | Flows can only be cancelled before broadcast. |
Polling for status
After submission, poll getFlow to track progress through execution and settlement states:
import { getFlow } from '@dynamic-labs-sdk/client';
const TERMINAL_EXECUTION = ['cancelled', 'expired', 'failed'];
const TERMINAL_SETTLEMENT = ['completed', 'failed'];
const pollFlow = async (flowId) => {
const poll = async () => {
const flow = await getFlow({ flowId });
if (
TERMINAL_EXECUTION.includes(flow.executionState) ||
TERMINAL_SETTLEMENT.includes(flow.settlementState)
) {
return flow;
}
await new Promise((resolve) => setTimeout(resolve, 3000));
return poll();
};
return poll();
};