Skip to main content

Documentation Index

Fetch the complete documentation index at: https://www.dynamic.xyz/docs/llms.txt

Use this file to discover all available pages before exploring further.

What We’re Building

A server-side payment flow using Dynamic’s Checkout API. The Checkout API lets you accept a crypto payment from any supported chain and have it settled in a specific token on a specific chain. By the end of this guide you will be able to:
  • Create a checkout configuration and reuse its checkoutId when settlement and destinations stay the same
  • Walk through the full payment flow: create transaction, attach source, get quote, sign, broadcast
  • Poll or receive webhooks for settlement completion
  • Handle errors and edge cases
This recipe uses raw HTTP calls, making it suitable for backend services, AI agents, cron jobs, or any language with fetch.

Prerequisites

  • A Dynamic environment with the checkout feature enabled
  • An environment API token (dyn_...) 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
  • Node.js 18+ (or any runtime with fetch)

Base URL

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

Overview

The checkout flow is a state machine with eight sequential steps. The same eight steps are used in both payment and deposit modes — only the checkout config (Step 1) and how the amount is interpreted (Step 2) change between them. This guide walks through both flows end-to-end:
1. Create checkout      (defines where funds go — reuse one or create more as your model needs)
2. Create transaction   (initiates a payment or deposit, returns a session token)
3. Attach source        (declare which wallet/chain the sender is paying from)
4. Get quote            (get swap route, fees, and estimated time)
5. Prepare signing      (lock in the quote, get the signing payload)
6. Sign and broadcast   (sender's wallet signs and submits to the network)
7. Notify backend       (report the tx hash so Dynamic can watch the chain)
8. 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:

Authentication

ContextHeader
Checkout management (create/update/delete)Authorization: Bearer dyn_...
Transaction creationNone required (JWT optional)
Transaction mutations (source, quote, prepare, broadcast, cancel)x-dynamic-checkout-session-token: dct_...
Transaction reads (polling)None required
The session token is returned once when you create a transaction. Store it for the duration of the flow.

Choose Your Flow

This guide covers two checkout modes side by side. The eight steps are the same in both flows — what changes is the checkout config you create in Step 1 and how the amount is interpreted in Step 2. Pick the flow that matches your use case:
  • Payment flowmode: "payment". The receiver fixes the amount on each transaction (e.g., a $25 invoice). Use for invoices, e-commerce checkouts, or paid services.
  • Deposit flowmode: "deposit". The sender chooses how much to send (e.g., a $100 top-up). Use for funding flows, on-ramps, or open-ended deposits.
Each flow below is self-contained and walks through all eight steps end-to-end.

Shared concepts

These apply to both flows:
ConceptDescription
settlementConfig.strategyHow the best quote is selected: "cheapest", "fastest", or "preferred_order"
settlementConfig.settlementsToken/chain pairs you want to receive. Each needs a matching destination
destinationConfig.destinationsWallet addresses where funds land. chainName must match a settlement entry
enableOrchestrationOptional. Default true. When false, skips cross-chain settlement orchestration
StrategyBehavior
"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
Manage existing checkouts with GET, PATCH, and DELETE on /environments/{environmentId}/checkouts/{checkoutId}.

Payment Flow

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

Step 1 (Payment): Create a Payment Checkout

A checkout is a reusable payment configuration: it defines what token(s) you want to receive and the destination wallet(s). Reuse the same checkoutId for every transaction when that configuration does not change.
POST /environments/{environmentId}/checkouts
Authorization: Bearer dyn_your_api_token
Content-Type: application/json
{
  "mode": "payment",
  "settlementConfig": {
    "strategy": "cheapest",
    "settlements": [
      {
        "chainName": "EVM",
        "chainId": "8453",
        "tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
        "symbol": "USDC",
        "tokenDecimals": 6
      },
      {
        "chainName": "SOL",
        "chainId": "101",
        "tokenAddress": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
        "symbol": "USDC",
        "tokenDecimals": 6
      }
    ]
  },
  "destinationConfig": {
    "destinations": [
      {
        "chainName": "EVM",
        "type": "address",
        "identifier": "0xYourEVMDestinationWallet"
      },
      {
        "chainName": "SOL",
        "type": "address",
        "identifier": "YourSOLDestinationWallet"
      }
    ]
  }
}
Response (201): Returns the checkout object with an id. Save the id — you’ll use it as checkoutId in Step 2.

Step 2 (Payment): Create a Transaction

Start a new payment with the amount the receiver wants to collect. This returns a session token that authenticates all subsequent calls.
POST /sdk/{environmentId}/checkouts/{checkoutId}/transactions
Content-Type: application/json
{
  "amount": "25.00",
  "currency": "USD",
  "memo": {
    "orderId": "order_abc123"
  }
}
FieldDescription
amountThe amount the receiver collects, as a string (e.g., "25.00")
currencyFiat currency code the amount is denominated in (e.g., "USD")
memoOptional. Arbitrary JSON metadata for your own correlation
expiresInOptional. TTL in seconds. Default: 3600 (1 hour)
destinationAddressesOptional. Override the checkout’s destination for this transaction
Response (201):
{
  "sessionToken": "dct_a1b2c3d4e5f6...",
  "sessionExpiresAt": "2025-03-23T11:00:00Z",
  "transaction": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "checkoutId": "660f9500-f39c-51e5-b827-557766551111",
    "amount": "25.00",
    "currency": "USD",
    "executionState": "initiated",
    "settlementState": "none",
    "riskState": "unknown",
    "quoteVersion": 0
  }
}
Store sessionToken and transaction.id immediately. The session token is returned once and cannot be retrieved again.

