Skip to main content
This is an enterprise-only feature. Please contact us to enable.
This guide runs a Flow payment 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. To run the payer’s side from a web or React Native app instead, use the JavaScript SDK, 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. 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:

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. Amounts and currencies gives the rules for each mode, and 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:
Each request uses one of three credentials: 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.
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.

Endpoints

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 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.
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:
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. 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 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 for the environment. uiConfig applies only to UIs that read it: Dynamic’s hosted page for 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 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 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

The response is 200 with the flow in source_attached, the session token, and its expiry:
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

The response is the flow in quoted, with a new quote:
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. 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.

Prepare signing

The response is the flow in signing, with the transaction to sign in quote.signingPayload:
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. 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:
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.
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 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

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.

Get the deposit address

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

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

Attach the exchange

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 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, signing the prepared transaction with a wallet you control, such as a server wallet.

Amounts and currencies

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: Any Dynamic SDK that can sign on the chain works, including the Node SDK 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.
sign-evm.ts

EVM with the Node SDK

sign-evm-node.ts

Solana

serializedTransaction is a base64 versioned transaction, 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.
sign-solana.ts

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.
sign-solana-node.ts

Sui

serializedTransaction is a base64 transaction. Deserialize and sign it with the payer’s Sui wallet through @mysten/sui, send it, and report the resulting digest as txHash.

Bitcoin

psbt is a base64 unsigned PSBT. 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: 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 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, and verify each event’s signature. Events can arrive more than once and out of order, and they do not include memo. Webhooks describes every event’s payload and how to reconcile a completed payment.
webhook-handler.ts
recordSettlement should be idempotent, because the same event can arrive twice.

Polling

The payer’s page can read the flow without a credential:
Poll every 3 seconds while the payment is in progress, and stop when one of these is true: 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.

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:

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

Read and cancel from your server

Read a flow from your server

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.
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:
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. 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.
The body takes the same fields as Create a flow, except the mode, which is always payment, plus a required baseUrl. The origin of baseUrl must be a registered 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:
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 or the attach step for another source:
read-payment-link.ts
Payment links describes the hosted, self-hosted, and custom domain options.

Supported chains and native tokens

For a native token, use one of these addresses wherever a token address is expected: For any other token, use its contract address or mint on that chain. Supported chains 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 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 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.
flow-example.ts
Last modified on October 29, 2026