Skip to main content

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 reusable checkout configuration (one-time setup)
  • 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 wallet with private key access (for signing)
  • 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:
1. Create checkout     (one-time setup — defines where funds go)
2. Create transaction  (initiates a payment, returns a session token)
3. Attach source       (declare which wallet/chain you're 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 on-chain       (sign and broadcast using your wallet key)
7. Record broadcast    (report the tx hash back to the API)
8. Poll for settlement (wait for funds to arrive at destination)
Each call advances the state — calling endpoints out of order returns 409.

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.

Step 1: Create a Checkout (One-Time Setup)

A checkout is a reusable payment configuration. It defines what token(s) you want to receive and the destination wallet(s). Create it once, then reuse the checkoutId for every payment.
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"
      }
    ]
  }
}
FieldDescription
mode"payment" — receiver specifies the amount. "deposit" — sender specifies the amount
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

Settlement Strategies

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
Response (201): Returns the checkout object with an id. Save it — you’ll reuse it for every payment. Manage checkouts with GET, PATCH, and DELETE on /environments/{environmentId}/checkouts/{checkoutId}.

Step 2: Create a Transaction

Start a new payment. 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
amountPayment amount as a string
currencyCurrency code (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: Attach Source

Declare which wallet and chain you’re 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"
fromAddressYour 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: Get a Quote

Specify which token you’re paying with. The API finds the best route to your checkout’s settlement token(s).
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"
  }
}
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: 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: Sign and Broadcast On-Chain

Use quote.signingPayload to sign and submit the transaction. If evmApproval is present, send the approval transaction first.
sign.ts
import { createWalletClient, http, publicActions, parseAbi } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';

const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const client = createWalletClient({
  account,
  chain: base,
  transport: http(),
}).extend(publicActions);

// Handle ERC-20 approval if needed
const { signingPayload } = prepared.quote;

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

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

Step 7: Record the Broadcast

Report the transaction hash back to the API.
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: 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
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);
});

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:
checkout-example.ts
import { createWalletClient, http, publicActions, parseAbi } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { base } from 'viem/chains';

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

const account = privateKeyToAccount(
  process.env.PRIVATE_KEY as `0x${string}`
);
const client = createWalletClient({
  account,
  chain: base,
  transport: http(),
}).extend(publicActions);

// --- 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 client.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 client.waitForTransactionReceipt({ hash: approvalHash });
    console.log('Token approved');
  }

  const txHash = await client.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 (mainnet only). 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

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