Step 3 (Payment): Attach Source

Declare which wallet and chain the payer is paying from. After this, risk screening runs asynchronously.
POST /sdk/{environmentId}/transactions/{transactionId}/source
x-dynamic-checkout-session-token: dct_...
Content-Type: application/json
{
  "sourceType": "wallet",
  "fromAddress": "0xYourWalletAddress",
  "fromChainId": "8453",
  "fromChainName": "EVM"
}
FieldDescription
sourceType"wallet" or "exchange"
fromAddressSource wallet address. Required for wallet type
fromChainIdChain ID (e.g., "1" = Ethereum, "8453" = Base). Required for wallet type
fromChainNameChain family: "EVM", "SOL", "BTC", "SUI". Required for wallet type
Response (200): Transaction with executionState: "source_attached". Error (403): Blocked by sanctions screening — cancel and retry with a different source.

Step 4 (Payment): Get a Quote

Specify which token the sender is paying with. The API finds the best route to your checkout’s settlement token(s) for the receiver’s requested amount.
POST /sdk/{environmentId}/transactions/{transactionId}/quote
x-dynamic-checkout-session-token: dct_...
Content-Type: application/json
{
  "fromTokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
}
FieldDescription
fromTokenAddressToken contract address. Use 0x0000000000000000000000000000000000000000 for EVM native tokens
slippageOptional. Slippage tolerance as a decimal (e.g., 0.005 for 0.5%)
Response (200):
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "executionState": "quoted",
  "quoteVersion": 1,
  "quote": {
    "version": 1,
    "fromAmount": "25.50",
    "toAmount": "25.00",
    "estimatedTimeSec": 120,
    "fees": {
      "totalFeeUsd": "0.50",
      "gasEstimate": {
        "usdValue": "0.30",
        "nativeValue": "0.00012",
        "nativeSymbol": "ETH"
      }
    },
    "createdAt": "2025-03-23T10:01:00Z",
    "expiresAt": "2025-03-23T10:02:00Z"
  }
}
In payment mode, toAmount matches the receiver’s requested amount and fromAmount is what the sender must pay (including swap costs and fees).
Quotes expire in 60 seconds. If it expires before you call /prepare, request a new quote. The quoteVersion increments with each new quote.
For same-chain, same-token payments (no swap needed), the API builds a direct transfer payload — no routing required.

