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

# Flow payments API guide

> Create, complete, track, and cancel Flow payments with HTTP requests, for every payment source and mode.

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

This guide runs a [Flow payment](https://docs.dynamic.xyz/docs/flow/payments.md) with HTTP requests, so it works from any backend, agent, or runtime that can send `fetch` requests. Your server creates a flow, the payer's page completes it from a wallet, a deposit address, or an exchange, and your server tracks it until the funds reach your destination.

The states and rules behind each step are explained in [Payment lifecycle and states](https://docs.dynamic.xyz/docs/flow/lifecycle-walkthrough.md). To run the payer's side from a web or React Native app instead, use the [JavaScript SDK](https://docs.dynamic.xyz/docs/javascript/reference/flow-getting-started.md), which sends these requests for you.

## Prerequisites

* A Dynamic environment with Flow payments enabled. A create request for an environment without it returns `400`.
* An environment API token with the `flow.write` permission, created in the [Dynamic Console](https://console.dynamic.xyz). Keep it on your server and read it from an environment variable or a secrets manager.
* For wallet payments, a wallet that can sign on the payer's chain. Your app connects it; this API does not.

The examples read these shell variables:

| Variable | Holds |
| - | - |
| `DYNAMIC_ENVIRONMENT_ID` | Your environment ID |
| `DYNAMIC_API_TOKEN` | Your environment API token, used only in server requests |
| `FLOW_ID` | The `id` of the flow your server created |
| `FLOW_SESSION_TOKEN` | The session token returned when the payer attached a source |

## How a payment runs

1. Your server creates a flow and passes its `id` to the payer's page.
2. The payer's page attaches a payment source. The response carries a session token for the payer's next requests.
3. The payer pays:
   * **Wallet.** Get a quote, prepare signing, sign and send the transaction, then report its hash.
   * **Deposit address.** Get a quote, which returns a deposit address, and the payer sends funds to it.
   * **Exchange.** Open the exchange's purchase link, and the payer pays on the exchange.
4. Your server tracks the flow until `settlementState` is `completed`, which means the payment is done.

The mode you choose when you create the flow changes who sets the amount and who signs. The requests are the same in every mode.

| Mode | Who sets the amount | `currency` | Quote `amount` | Who pays and signs |
| - | - | - | - | - |
| `payment` | You, as a fixed amount | `USD` or `SETTLEMENT_TOKEN` | Rejected | The payer |
| `deposit` | You suggest an amount, and the payer can change it on each quote | `USD` | Accepted | The payer |
| `withdraw` | Your platform, as a fixed amount | `USD` | Rejected | Your platform's wallet |

[Amounts and currencies](#amounts-and-currencies) gives the rules for each mode, and [Confirm what was delivered](#confirm-what-was-delivered) explains how the mode decides what a completed payment means.

## Base URL and authentication

All requests go to this base URL:

```
https://app.dynamic.xyz/api/v0
```

Each request uses one of three credentials:

| Requests | Credential | Header |
| - | - | - |
| Server requests (`/server/{environmentId}/…`) | Environment API token with `flow.write` | `Authorization: Bearer <API token>` |
| Read a flow, list exchanges, attach a source | None. The flow ID identifies the flow | None |
| Quote, prepare, broadcast, cancel | The flow's session token, which starts with `dft_` | `X-Dynamic-Flow-Session-Token: <token>` |

The attach request returns the session token once, in `sessionToken`, with `Cache-Control: no-store`. Store it on the payer's device for the rest of the payment. It stays valid until the flow's `expiresAt`, and it stops working earlier when the payer attaches a source again, when the flow is cancelled, fails, or expires, or when settlement reaches `completed` or `failed`.

A device without the session token, such as the payer's second device, can attach a source again to get a new one, as long as the payment has not been sent. While the flow is in `signing`, attaching a source again requires the current session token.

> [!WARNING]
>
> Do not send `Authorization: Bearer` to the payer endpoints under `/sdk/{environmentId}/…`. They accept only the session token header, and a Bearer header returns `401`. Never send your API token to a browser or app.

The flow ID, together with your environment ID, is enough to read the payer's view of a flow and to attach a source before the payment is sent. Share it only with the payer, as described in [Access and data](https://docs.dynamic.xyz/docs/flow/security.md#treat-the-flow-id-as-a-capability).

## Endpoints

| Method | Path | Credential | Purpose |
| - | - | - | - |
| `POST` | `/server/{environmentId}/flow/{mode}` | API token | [Create a flow](#create-a-flow) |
| `POST` | `/server/{environmentId}/payment-links` | API token | [Create a flow with a payment link](#payment-links) |
| `GET` | `/server/{environmentId}/flow/{flowId}` | API token | [Read the full flow](#read-a-flow-from-your-server) |
| `POST` | `/server/{environmentId}/flow/{flowId}/cancel` | API token | [Cancel a flow in `initiated`](#cancel-a-flow) |
| `GET` | `/server/{environmentId}/flow/fees/balances` | API token | [Check fee balances](https://docs.dynamic.xyz/docs/flow/fees.md#check-claimable-balances) |
| `POST` | `/server/{environmentId}/flow/fees/claim` | API token | [Start a fee claim](https://docs.dynamic.xyz/docs/flow/fees.md#claim-accrued-fees) |
| `POST` | `/server/{environmentId}/flow/fees/claim/submit` | API token | [Submit a fee claim](https://docs.dynamic.xyz/docs/flow/fees.md#claim-accrued-fees) |
| `GET` | `/sdk/{environmentId}/flow/{flowId}` | None | [Read the payer's view of the flow](#track-progress) |
| `GET` | `/sdk/{environmentId}/flow/{flowId}/exchanges` | None | [List the exchanges the flow supports](#pay-from-an-exchange) |
| `POST` | `/sdk/{environmentId}/flow/{flowId}/source` | None, or the session token from `signing` | Attach a payment source and get a session token |
| `POST` | `/sdk/{environmentId}/flow/{flowId}/quote` | Session token | Get a quote, and a deposit address for that source |
| `POST` | `/sdk/{environmentId}/flow/{flowId}/prepare` | Session token | Get the transaction to sign (wallet sources) |
| `POST` | `/sdk/{environmentId}/flow/{flowId}/broadcast` | Session token | Report the sent transaction (wallet sources) |
| `POST` | `/sdk/{environmentId}/flow/{flowId}/cancel` | Session token | [Cancel before the payment is sent](#cancel-a-flow) |

Every request is valid only from certain states. A request that the flow's current state does not allow returns `409` with the code `flow_invalid_transition`. [API errors](https://docs.dynamic.xyz/docs/flow/api-errors.md) lists the errors each endpoint returns and which ones to retry.

## Create a flow

Your server creates the flow with its API token. Put the mode in the path: `payment`, `deposit`, or `withdraw`.

```bash theme={"system"}
curl --request POST \
  --url "https://app.dynamic.xyz/api/v0/server/$DYNAMIC_ENVIRONMENT_ID/flow/payment" \
  --header "Authorization: Bearer $DYNAMIC_API_TOKEN" \
  --header 'Content-Type: application/json' \
  --data '{
    "amount": "25.00",
    "currency": "USD",
    "settlementConfig": {
      "strategy": "cheapest",
      "settlements": [
        {
          "chainName": "EVM",
          "chainId": "8453",
          "tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
          "symbol": "USDC",
          "tokenDecimals": 6
        }
      ]
    },
    "destinationConfig": {
      "destinations": [
        { "chainName": "EVM", "type": "address", "identifier": "0xYourEvmDestinationAddress" }
      ]
    },
    "memo": { "orderId": "order_abc123" }
  }'
```

To accept more tokens or chains, for example USDC on Solana as well, add an entry to `settlements` for each, and a destination for each chain family.

A successful request returns `201` with the new flow wrapped in `flow`. This response is abridged:

```json theme={"system"}
{
  "flow": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "mode": "payment",
    "amount": "25.00",
    "currency": "USD",
    "executionState": "initiated",
    "settlementState": "none",
    "riskState": "unknown",
    "memo": { "orderId": "order_abc123" },
    "expiresAt": "2026-06-16T12:00:00.000Z"
  }
}
```

Store `flow.id` and pass it to the payer's page. Only create and payment-link responses wrap the flow in `flow`, and the attach response returns it in `flow` next to the session token. The quote, prepare, broadcast, cancel, and read responses are the flow object itself.

| Field | Required | Description |
| - | - | - |
| `amount` | Yes | The amount as a decimal string in `currency`. In `deposit` mode it is a suggestion the payer can change. See [Amounts and currencies](#amounts-and-currencies). |
| `currency` | Yes | `USD`, or `SETTLEMENT_TOKEN` in `payment` mode with exactly one settlement token. |
| `settlementConfig.settlements` | Yes | The tokens you accept, each with `chainName`, `chainId`, `tokenAddress`, `symbol`, and `tokenDecimals`. |
| `settlementConfig.strategy` | Yes | How a route is chosen when several can settle the payment: `cheapest`, `fastest`, or `preferred_order`. See [Routing strategy](https://docs.dynamic.xyz/docs/flow/settlements.md#routing-strategy). |
| `destinationConfig.destinations` | Yes | Where funds land: one destination per `chainName`, each with `type: "address"` and the address in `identifier`. Every settlement chain needs a destination. |
| `memo` | No | Any JSON object for your own reference, such as an order ID. The payer's view of the flow includes it, and webhooks do not, so do not put secrets or personal data in it. |
| `expiresIn` | No | Seconds until the flow expires. The default is `86400` (24 hours), and larger values than `31536000` (365 days) are reduced to it. Expiry applies only before the payment is sent. See [Expiry and cancellation](https://docs.dynamic.xyz/docs/flow/expiry-and-cancellation.md). |
| `feeConfig` | No | Your own fee on the payment. See [Fees API](https://docs.dynamic.xyz/docs/flow/fees.md). |
| `disableSwaps` | No | When `true`, the payer must pay with a token and chain that match a settlement. See [Disable swaps](#disable-swaps). |
| `pegStablecoins` | No | When `true`, known stablecoins in your settlements are priced 1:1 with the flow currency instead of at market price. |
| `uiConfig` | No | Display settings for a payment UI. See [UI configuration](#ui-configuration). |

Everything in the create request is fixed once the flow exists. Only the payer's source, and in `deposit` mode the amount, can change later.

Dynamic screens your destination addresses during this request. If a destination is blocked, the request returns `403` and no flow is created. [Screening](https://docs.dynamic.xyz/docs/flow/screening.md) describes what is screened, including the address screening webhooks a block or alert sends.

### UI configuration

`uiConfig` carries display settings for whichever UI shows the payment: `merchantName`, `merchantLogoUrl`, `returnUrl`, `termsOfServicesUrl`, `externalPaymentId`, and `enabledPaymentMethods`. The origins of `returnUrl` and `termsOfServicesUrl` must be registered [CORS origins](https://docs.dynamic.xyz/docs/platform/console/security.md#allowed-cors-origin) for the environment.

`uiConfig` applies only to UIs that read it: Dynamic's hosted page for [payment links](#payment-links), or your own UI reading it from the flow. The API does not enforce it. In particular, `enabledPaymentMethods` lists the sources a UI should offer, but it does not stop a payer from attaching another source with direct requests.

### Disable swaps

Set `disableSwaps: true` when the payer must pay with a token and chain that already match one of your settlements. The value is fixed when the flow is created, and the flow's responses include `disableSwaps: true` only when it is set.

When `disableSwaps` is `true`:

* Attaching an incompatible source still succeeds. The quote request rejects it.
* A wallet or deposit-address quote succeeds when the source `chainName`, `chainId`, and token match a settlement that has a destination. Token addresses are compared case-insensitively.
* A matching wallet quote is a direct transfer. A matching deposit address is issued as usual, and a later quote for the same chain and token reuses it.
* A quote that needs a swap or bridge returns `422` with `Swaps are disabled for this flow. The source chain/token must match a configured settlement.`
* Exchange payments are unaffected, because the exchange buys the settlement token directly.

A quote request without `fromTokenAddress` uses the chain's native token. A flow that settles in USDC therefore rejects that quote, unless the native token is itself a settlement. [Accept a same-token payment](https://docs.dynamic.xyz/docs/flow/same-token-payment.md) walks through a complete flow with `disableSwaps`.

### Testnets

Base Sepolia is the supported testnet. Use `chainName: "EVM"` and `chainId: "84532"` in `settlementConfig.settlements` and `destinationConfig.destinations`, as `fromChainId` when the payer attaches a wallet, and put the token's address in `fromTokenAddress` when requesting a quote. [Supported chains](https://docs.dynamic.xyz/docs/flow/supported-chains.md#testnets) lists the Base Sepolia token addresses.

## Pay with a wallet

The payer connects a wallet in your app, and the payer's page sends these requests in order.

### Attach the wallet

```bash theme={"system"}
curl --request POST \
  --url "https://app.dynamic.xyz/api/v0/sdk/$DYNAMIC_ENVIRONMENT_ID/flow/$FLOW_ID/source" \
  --header 'Content-Type: application/json' \
  --data '{
    "sourceType": "wallet",
    "fromAddress": "0xPayerWalletAddress",
    "fromChainId": "8453",
    "fromChainName": "EVM"
  }'
```

| Field | Description |
| - | - |
| `sourceType` | `wallet` |
| `fromAddress` | The payer's wallet address |
| `fromChainId` | The chain the payer pays from, for example `"8453"` for Base or `"101"` for Solana |
| `fromChainName` | The chain family: `EVM`, `SOL`, `BTC`, `SUI`, or `TRON`. It cannot change until another source is attached |

The response is `200` with the flow in `source_attached`, the session token, and its expiry:

```json theme={"system"}
{
  "flow": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "executionState": "source_attached",
    "riskState": "cleared",
    "sourceType": "wallet"
  },
  "sessionToken": "dft_…",
  "sessionExpiresAt": "2026-06-16T12:00:00.000Z"
}
```

Dynamic screens the wallet address during this request. When the wallet is blocked, the request returns `403` with the code `sanctions_blocked`, and the flow's `riskState` becomes `blocked`. The payer can attach a different wallet, or choose another source, on the same flow. You do not need to cancel it. When screening does not return a result, `riskState` is `unknown`: screening did not return a result, and the payment can continue.

### Get a quote

```bash theme={"system"}
curl --request POST \
  --url "https://app.dynamic.xyz/api/v0/sdk/$DYNAMIC_ENVIRONMENT_ID/flow/$FLOW_ID/quote" \
  --header "X-Dynamic-Flow-Session-Token: $FLOW_SESSION_TOKEN" \
  --header 'Content-Type: application/json' \
  --data '{ "fromTokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" }'
```

| Field | Description |
| - | - |
| `fromTokenAddress` | Optional. The token the payer pays with. When omitted, the chain's native token is used. See [Supported chains and native tokens](#supported-chains-and-native-tokens). |
| `fromChainId` | Optional. A different chain ID in the same chain family as the attached source. |
| `slippage` | Optional. Slippage tolerance for a swap, from `0` to `0.1`. For example, `0.005` is 0.5%. |
| `amount` | Optional, `deposit` mode only. The amount the payer chose. See [Amounts and currencies](#amounts-and-currencies). |
| `amountCurrency` | Optional, `deposit` mode only. `USD` (the default) or `SOURCE_TOKEN`. |

The response is the flow in `quoted`, with a new `quote`:

```json theme={"system"}
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "executionState": "quoted",
  "quoteVersion": 1,
  "quote": {
    "version": 1,
    "fromAmount": "25000000",
    "fromTokenInfo": { "symbol": "USDC", "decimals": 6 },
    "toAmount": "25000000",
    "toTokenInfo": { "symbol": "USDC", "decimals": 6 },
    "fees": {
      "totalFeeUsd": "0.01",
      "gasEstimate": { "usdValue": "0.01", "nativeValue": "2400000000000", "nativeSymbol": "ETH" }
    },
    "createdAt": "2026-06-15T12:01:00.000Z",
    "expiresAt": "2026-06-15T12:02:00.000Z"
  }
}
```

`fromAmount` is what the payer sends and `toAmount` is what the quote expects to reach your destination. Both are integer strings in the token's smallest unit, so divide by `10 ** decimals` from `fromTokenInfo` or `toTokenInfo` to display them. `fees.totalFeeUsd` is the estimated cost of the payment in USD.

When the payer's token and chain match a settlement, the quote is a direct transfer. Otherwise Dynamic converts the payment with a swap or bridge, as described in [Conversions](https://docs.dynamic.xyz/docs/flow/conversions.md).

A wallet quote is valid for 60 seconds, until `quote.expiresAt`. After that, request a new quote with the same `fromChainId` and `fromTokenAddress`. Each quote increments `quoteVersion`, and the flow stays in `quoted`.

If the payer's wallet is blocked, the quote returns `403` with the code `risk_blocked`. In `deposit` mode, a quote that changes the amount screens the wallet again and can return `403` with `sanctions_blocked`. When no route can settle the payment, the quote returns `422` with a `quoteFailures` list that gives the reason for each settlement token, described in [API errors](https://docs.dynamic.xyz/docs/flow/api-errors.md#quote-failures).

### Prepare signing

```bash theme={"system"}
curl --request POST \
  --url "https://app.dynamic.xyz/api/v0/sdk/$DYNAMIC_ENVIRONMENT_ID/flow/$FLOW_ID/prepare" \
  --header "X-Dynamic-Flow-Session-Token: $FLOW_SESSION_TOKEN" \
  --header 'Content-Type: application/json' \
  --data '{}'
```

| Field | Default | Description |
| - | - | - |
| `assertBalanceForGasCost` | `true` | Check that the wallet holds enough for gas, and return `422` with `insufficient_balance` if not |
| `assertBalanceForTransferAmount` | `true` | Check that the wallet holds enough to pay, and return `422` with `insufficient_balance` if not |

The response is the flow in `signing`, with the transaction to sign in `quote.signingPayload`:

```json theme={"system"}
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "executionState": "signing",
  "quote": {
    "version": 1,
    "signingPayload": {
      "chainName": "EVM",
      "chainId": "8453",
      "evmTransaction": {
        "to": "0xContractAddress",
        "data": "0xCalldata",
        "value": "0x0",
        "gasLimit": "0x5208"
      },
      "evmApproval": {
        "tokenAddress": "0xTokenAddress",
        "spenderAddress": "0xSpenderAddress",
        "amount": "25000000"
      }
    }
  }
}
```

Prepare fails with `422` and the code `quote_expired` when the quote is older than 60 seconds. Request a new quote and prepare again. It also fails with `422` while the flow's `riskState` is `blocked`. The risk state does not change on its own, so the payer must attach a different source.

### Sign and send the transaction

The payer's wallet signs and sends `quote.signingPayload` to the network. The payload depends on the chain, as described in [Signing the transaction by chain](#signing-the-transaction-by-chain). Keep the transaction hash the wallet returns.

If the payer declines in the wallet, nothing was sent. Request a new quote, prepare again, and ask the payer to sign again. If sending fails for another reason, read the flow before trying again, because the transaction might already be on its way.

### Report the transaction

Send the transaction hash to Dynamic as soon as the wallet returns it:

```bash theme={"system"}
curl --request POST \
  --url "https://app.dynamic.xyz/api/v0/sdk/$DYNAMIC_ENVIRONMENT_ID/flow/$FLOW_ID/broadcast" \
  --header "X-Dynamic-Flow-Session-Token: $FLOW_SESSION_TOKEN" \
  --header 'Content-Type: application/json' \
  --data '{ "txHash": "0xTransactionHash" }'
```

This request records the hash. It does not send the transaction, which the wallet already did. The response is the flow in `broadcasted`, and Dynamic watches the chain from here.

> [!WARNING]
>
> After this request the payment cannot be cancelled and the source cannot change. The flow continues until it is confirmed or fails.

A `409` with the code `flow_tx_hash_already_recorded` means a flow already recorded this hash. It does not show that this flow recorded it, so read the flow to see its current state. Then [track the flow](#track-progress) until settlement completes.

## Pay with a deposit address

The payer sends funds to an address generated for the payment, from any wallet or exchange. Nothing is connected or signed in your app. Deposit addresses are available on EVM chains, Solana, Bitcoin, and TRON.

### Attach a deposit address source

```bash theme={"system"}
curl --request POST \
  --url "https://app.dynamic.xyz/api/v0/sdk/$DYNAMIC_ENVIRONMENT_ID/flow/$FLOW_ID/source" \
  --header 'Content-Type: application/json' \
  --data '{
    "sourceType": "deposit_address",
    "fromChainId": "1",
    "fromChainName": "BTC",
    "refundAddress": "bc1qPayerRefundAddress"
  }'
```

`fromChainId`, `fromChainName`, and `refundAddress` are required, and `fromAddress` is rejected. The refund address must be valid on the payer's chain. It is where the deposit provider returns funds it does not accept. The response has the same shape as for a wallet, including the session token.

Dynamic does not screen the funder of a deposit-address payment, and the flow's `riskState` becomes `cleared`. See [Screening](https://docs.dynamic.xyz/docs/flow/screening.md#what-is-not-screened).

### Get the deposit address

```bash theme={"system"}
curl --request POST \
  --url "https://app.dynamic.xyz/api/v0/sdk/$DYNAMIC_ENVIRONMENT_ID/flow/$FLOW_ID/quote" \
  --header "X-Dynamic-Flow-Session-Token: $FLOW_SESSION_TOKEN" \
  --header 'Content-Type: application/json' \
  --data '{}'
```

Pass `fromTokenAddress` for a token, or omit it for the chain's native token. The response is the flow in `quoted`, with the address and the amount to send:

```json theme={"system"}
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "executionState": "quoted",
  "depositAddress": "bc1qGeneratedDepositAddress",
  "quote": {
    "version": 1,
    "depositAddress": "bc1qGeneratedDepositAddress",
    "fromAmount": "38200",
    "fromTokenInfo": { "symbol": "BTC", "decimals": 8 },
    "toAmount": "25000000",
    "toTokenInfo": { "symbol": "USDC", "decimals": 6 },
    "fees": { "totalFeeUsd": "0.42" }
  }
}
```

Show the payer `depositAddress`, the amount `quote.fromAmount` converted with `quote.fromTokenInfo.decimals`, the token, and the chain.

The quoted amount is valid for 60 seconds. The address stays open until the flow ends, but the deposit provider guarantees the quoted amount for only 60 seconds, and `quote.expiresAt` shows the flow's expiry rather than that window. If the payer has not sent funds after 60 seconds, request a new quote to refresh the amount. A new quote for the same chain and token usually keeps the same address. Funds sent later can still arrive, but the delivered amount can differ from the quote.

A QR code that also carries the amount saves the payer from typing it. Use the chain's URI scheme, and always show the address as text too, because wallets differ in how they read the amount:

| Chain | URI |
| - | - |
| BTC | `bitcoin:<address>?amount=<decimal>` |
| SOL | `solana:<address>?amount=<decimal>` |
| EVM (token) | `ethereum:<tokenContract>/transfer?address=<depositAddress>&uint256=<rawAmount>` (EIP-681) |
| EVM (native) | `ethereum:<depositAddress>?value=<weiAmount>` (EIP-681) |

For BTC and SOL, convert `fromAmount` to a decimal. For EVM, use the integer `fromAmount` as is.

### Wait for the deposit

A deposit-address payment has no prepare, sign, or broadcast step, and a broadcast request returns `409` with `flow_invalid_transition`. Dynamic watches the address and moves the flow:

| `executionState` | Meaning |
| - | - |
| `quoted` | Waiting for the payer to send funds |
| `source_detected` | The deposit was seen and is being confirmed. The source can no longer change |
| `source_confirmed` | The deposit is confirmed, and settlement continues until `completed` or `failed` |
| `expired` | No deposit was seen before the flow's `expiresAt` |

If the payer switches chain or token, attach the source again and request a new quote, which returns a new address. Funds already sent to an earlier address for the same flow can still complete the payment, so tell the payer which address is current. When a payment arrives at an earlier address, the flow's `fromChainId`, `fromChainName`, and `fromToken` describe that deposit.

In `payment` mode, a deposit smaller than the quote is not accepted, and the provider attempts to return it to the refund address. In `deposit` mode, a smaller deposit settles for what arrived. Refunds are performed by the provider, not by Dynamic, and a refund can fail. See [Failures and refunds](https://docs.dynamic.xyz/docs/flow/failures-and-refunds.md#refunds).

Funds sent to the address after the flow is cancelled or expired can still reach your destination without the flow recording them, and you handle any refund to the payer. See [Expiry and cancellation](https://docs.dynamic.xyz/docs/flow/expiry-and-cancellation.md#funds-sent-after-cancellation-or-expiry).

The flow's `txHash` is the deposit transaction as the deposit provider reported it, and it is absent when the provider reports none.

## Pay from an exchange

The payer pays on an exchange, such as Coinbase, through a purchase link the flow provides. An exchange is available for a flow when it is enabled for your environment in the [Dynamic Console](https://console.dynamic.xyz) and the flow settles in a token and on a network that the exchange supports, with a destination on that network. For Coinbase, the settlement token is USDC.

### List the exchanges

```bash theme={"system"}
curl --url "https://app.dynamic.xyz/api/v0/sdk/$DYNAMIC_ENVIRONMENT_ID/flow/$FLOW_ID/exchanges"
```

```json theme={"system"}
{
  "exchanges": [
    {
      "id": "8c1f5b7e-2d4a-4f6b-9e3c-1a2b3c4d5e6f",
      "key": "coinbase",
      "label": "Coinbase",
      "iconUrl": "https://…/coinbase.svg"
    }
  ]
}
```

Show only the exchanges in this list. It is empty when no exchange can pay this flow.

### Attach the exchange

```bash theme={"system"}
curl --request POST \
  --url "https://app.dynamic.xyz/api/v0/sdk/$DYNAMIC_ENVIRONMENT_ID/flow/$FLOW_ID/source" \
  --header 'Content-Type: application/json' \
  --data '{ "sourceType": "exchange", "exchangeProvider": "coinbase" }'
```

Set `exchangeProvider` to the exchange's `key`. The response carries the session token and the flow in `source_attached`, with the purchase link in `flow.exchangeSource.metadata.url`. Dynamic does not screen the funder of an exchange payment.

### Send the payer to the exchange

Open `exchangeSource.metadata.url` in a new tab from the payer's click. The payer completes the purchase on the exchange, and the exchange confirms the payment to Dynamic.

Do not request a quote, prepare, or broadcast for an exchange source. [Track the flow](#track-progress) instead: it moves from `source_attached` to `source_confirmed`, and settlement moves from `none` to `completed`.

Attaching the exchange again returns a new purchase link. A purchase already completed through an earlier link can still reach your destination.

An exchange payment has no source transaction hash, because the payer pays on the exchange. The webhooks identify it with `exchangeTransactionId`, the exchange's ID for the payment. In `payment` mode, an exchange payment that arrives short is not accepted, and the flow fails.

## Withdrawals

In `withdraw` mode, your platform pays out a fixed `USD` amount to an address you set in `destinationConfig`, for example a user's wallet. Your platform attaches its own wallet as a `wallet` source and runs the same requests as in [Pay with a wallet](#pay-with-a-wallet), signing the prepared transaction with a wallet you control, such as a [server wallet](https://docs.dynamic.xyz/docs/embedded-wallets/server-wallets.md).

## Amounts and currencies

| Value | Meaning |
| - | - |
| `amount` and `currency` on the flow | The amount you requested, as a decimal string. With `SETTLEMENT_TOKEN` it is in whole units of the settlement token, for example `"0.5"` for 0.5 ETH |
| `currencyTokenInfo` | The settlement token's `symbol` and `decimals`, for a `SETTLEMENT_TOKEN` flow |
| `amountUsd` | The USD value of the amount. It equals `amount` for a `USD` flow. For a `SETTLEMENT_TOKEN` flow it is priced when a quote is created, and absent until then or when no price is available |
| `quote.fromAmount`, `quote.toAmount` | What the payer sends and what the quote expects to deliver, as integer strings in each token's smallest unit. Use `quote.fromTokenInfo` and `quote.toTokenInfo` for symbols and decimals |
| `quote.fees.totalFeeUsd` | The estimated cost of the payment, in USD |
| `settlement.amount` | What actually reached your destination, in the settlement token's smallest unit. See [Confirm what was delivered](#confirm-what-was-delivered) |

`SETTLEMENT_TOKEN` is available only in `payment` mode, and only when the flow has exactly one settlement token.

In `deposit` mode, the amount you set is a suggestion. The payer can change it on any quote with `amount` and `amountCurrency`:

* `amountCurrency: "USD"` (the default) takes an amount with at most two decimals.
* `amountCurrency: "SOURCE_TOKEN"` takes an amount in whole units of the token the payer pays with, with at most that token's decimals.
* The new amount replaces the flow's `amount`.
* A quote without `amount` uses the flow's current amount in USD. To keep a token amount, send it on every quote.
* When the source is a wallet, changing the amount screens the wallet again.

A quote with `amount` in `payment` or `withdraw` mode returns `400` with the code `amount_not_editable`. An amount that is malformed, has too many decimals, or is too small returns `400` with `amount_invalid`.

## Signing the transaction by chain

The prepare response puts the transaction in `quote.signingPayload`. Its fields depend on the chain:

| Chain | Fields | Sign with |
| - | - | - |
| EVM | `evmTransaction` (`to`, `data`, `value`, `gasLimit`), and `evmApproval` for an ERC-20 payment | Any EVM wallet client, such as [viem](https://viem.sh) or wagmi |
| Solana | `serializedTransaction`, a base64 versioned transaction | [`@solana/web3.js`](https://solana.com/docs/clients/javascript) |
| Sui | `serializedTransaction`, a base64 transaction | [`@mysten/sui`](https://sdk.mystenlabs.com/typescript) |
| Bitcoin | `psbt`, a base64 unsigned PSBT | Any PSBT signer, such as `bitcoinjs-lib` |
| TRON | `tronTransaction` (`rawDataHex`, `to`, `value`) | TronWeb or TronLink |

Any Dynamic SDK that can sign on the chain works, including the [Node SDK](https://docs.dynamic.xyz/docs/node/quickstart.md) for server wallets. In each example below, `prepared` is the prepare response.

### EVM

When `evmApproval` is present, send the approval transaction first and wait for it, then send the payment transaction. `walletClient` is your app's connected viem wallet client.

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

declare const walletClient: WalletClient & {
  waitForTransactionReceipt: (args: { hash: `0x${string}` }) => Promise<unknown>;
};
const account = walletClient.account!;
const { signingPayload } = prepared.quote;

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

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

#### EVM with the Node SDK

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

const client = new DynamicEvmWalletClient({
  environmentId: process.env.DYNAMIC_ENVIRONMENT_ID!,
});
await client.authenticateApiToken(process.env.DYNAMIC_API_TOKEN!);

const walletClient = await client.getWalletClient({
  accountAddress, // your server wallet's address
  chainId: 8453,
  rpcUrl: process.env.BASE_RPC_URL!,
});

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

if (evmApproval) {
  // Send and confirm the approval first, as in the previous example.
}

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 [versioned transaction](https://solana.com/docs/core/transactions/versions), with no separate approval. Deserialize it, have the payer's wallet sign it, and send it. `signTransaction` stands for your app's connected Solana wallet.

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

declare function signTransaction(
  transaction: VersionedTransaction,
): Promise<VersionedTransaction>;

const connection = new Connection(process.env.SOLANA_RPC_URL!, 'confirmed');

const transaction = VersionedTransaction.deserialize(
  Buffer.from(prepared.quote.signingPayload.serializedTransaction, 'base64'),
);
const signed = await signTransaction(transaction);

const txHash = await connection.sendRawTransaction(signed.serialize());
```

#### Solana with the Node SDK

The Node SDK's `DynamicSvmWalletClient.signTransaction` returns only the signature, as a base58 string, not a signed transaction. Add the signature to the transaction before sending it.

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

const client = new DynamicSvmWalletClient({
  environmentId: process.env.DYNAMIC_ENVIRONMENT_ID!,
});
await client.authenticateApiToken(process.env.DYNAMIC_API_TOKEN!);

const transaction = VersionedTransaction.deserialize(
  Buffer.from(prepared.quote.signingPayload.serializedTransaction, 'base64'),
);

const signatureBase58 = await client.signTransaction({
  walletMetadata, // your server wallet's metadata
  transaction,
});

const signed = addSignatureToTransaction({
  transaction,
  signature: decodeBase58(signatureBase58),
  signerPublicKey: new PublicKey(accountAddress),
});

const connection = new Connection(process.env.SOLANA_RPC_URL!, 'confirmed');
const txHash = await connection.sendRawTransaction(signed.serialize());
```

### Sui

`serializedTransaction` is a base64 transaction. Deserialize and sign it with the payer's Sui wallet through [`@mysten/sui`](https://sdk.mystenlabs.com/typescript), send it, and report the resulting digest as `txHash`.

### Bitcoin

`psbt` is a base64 unsigned [PSBT](https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki). Sign each input with the payer's wallet, finalize the PSBT, extract and send the raw transaction, and report its transaction ID as `txHash`.

### TRON

`tronTransaction.rawDataHex` is the hex-encoded raw transaction. Sign it with TronWeb or TronLink, send it, and report the transaction ID as `txHash`.

## Track progress

A flow reports progress on three independent axes, and a payment is done only when `settlementState` is `completed`:

| Axis | States |
| - | - |
| `executionState` | `initiated`, `source_attached`, `quoted`, `signing`, `broadcasted`, `source_detected`, `source_confirmed`, `cancelled`, `expired`, `failed` |
| `settlementState` | `none`, `settling`, `completed`, `failed` |
| `riskState` | `unknown`, `cleared`, `blocked` |

Settlement moves only after execution reaches `source_confirmed`. A payment that needs a swap goes from `none` to `settling` to `completed`, and a direct transfer, deposit-address payment, or exchange payment goes from `none` to `completed`. [Payment lifecycle and states](https://docs.dynamic.xyz/docs/flow/lifecycle-walkthrough.md) describes every state.

### Webhooks

Webhooks are the recommended way for your server to follow a flow. Subscribe to `flow.execution.updated`, `flow.settlement.updated`, and `flow.risk.updated` as described in [Setting up webhooks](https://docs.dynamic.xyz/docs/platform/console/webhooks/setup.md), and verify each event's signature.

| Event | `data.newState` | What to do |
| - | - | - |
| `flow.execution.updated` | `source_confirmed` | Record the source transaction: `txHash`, `chainId`, `chainName`, and `blockExplorerUrls` when present, or `exchangeTransactionId` for an exchange payment |
| `flow.settlement.updated` | `completed` | The payment is done. Record `settlementTxHash` and `settledAmount`, then fulfill as described in [Confirm what was delivered](#confirm-what-was-delivered) |
| `flow.settlement.updated` | `failed` | Read `failure` and handle it as described in [Failures and refunds](https://docs.dynamic.xyz/docs/flow/failures-and-refunds.md) |
| `flow.execution.updated` | `failed` | Read `failure` and handle it as described in [Failures and refunds](https://docs.dynamic.xyz/docs/flow/failures-and-refunds.md) |

Events can arrive more than once and out of order, and they do not include `memo`. [Webhooks](https://docs.dynamic.xyz/docs/flow/webhooks.md) describes every event's payload and how to reconcile a completed payment.

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

// Your application's functions.
declare function verifySignature(req: express.Request): boolean;
declare function recordSettlement(flowId: string, data: unknown): Promise<void>;
declare function recordFailure(flowId: string, data: unknown): Promise<void>;

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

app.post('/webhooks/flow', async (req, res) => {
  if (!verifySignature(req)) {
    res.sendStatus(401);
    return;
  }

  const { eventName, data } = req.body;

  if (eventName === 'flow.settlement.updated' && data.newState === 'completed') {
    await recordSettlement(data.flowId, data.additionalData);
  } else if (data.newState === 'failed') {
    await recordFailure(data.flowId, data.additionalData);
  }

  res.sendStatus(200);
});
```

`recordSettlement` should be idempotent, because the same event can arrive twice.

### Polling

The payer's page can read the flow without a credential:

```bash theme={"system"}
curl --url "https://app.dynamic.xyz/api/v0/sdk/$DYNAMIC_ENVIRONMENT_ID/flow/$FLOW_ID"
```

Poll every 3 seconds while the payment is in progress, and stop when one of these is true:

| Condition | Meaning |
| - | - |
| `settlementState` is `completed` | The payment is done |
| `settlementState` or `executionState` is `failed` | The payment failed. Read `failure` |
| `executionState` is `cancelled` | The flow was cancelled |
| `executionState` is `expired` | The flow expired before the payment was sent |

A flow can expire while it waits in `initiated`, `source_attached`, `quoted`, or `signing`. Read the flow, or compare the current time with `expiresAt`, before you send the next request.

For fulfillment, read the flow from your server instead, as described in [Read a flow from your server](#read-a-flow-from-your-server).

### Resume and retry by state

Before you retry a request or resume an interrupted page, read the flow and choose the next request from its `executionState`:

| `executionState` | Valid next requests |
| - | - |
| `initiated` | Attach a source. Your server can cancel |
| `source_attached` | Attach a different source, get a quote, or cancel with the session token. For an exchange source, wait for the payment |
| `quoted` | Get a new quote, attach a different source, prepare signing for a wallet, or cancel |
| `signing` | Report the transaction, get a new quote, attach a different source with the session token, or cancel |
| `broadcasted`, `source_detected` | Wait. The payment is on its way, and the flow cannot be cancelled or change source |
| `source_confirmed` | Wait until `settlementState` is `completed` or `failed` |
| `cancelled`, `expired`, or `failed` | Create a new flow for another attempt |

## Confirm what was delivered

The mode decides what a completed payment means:

* **`payment` mode.** `completed` means the payment was accepted in full. A payment that arrives short is not settled: a short deposit-address payment is returned to the refund address by the provider, and a short exchange payment fails.
* **`deposit` mode.** A deposit can settle for less than the flow's `amount`. Credit the delivered amount, which is `settlement.amount` on the flow or `settledAmount` in the `flow.settlement.updated` webhook, not the requested amount.

When settlement reaches `completed`, the flow's `settlement` object records the delivery:

| Field | Meaning |
| - | - |
| `amount` | The delivered amount, in the settlement token's smallest unit |
| `amountUsd` | The USD value of the delivered amount |
| `toTokenInfo` | The settlement token's `symbol` and `decimals` |
| `toChainId` | The chain the funds were delivered on |
| `toToken` | The settlement token's address |
| `toAddress` | The destination. Only your server sees the address; the payer's view shows a placeholder |
| `completedAt` | When settlement completed |

The `settlement` object exists only once settlement reaches `completed`. `amount` is present only when the provider or the chain reports it, and `amountUsd` and `toTokenInfo` can be absent even then. When the delivered amount is absent, verify it on chain from `settlementTxHash` before you credit it.

A receipt can cite the transaction that funded the payment and the one that delivered it:

| Source | Funding | Delivery |
| - | - | - |
| Wallet | `txHash`, sent by the payer's wallet | `settlementTxHash` |
| Deposit address | `txHash`, as reported by the deposit provider, when it reports one | `settlementTxHash` |
| Exchange | `exchangeTransactionId`, which arrives only in webhooks | `settlementTxHash` |

`settlementTxHash` is in the server read response and in webhooks, not in the payer's view. Decide whether to fulfill on your server, from a verified webhook or a server read, as described in [Access and data](https://docs.dynamic.xyz/docs/flow/security.md#decide-fulfillment-on-your-server).

## Read and cancel from your server

### Read a flow from your server

```bash theme={"system"}
curl --url "https://app.dynamic.xyz/api/v0/server/$DYNAMIC_ENVIRONMENT_ID/flow/$FLOW_ID" \
  --header "Authorization: Bearer $DYNAMIC_API_TOKEN"
```

The response is the full flow, including what the payer's view leaves out: the real `toAddress`, `settlementTxHash`, `settlementConfig`, and `destinationConfig`. Use it to reconcile and to decide fulfillment.

> [!NOTE]
>
> The generated API reference lists this endpoint under the create path, as `GET /server/{environmentId}/flow/{mode}`. The path segment is the flow ID.

### Cancel a flow

Who can cancel depends on the flow's state:

| Request | Credential | Allowed from |
| - | - | - |
| `POST /server/{environmentId}/flow/{flowId}/cancel` | API token | `initiated` only |
| `POST /sdk/{environmentId}/flow/{flowId}/cancel` | Session token | `source_attached`, `quoted`, `signing` |

```bash theme={"system"}
curl --request POST \
  --url "https://app.dynamic.xyz/api/v0/server/$DYNAMIC_ENVIRONMENT_ID/flow/$FLOW_ID/cancel" \
  --header "Authorization: Bearer $DYNAMIC_API_TOKEN"
```

Both return the flow in `cancelled`, which is final, and the session token stops working. A cancel from any other state returns `409` with `flow_invalid_transition`, and the flow keeps its state. No one can cancel a flow after the payment is sent or a deposit is seen. To ask the payer again, create a new flow.

## Payment links

A payment link is a `payment` flow created together with a URL to send to the payer. Use it to share a payment by email, chat, or QR code instead of passing the flow ID to your own page.

```bash theme={"system"}
curl --request POST \
  --url "https://app.dynamic.xyz/api/v0/server/$DYNAMIC_ENVIRONMENT_ID/payment-links" \
  --header "Authorization: Bearer $DYNAMIC_API_TOKEN" \
  --header 'Content-Type: application/json' \
  --data '{
    "amount": "25.00",
    "currency": "USD",
    "settlementConfig": {
      "strategy": "cheapest",
      "settlements": [
        {
          "chainName": "EVM",
          "chainId": "8453",
          "tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
          "symbol": "USDC",
          "tokenDecimals": 6
        }
      ]
    },
    "destinationConfig": {
      "destinations": [
        { "chainName": "EVM", "type": "address", "identifier": "0xYourEvmDestinationAddress" }
      ]
    },
    "memo": { "invoiceId": "inv_123" },
    "baseUrl": "https://pay.dynamicauth.com"
  }'
```

The body takes the same fields as [Create a flow](#create-a-flow), except the mode, which is always `payment`, plus a required `baseUrl`. The origin of `baseUrl` must be a registered [CORS origin](https://docs.dynamic.xyz/docs/platform/console/security.md#allowed-cors-origin) for the environment, or the request returns `422`. For a long-lived invoice, set `expiresIn` up to `31536000` seconds (365 days).

The response is `201`:

```json theme={"system"}
{
  "flow": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "mode": "payment",
    "executionState": "initiated",
    "settlementState": "none"
  },
  "paymentUrl": "https://pay.dynamicauth.com?flow=eyJlbnZpcm9ubWVudElkIjoiLi4uIiwiZmxvd0lkIjoiLi4uIn0"
}
```

Send `paymentUrl` to the payer, and store `flow.id` to track the payment. `paymentUrl` is `baseUrl` with a `flow` query parameter added. The parameter is `{ "environmentId", "flowId" }` as JSON, base64url-encoded. It contains no secret, but anyone who has the link can open the payment, so send it only to the payer it is for.

Set `baseUrl` to `https://pay.dynamicauth.com` to use Dynamic's hosted payment page, which works only with links created with this endpoint. To host the page yourself, set `baseUrl` to your own page, read the parameter, and continue from [Attach the wallet](#attach-the-wallet) or the attach step for another source:

```typescript title="read-payment-link.ts" theme={"system"}
const segment = new URLSearchParams(window.location.search).get('flow');
if (!segment) {
  throw new Error('Missing flow parameter');
}

const base64 = segment.replace(/-/g, '+').replace(/_/g, '/');
const padded = base64.padEnd(base64.length + ((4 - (base64.length % 4)) % 4), '=');
const { environmentId, flowId } = JSON.parse(atob(padded)) as {
  environmentId: string;
  flowId: string;
};
```

[Payment links](https://docs.dynamic.xyz/docs/flow/payment-links.md) describes the hosted, self-hosted, and custom domain options.

## Supported chains and native tokens

| Chain | `chainName` | `chainId` |
| - | - | - |
| EVM | `EVM` | The chain's ID, for example `"1"` (Ethereum), `"8453"` (Base), or `"42161"` (Arbitrum) |
| Solana | `SOL` | `"101"` |
| Bitcoin | `BTC` | `"1"` |
| Sui | `SUI` | `"501"` |
| TRON | `TRON` | `"728126428"` |

For a native token, use one of these addresses wherever a token address is expected:

| Chain | Native token addresses |
| - | - |
| EVM | `0x0000000000000000000000000000000000000000` or `0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee` |
| Solana | `11111111111111111111111111111111` or `So11111111111111111111111111111111111111112` (wrapped SOL) |
| Bitcoin | `11111111111111111111111111111111` or `bitcoin` |
| Sui | `0x2::sui::SUI` or `0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI` |
| TRON | `T9yD14Nj9j7xAB4dbGeiX9h8unkKHxuWwb` |

For any other token, use its contract address or mint on that chain. [Supported chains](https://docs.dynamic.xyz/docs/flow/supported-chains.md) lists which chains each payment source supports, and the Base Sepolia testnet values.

## Errors

A rejected request returns JSON with `error`, a human-readable message, and usually `code`, a stable identifier to branch on. A rejected request leaves the flow unchanged, so read the flow and continue from its current state. [API errors](https://docs.dynamic.xyz/docs/flow/api-errors.md) lists the errors per endpoint, which to retry, and the `quoteFailures` reasons.

A flow that fails after a request succeeded, for example because the transaction reverted, reports it in its `failure` field instead. [Failures and refunds](https://docs.dynamic.xyz/docs/flow/failures-and-refunds.md) lists those codes.

## Complete example

This script creates a flow, pays it from an EVM wallet on Base, and waits for settlement. It suits a backend or an agent that controls both the server and the paying wallet. In a browser app, keep the create request on your server and run the rest in the payer's page.

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

const API = 'https://app.dynamic.xyz/api/v0';
const ENV_ID = process.env.DYNAMIC_ENVIRONMENT_ID!;
const API_TOKEN = process.env.DYNAMIC_API_TOKEN!; // needs flow.write
const USDC_BASE = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913';

// Your connected wallet client.
declare const walletClient: WalletClient & {
  waitForTransactionReceipt: (args: { hash: `0x${string}` }) => Promise<unknown>;
};
const account = walletClient.account!;

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

// Server: create the flow.
const { flow: created } = await api(`/server/${ENV_ID}/flow/payment`, {
  method: 'POST',
  headers: { Authorization: `Bearer ${API_TOKEN}` },
  body: JSON.stringify({
    amount: '25.00',
    currency: 'USD',
    settlementConfig: {
      strategy: 'cheapest',
      settlements: [
        { chainName: 'EVM', chainId: '8453', tokenAddress: USDC_BASE, symbol: 'USDC', tokenDecimals: 6 },
      ],
    },
    destinationConfig: {
      destinations: [
        { chainName: 'EVM', type: 'address', identifier: process.env.DESTINATION_ADDRESS! },
      ],
    },
    memo: { orderId: 'order_abc123' },
  }),
});
const flowId: string = created.id;

// Payer: attach the wallet and keep the 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 };

// Payer: quote, then prepare within 60 seconds.
await api(`/sdk/${ENV_ID}/flow/${flowId}/quote`, {
  method: 'POST',
  headers: session,
  body: JSON.stringify({ fromTokenAddress: USDC_BASE }),
});
const prepared = await api(`/sdk/${ENV_ID}/flow/${flowId}/prepare`, {
  method: 'POST',
  headers: session,
  body: JSON.stringify({}),
});

// Payer: sign and send.
const { signingPayload } = prepared.quote;
if (signingPayload.evmApproval) {
  const approvalHash = await walletClient.writeContract({
    account,
    address: signingPayload.evmApproval.tokenAddress,
    abi: parseAbi(['function approve(address, uint256) returns (bool)']),
    functionName: 'approve',
    args: [signingPayload.evmApproval.spenderAddress, BigInt(signingPayload.evmApproval.amount)],
    chain: walletClient.chain,
  });
  await walletClient.waitForTransactionReceipt({ hash: approvalHash });
}
const txHash = await walletClient.sendTransaction({
  account,
  to: signingPayload.evmTransaction.to,
  data: signingPayload.evmTransaction.data,
  value: BigInt(signingPayload.evmTransaction.value),
  chain: walletClient.chain,
});

// Payer: report the transaction.
await api(`/sdk/${ENV_ID}/flow/${flowId}/broadcast`, {
  method: 'POST',
  headers: session,
  body: JSON.stringify({ txHash }),
});

// Server: wait for settlement.
while (true) {
  const flow = await api(`/server/${ENV_ID}/flow/${flowId}`, {
    headers: { Authorization: `Bearer ${API_TOKEN}` },
  });
  if (flow.settlementState === 'completed') {
    console.log('Paid. Settlement transaction:', flow.settlementTxHash);
    break;
  }
  if (
    flow.settlementState === 'failed' ||
    ['cancelled', 'expired', 'failed'].includes(flow.executionState)
  ) {
    throw new Error(`Payment did not complete: ${flow.failure?.code ?? flow.executionState}`);
  }
  await new Promise((resolve) => setTimeout(resolve, 3000));
}
```

## Related

* [API errors](https://docs.dynamic.xyz/docs/flow/api-errors.md) lists every error with retry guidance.
* [Fees API](https://docs.dynamic.xyz/docs/flow/fees.md) adds your own fee and claims accrued balances.
* [Accept a same-token payment](https://docs.dynamic.xyz/docs/flow/same-token-payment.md) is a complete deposit-address payment with `disableSwaps`.
* [Token swaps via API](https://docs.dynamic.xyz/docs/recipes/integrations/swaps/swap-api.md) covers standalone swaps without a flow.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.