Step 5 (Payment): Prepare Signing

Locks in the quote and returns the signing payload. You can optionally request on-chain balance assertions.
POST /sdk/{environmentId}/transactions/{transactionId}/prepare
x-dynamic-checkout-session-token: dct_...
Content-Type: application/json
{
  "assertBalanceForGasCost": true,
  "assertBalanceForTransferAmount": true
}
FieldDefaultDescription
assertBalanceForGasCostfalseVerify the wallet has enough for gas. Returns 422 if insufficient
assertBalanceForTransferAmountfalseVerify the wallet has enough for the transfer. Returns 422 if insufficient
Response (200): Transaction with executionState: "signing" and quote.signingPayload:
{
  "executionState": "signing",
  "quote": {
    "signingPayload": {
      "chainName": "EVM",
      "chainId": "8453",
      "evmTransaction": {
        "to": "0xContractAddress",
        "data": "0xCalldata...",
        "value": "0x0",
        "gasLimit": "0x5208"
      },
      "evmApproval": {
        "tokenAddress": "0xTokenAddress",
        "spenderAddress": "0xSpenderAddress",
        "amount": "25500000"
      }
    }
  }
}
The payload structure depends on the chain:
ChainFieldsNotes
EVMevmTransaction (to, data, value, gasLimit)Standard EVM transaction
EVM (ERC-20)evmApproval (tokenAddress, spenderAddress, amount)Send approval tx first if present
SOL, SUIserializedTransactionBase64-encoded serialized transaction
BTCpsbtBase64-encoded unsigned PSBT
Possible errors:
  • 422 — Quote expired: go back to Step 4
  • 422 — Risk not cleared: poll GET /transactions/{id} until riskState is "cleared", then retry
  • 422 — Insufficient balance: response includes required and available amounts

Step 6 (Payment): Sign and Broadcast On-Chain

Use quote.signingPayload to sign and submit the transaction with whatever wallet your sender has connected. If evmApproval is present, send the approval transaction first, then the main one.
Don’t have wallet connection in your app? Use the Dynamic JavaScript SDK to handle wallet connection, signing, and broadcasting end-to-end — including the full checkout flow via checkout-flow. The JS SDK works in any browser or Node environment and removes the need to manage signing yourself.
The example below uses viem with an already-connected EVM wallet client (walletClient) — for example, the one returned by useWalletClient() in wagmi or by Dynamic’s primary wallet connector.
sign.ts
import { parseAbi } from 'viem';

// `walletClient` is your connected wallet (viem WalletClient, wagmi, Dynamic primary wallet, etc.)
// `prepared` is the response from Step 5.
const { signingPayload } = prepared.quote;

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

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

Step 7 (Payment): Notify Backend of Broadcast

This endpoint does not broadcast the transaction on-chain. That already happened in Step 6 — 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}/transactions/{transactionId}/broadcast
x-dynamic-checkout-session-token: dct_...
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.

Step 8 (Payment): Wait for Settlement

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

Option A: Polling

GET /sdk/{environmentId}/transactions/{transactionId}
No authentication required. Poll every 3 seconds. Stop when you see a terminal state:
ConditionMeaning
settlementState === "completed"Funds delivered to the receiver’s destination
settlementState === "failed"Settlement failed
executionState === "failed"Execution failed
executionState === "cancelled"Transaction cancelled
executionState === "expired"Session timed out
Settlement progresses through: noneroutingbridgingswappingsettlingcompleted. Same-chain, same-token payments jump directly to completed. Set up a webhook to receive events as the transaction progresses:
POST /environments/{environmentId}/webhooks
Authorization: Bearer dyn_your_api_token
Content-Type: application/json
{
  "url": "https://your-api.example.com/webhooks/checkout",
  "events": [
    "execution.state.broadcasted",
    "execution.state.source_confirmed",
    "settlement.state.completed",
    "execution.state.failed",
    "settlement.state.failed"
  ],
  "isEnabled": true
}
Key events to handle:
EventAction
settlement.state.completedPayment is done — fulfill the order
settlement.state.failedSettlement failed — inspect data.failure
execution.state.failedExecution failed — inspect data.failure
webhook-handler.ts
import express from 'express';

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

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

  switch (eventName) {
    case 'settlement.state.completed':
      fulfillOrder(data.transactionId);
      break;
    case 'execution.state.failed':
    case 'settlement.state.failed':
      handleFailure(data.transactionId, data.failure);
      break;
  }

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

Deposit Flow

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

Step 1 (Deposit): Create a Deposit Checkout

A checkout is a reusable deposit configuration: it defines what token(s) you’ll receive and the destination wallet(s). Reuse the same checkoutId for every deposit when that configuration does not change.
POST /environments/{environmentId}/checkouts
Authorization: Bearer dyn_your_api_token
Content-Type: application/json
{
  "mode": "deposit",
  "settlementConfig": {
    "strategy": "cheapest",
    "settlements": [
      {
        "chainName": "EVM",
        "chainId": "8453",
        "tokenAddress": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
        "symbol": "USDC",
        "tokenDecimals": 6
      },
      {
        "chainName": "SOL",
        "chainId": "101",
        "tokenAddress": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
        "symbol": "USDC",
        "tokenDecimals": 6
      }
    ]
  },
  "destinationConfig": {
    "destinations": [
      {
        "chainName": "EVM",
        "type": "address",
        "identifier": "0xYourEVMDestinationWallet"
      },
      {
        "chainName": "SOL",
        "type": "address",
        "identifier": "YourSOLDestinationWallet"
      }
    ]
  }
}
The only structural difference from a payment checkout is "mode": "deposit". Settlement and destination configs work the same way. Response (201): Returns the checkout object with an id. Save the id — you’ll use it as checkoutId in Step 2.

Step 2 (Deposit): Create a Transaction

Start a new deposit. In deposit mode the sender chooses how much they want to send — for example, an end user topping up a $100 balance. This returns a session token that authenticates all subsequent calls.
POST /sdk/{environmentId}/checkouts/{checkoutId}/transactions
Content-Type: application/json
{
  "amount": "100.00",
  "currency": "USD",
  "memo": {
    "userId": "user_abc123",
    "purpose": "account_top_up"
  }
}
FieldDescription
amountThe amount the sender wants to deposit, as a string (e.g., "100.00")
currencyCurrency code the amount is denominated in (e.g., "USD")
memoOptional. Arbitrary JSON metadata for your own correlation (e.g., user ID)
expiresInOptional. TTL in seconds. Default: 3600 (1 hour)
destinationAddressesOptional. Override the checkout’s destination for this transaction
Response (201):
{
  "sessionToken": "dct_a1b2c3d4e5f6...",
  "sessionExpiresAt": "2025-03-23T11:00:00Z",
  "transaction": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "checkoutId": "660f9500-f39c-51e5-b827-557766551111",
    "amount": "100.00",
    "currency": "USD",
    "executionState": "initiated",
    "settlementState": "none",
    "riskState": "unknown",
    "quoteVersion": 0
  }
}
Store sessionToken and transaction.id immediately. The session token is returned once and cannot be retrieved again.

Step 3 (Deposit): Attach Source

Declare which wallet and chain the depositor is funding from. After this, risk screening runs asynchronously.
POST /sdk/{environmentId}/transactions/{transactionId}/source
x-dynamic-checkout-session-token: dct_...
Content-Type: application/json
{
  "sourceType": "wallet",
  "fromAddress": "0xYourWalletAddress",
  "fromChainId": "8453",
  "fromChainName": "EVM"
}
FieldDescription
sourceType"wallet" or "exchange"
fromAddressDepositor’s wallet address. Required for wallet type
fromChainIdChain ID (e.g., "1" = Ethereum, "8453" = Base). Required for wallet type
fromChainNameChain family: "EVM", "SOL", "BTC", "SUI". Required for wallet type
Response (200): Transaction with executionState: "source_attached". Error (403): Blocked by sanctions screening — cancel and retry with a different source.

Step 4 (Deposit): Get a Quote

Specify which token the depositor is funding with. The API finds the best route from that token to your checkout’s settlement token(s) for the deposit amount.
POST /sdk/{environmentId}/transactions/{transactionId}/quote
x-dynamic-checkout-session-token: dct_...
Content-Type: application/json
{
  "fromTokenAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
}
FieldDescription
fromTokenAddressToken contract address the depositor is sending. Use 0x0000000000000000000000000000000000000000 for EVM native tokens
slippageOptional. Slippage tolerance as a decimal (e.g., 0.005 for 0.5%)
Response (200):
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "executionState": "quoted",
  "quoteVersion": 1,
  "quote": {
    "version": 1,
    "fromAmount": "100.50",
    "toAmount": "100.00",
    "estimatedTimeSec": 120,
    "fees": {
      "totalFeeUsd": "0.50",
      "gasEstimate": {
        "usdValue": "0.30",
        "nativeValue": "0.00012",
        "nativeSymbol": "ETH"
      }
    },
    "createdAt": "2025-03-23T10:01:00Z",
    "expiresAt": "2025-03-23T10:02:00Z"
  }
}
fromAmount is what the depositor’s wallet will be charged in the source token; toAmount is what lands at the destination after swap and fees.
Quotes expire in 60 seconds. If it expires before you call /prepare, request a new quote. The quoteVersion increments with each new quote.
For same-chain, same-token deposits (no swap needed), the API builds a direct transfer payload — no routing required.

Step 5 (Deposit): Prepare Signing

Locks in the quote and returns the signing payload. You can optionally request on-chain balance assertions.
POST /sdk/{environmentId}/transactions/{transactionId}/prepare
x-dynamic-checkout-session-token: dct_...
Content-Type: application/json
{
  "assertBalanceForGasCost": true,
  "assertBalanceForTransferAmount": true
}
FieldDefaultDescription
assertBalanceForGasCostfalseVerify the wallet has enough for gas. Returns 422 if insufficient
assertBalanceForTransferAmountfalseVerify the wallet has enough for the transfer. Returns 422 if insufficient
Response (200): Transaction with executionState: "signing" and quote.signingPayload:
{
  "executionState": "signing",
  "quote": {
    "signingPayload": {
      "chainName": "EVM",
      "chainId": "8453",
      "evmTransaction": {
        "to": "0xContractAddress",
        "data": "0xCalldata...",
        "value": "0x0",
        "gasLimit": "0x5208"
      },
      "evmApproval": {
        "tokenAddress": "0xTokenAddress",
        "spenderAddress": "0xSpenderAddress",
        "amount": "100500000"
      }
    }
  }
}
The payload structure depends on the chain:
ChainFieldsNotes
EVMevmTransaction (to, data, value, gasLimit)Standard EVM transaction
EVM (ERC-20)evmApproval (tokenAddress, spenderAddress, amount)Send approval tx first if present
SOL, SUIserializedTransactionBase64-encoded serialized transaction
BTCpsbtBase64-encoded unsigned PSBT
Possible errors:
  • 422 — Quote expired: go back to Step 4
  • 422 — Risk not cleared: poll GET /transactions/{id} until riskState is "cleared", then retry
  • 422 — Insufficient balance: response includes required and available amounts

Step 6 (Deposit): Sign and Broadcast On-Chain

Use quote.signingPayload to sign and submit the transaction with whatever wallet the depositor has connected. If evmApproval is present, send the approval transaction first, then the main one.
Don’t have wallet connection in your app? Use the Dynamic JavaScript SDK to handle wallet connection, signing, and broadcasting end-to-end — including the full checkout flow via checkout-flow. The JS SDK works in any browser or Node environment and removes the need to manage signing yourself.
The example below uses viem with an already-connected EVM wallet client (walletClient) — for example, the one returned by useWalletClient() in wagmi or by Dynamic’s primary wallet connector.
sign.ts
import { parseAbi } from 'viem';

// `walletClient` is your connected wallet (viem WalletClient, wagmi, Dynamic primary wallet, etc.)
// `prepared` is the response from Step 5.
const { signingPayload } = prepared.quote;

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

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

Step 7 (Deposit): Notify Backend of Broadcast

This endpoint does not broadcast the transaction on-chain. That already happened in Step 6 — 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}/transactions/{transactionId}/broadcast
x-dynamic-checkout-session-token: dct_...
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.

Step 8 (Deposit): Wait for Settlement

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

Option A: Polling

GET /sdk/{environmentId}/transactions/{transactionId}
No authentication required. Poll every 3 seconds. Stop when you see a terminal state:
ConditionMeaning
settlementState === "completed"Deposit delivered to the destination
settlementState === "failed"Settlement failed
executionState === "failed"Execution failed
executionState === "cancelled"Transaction cancelled
executionState === "expired"Session timed out
Settlement progresses through: noneroutingbridgingswappingsettlingcompleted. Same-chain, same-token deposits jump directly to completed. Set up a webhook to receive events as the transaction progresses:
POST /environments/{environmentId}/webhooks
Authorization: Bearer dyn_your_api_token
Content-Type: application/json
{
  "url": "https://your-api.example.com/webhooks/checkout",
  "events": [
    "execution.state.broadcasted",
    "execution.state.source_confirmed",
    "settlement.state.completed",
    "execution.state.failed",
    "settlement.state.failed"
  ],
  "isEnabled": true
}
Key events to handle:
EventAction
settlement.state.completedDeposit is done — credit the user’s account
settlement.state.failedSettlement failed — inspect data.failure
execution.state.failedExecution failed — inspect data.failure
webhook-handler.ts
import express from 'express';

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

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

  switch (eventName) {
    case 'settlement.state.completed':
      creditUserAccount(data.transactionId);
      break;
    case 'execution.state.failed':
    case 'settlement.state.failed':
      handleFailure(data.transactionId, data.failure);
      break;
  }

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

Cancelling a Transaction

Cancel any time before broadcast (states: initiated, source_attached, quoted, signing):
POST /sdk/{environmentId}/transactions/{transactionId}/cancel
x-dynamic-checkout-session-token: dct_...
Returns the transaction with executionState: "cancelled". Once cancelled, create a new transaction to retry.

Complete Example

A self-contained TypeScript script that creates a checkout, executes a payment, 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.
checkout-example.ts
import { parseAbi, type WalletClient } from 'viem';

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

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

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

// --- One-time setup ---
async function createCheckout(): Promise<string> {
  const checkout = await api(`/environments/${ENV_ID}/checkouts`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${API_TOKEN}` },
    body: JSON.stringify({
      mode: 'payment',
      settlementConfig: {
        strategy: 'cheapest',
        settlements: [
          {
            chainName: 'EVM',
            chainId: '8453',
            symbol: 'USDC',
            tokenAddress: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913',
            tokenDecimals: 6,
          },
        ],
      },
      destinationConfig: {
        destinations: [
          {
            chainName: 'EVM',
            type: 'address',
            identifier: '0xYourDestinationWallet',
          },
        ],
      },
    }),
  });
  console.log('Checkout created:', checkout.id);
  return checkout.id;
}

// --- Per-payment flow ---
async function pay(checkoutId: string, amount: string) {
  // Step 2: Create transaction
  const { sessionToken, transaction } = await api(
    `/sdk/${ENV_ID}/checkouts/${checkoutId}/transactions`,
    {
      method: 'POST',
      body: JSON.stringify({
        amount,
        currency: 'USD',
        memo: { orderId: 'order_abc123' },
      }),
    }
  );
  const txId = transaction.id;
  const session = { 'x-dynamic-checkout-session-token': sessionToken };

  // Step 3: Attach source
  await api(`/sdk/${ENV_ID}/transactions/${txId}/source`, {
    method: 'POST',
    headers: session,
    body: JSON.stringify({
      sourceType: 'wallet',
      fromAddress: account.address,
      fromChainId: '8453',
      fromChainName: 'EVM',
    }),
  });

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

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

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

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

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

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

  // Step 8: Poll for settlement
  while (true) {
    const tx = await api(`/sdk/${ENV_ID}/transactions/${txId}`);

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

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

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

// --- Run ---
const checkoutId = await createCheckout();
await pay(checkoutId, '25.00');

Supported Chains and Native Tokens

The Checkout API supports the following chains. Use these values for chainName, chainId, and token addresses in your settlement config and source attachment.

Chain Reference

ChainchainNamechainIdNetworks
EVM"EVM"Standard EVM chain ID (e.g., "1" for Ethereum, "137" for Polygon, "8453" for Base, "42161" for Arbitrum, "10" for Optimism)All major EVM networks
Solana"SOL""101"Solana mainnet
Bitcoin"BTC""1"Bitcoin mainnet
Sui"SUI""501"Sui mainnet
Except for the EVM chains listed below, only mainnet is supported:
NameID
Base Sepolia Testnet"84532"
Arbitrum Sepolia Testnet"421614"
Arc Testnet"5042002"
OP Sepolia Testnet"11155420"

Native Token Addresses

For native tokens (ETH, SOL, BTC, SUI), use any of the accepted addresses below in token address fields:
ChainAccepted native token addresses
EVM0x0000000000000000000000000000000000000000 (zero address) or 0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
Solana11111111111111111111111111111111 (System Program) or So11111111111111111111111111111111111111112 (Wrapped SOL)
Bitcoin11111111111111111111111111111111 or bitcoin
Sui0x2::sui::SUI or 0x0000000000000000000000000000000000000000000000000000000000000002::sui::SUI
For non-native tokens, use the token’s contract address on that chain (e.g., 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913 for USDC on Base).

Error Reference

StatusCauseRecovery
400Invalid request bodyCheck field formats
401Missing or invalid session tokenVerify x-dynamic-checkout-session-token header
403Risk screening blockedCancel and retry with a different source
404Resource not foundVerify checkout/transaction IDs
409State conflict or duplicate tx hashCheck executionState and call the correct next step
422Quote expiredRe-quote (Step 4) and retry prepare
422Insufficient balanceSource wallet doesn’t have enough funds
422Risk not clearedPoll until riskState is "cleared", then retry

Tips

  • Balance assertions: Enable both assertBalanceForGasCost and assertBalanceForTransferAmount in prepare to catch insufficient balance before signing.
  • Quote evaluation: Check quote.fees.totalFeeUsd and quote.estimatedTimeSec programmatically before proceeding. Cancel and retry with a different token/chain if fees are too high.
  • Quote expiry: Quotes last 60 seconds. Sign promptly. You can re-quote multiple times — quoteVersion increments with each new quote.
  • Idempotency: Use the memo field to store your own idempotency keys (e.g., { "orderId": "order_abc123" }).
  • Error recovery: If your process crashes mid-flow, call GET /transactions/{id} to check the current executionState and resume from the correct step.
  • Session token lifetime: Matches the transaction’s expiresIn (default 1 hour).

Quick Reference

StepMethodEndpointAuth
Create checkoutPOST/environments/{envId}/checkoutsAPI token
Create transactionPOST/sdk/{envId}/checkouts/{checkoutId}/transactionsNone
Attach sourcePOST/sdk/{envId}/transactions/{txId}/sourceSession token
Get quotePOST/sdk/{envId}/transactions/{txId}/quoteSession token
Prepare signingPOST/sdk/{envId}/transactions/{txId}/prepareSession token
Record broadcastPOST/sdk/{envId}/transactions/{txId}/broadcastSession token
CancelPOST/sdk/{envId}/transactions/{txId}/cancelSession token
Poll statusGET/sdk/{envId}/transactions/{txId}None