Back to Blog
Engineering
2026-07-149 min readBy StableOps

How to Accept USDT Payments Reliably: Addresses, Confirmations, and Webhooks

Accept USDT payments with chain-specific instructions, exact order matching, confirmation tracking, and verified webhook fulfillment.

USDT
Stablecoin payments
Payment architecture

If you want to accept USDT payments in production, the first design choice is not “which wallet address should we show?” It is “which chain, asset, amount, address, and business order are we asking the customer to pay?”

USDT exists on multiple chains. A transfer of the same named asset on the wrong network is not a successful payment for your order, even if the amount looks right. A reliable USDT flow therefore needs explicit payment instructions, deterministic matching, confirmation and reorg handling, and a verified webhook boundary for fulfillment. StableOps provides that order and event layer while the merchant keeps control of the receiving addresses.

USDT is an asset plus a chain

Customers often think of USDT as one balance. Payment systems cannot. USDT on TRON, Ethereum, Optimism, BNB Chain, or Solana is represented by different chain events, address formats, token contracts or mints, fees, and confirmation behavior. Your checkout must ask for a specific (chain, asset) pair, not just “send USDT.”

The practical model is:

Business order
      |
      v
Payment Order: amount + accepted chain/asset pairs + expiry
      |
      v
Payment instructions: chain + USDT + address + exact amount
      |
      v
Chain transfer matched by organization, environment, chain, asset, address, and amount
      |
      v
payment.detected -> payment.confirmed -> payment.finalized

StableOps rejects unsupported (chain, asset) pairs at order creation. For supported pairs, it allocates a receiving candidate per accepted pair and returns the exact paymentInstructions your application or Checkout page should render. The supported assets table in the Introduction remains the source of truth for live chain coverage.

Create one order with explicit USDT options

Create and persist your business order before creating the Payment Order. Its immutable identifier becomes merchantOrderId, and the amount and expiry should be stable across retries. Do not create a new payment attempt with a fresh expiry just because a browser refreshed.

import { StableOps } from '@stableops/api-sdk'

const stableops = new StableOps({
  apiKey: process.env.STABLEOPS_API_KEY!,
})

export async function createUsdtPayment(order: {
  id: string
  amount: string
  paymentExpiresAt: string
}) {
  return stableops.paymentOrders.create(
    {
      merchantOrderId: order.id,
      amount: order.amount,
      acceptedAssets: [
        { chain: 'tron', asset: 'USDT' },
        { chain: 'ethereum', asset: 'USDT' },
        { chain: 'solana', asset: 'USDT' },
      ],
      expiresAt: order.paymentExpiresAt,
      metadata: { product: 'checkout' },
    },
    { idempotencyKey: `payment-order:${order.id}` },
  )
}

On retry, pass the same merchantOrderId, amount, accepted assets, expiry, and idempotency key. Render the returned paymentInstructions rather than reconstructing addresses or token contracts in the browser. If the payer selects TRON USDT, show the TRON instruction. If they select Ethereum USDT, show the Ethereum instruction. Those are different payment paths, even when the business order and invoice amount are the same.

amount is a decimal asset-unit string. Chain events are compared in smallest units after applying token decimals, so display the exact amount from order.amount, the expiry from order.expiresAt, and the chain, asset, and address from the selected paymentInstructions entry. If you use shared addresses with automatic amount adjustment, the returned order.amount may differ from the base amount you requested; the customer must pay the returned amount exactly. See Payment Orders for allocation and matching behavior.

Match the full instruction, not a wallet balance

A USDT payment gateway or direct wallet flow becomes unreliable when it treats “wallet balance increased” as payment proof. That loses the relationship between the transfer and the order, especially when multiple chains and many customers are active at the same time.

StableOps promotes an order only when the transfer matches the open order on every relevant key:

Match keyWhy it matters
Organization and environmentPrevents test, live, and tenant records from bleeding into each other
ChainTRON USDT is not Ethereum USDT; the network is part of the payment instruction
AssetUSDT sent to a USDC-only order is not a match
Receiving addressConnects the transfer to the allocated candidate
Exact amountPrevents underpayments, overpayments, and shared-address ambiguity
Open order stateLate transfers should not silently revive an expired or canceled attempt

Keep this tuple together in your own records:

(internal order id, StableOps payment order id, chain, asset, address, amount, expiry)

Support, reconciliation, and fulfillment should refer to that tuple, not only to a transaction hash or wallet balance. A transaction hash is useful for investigation, but it is not enough to decide that a specific order is paid.

Be deliberate about address reuse

Address policy is an operating decision, not just a wallet-management detail.

Address policyBest fitReliability trade-off
Single-use address per orderCheckout, invoices, and one-time purchasesClearest attribution; requires enough available addresses for each accepted chain and asset
Shared address with exact amountsFixed-price tiers and limited address poolsRequires exact unique amounts among in-flight orders on the same address
Shared address with automatic amount adjustmentRepeated fixed prices where tiny amount nudges are acceptableEasier operations, but the checkout must display and enforce the returned adjusted amount

For USDT, address formats differ by chain family. EVM addresses use 0x..., TRON addresses are base58 strings beginning with T, and Solana token accounts and wallet addresses follow Solana conventions. Store and display the address returned in paymentInstructions; do not apply a normalization rule that is correct for one chain family to another.

Address capacity also affects checkout reliability. If you use single-use pools, alert before eligible USDT addresses are exhausted. If you use shared pools, monitor same-amount collisions, expired orders, and customers who manually edit the amount.

Use confirmations as progress and finalized as the fulfillment boundary

USDT payment confirmation is not one universal block count. Each chain has its own timing, finality assumptions, and reorg risk. Your application should react to the normalized payment lifecycle instead of hard-coding a single number everywhere.

EventWhat it meansGood useDo not use it for
payment.detectedA matching USDT transfer was observedShow “payment received, confirming” and retain the transaction hashShipping, granting irreversible access, or final ledger posting
payment.confirmedThe configured confirmation level was reachedUpdate progress or perform carefully reversible workAssuming every chain risk has passed
payment.finalizedThe payment reached the final fulfillment stateFulfill idempotently and write the final receipt recordSkipping webhook verification or event deduplication
payment.revertedA previously observed payment no longer standsStop optimistic work and run compensation or reviewIgnoring rare rollback paths
payment.expiredNo matching transfer arrived before the deadlineClose the attempt and ask the customer to start a new oneTreating a later transfer as automatic success

For irreversible actions, wait for a verified and deduplicated payment.finalized webhook. The confirmation model explains the state machine, and Stablecoin Payment Confirmations gives a decision framework for product teams.

Make webhooks the durable handoff to your app

The browser can return after the customer submits a wallet transaction. A wallet can show a hash. Neither is a final payment credential. Your backend should fulfill from a signed webhook after it has durably accepted the event.

Use this boundary:

Receive StableOps webhook
          |
          v
Verify raw body signature
          |
          v
Persist unique X-Event-Id and legal order transition in one transaction
          |
          v
For first verified payment.finalized, enqueue irreversible fulfillment by internal order ID
          |
          v
Return 2xx, then let a worker fulfill idempotently

Event deduplication and fulfillment idempotency solve different problems. The unique event ID prevents retries and replays from applying the same event twice. Fulfillment idempotency by internal order ID prevents a product entitlement, shipment, or ledger write from happening twice even if two valid code paths race. The full webhook handling pattern is covered in Stablecoin Payment Webhooks and the Webhook guide.

Plan for wrong network, wrong amount, and expired orders

Reliable USDT collection includes a support policy for misses. Do not improvise these cases after the first customer sends funds incorrectly.

CaseWhat StableOps doesWhat your team should do
Wrong chainThe transfer does not match an order that did not accept that chainInvestigate the on-chain transfer and recover or refund out of band from the wallet you control
Wrong assetThe order does not advance if the asset is not the accepted assetTreat it as a support exception, not automatic payment success
Underpayment or overpaymentThe order does not match because the amount differs in smallest unitsLet the order expire or resolve manually with a top-up, refund, or credit policy
Late transfer after expiryThe original order remains expiredReconcile manually; create a new order when the customer should try again
Address pool exhaustedA complete instruction cannot be issued for that chain and assetImport more addresses or narrow the accepted chain/asset choices

Because StableOps is non-custodial, you recover misdirected funds yourself from the addresses you control. StableOps can help you identify what happened, but it does not hold private keys or move funds for you. The wrong amount or chain FAQ covers the operational behavior in more detail.

Production checklist

  • Choose the exact USDT chains your product will accept; do not present “USDT” without a network.
  • Import and monitor enough eligible receiving addresses for each accepted chain and asset.
  • Persist the business order before creating the Payment Order, and reuse the same idempotency key on retries.
  • Render order.amount and order.expiresAt together with chain, asset, and address from the selected paymentInstructions entry.
  • Keep the selected instruction tuple in your own reconciliation records.
  • Define support policies for wrong chain, wrong asset, underpayment, overpayment, expiry, and late transfers.
  • Use payment.detected and payment.confirmed for progress; reserve irreversible fulfillment for verified payment.finalized.
  • Verify webhooks from the raw body, store X-Event-Id with a unique constraint, and make fulfillment idempotent by internal order ID.
  • Test duplicate delivery, replay, expired orders, and a worker crash before launch.

FAQ

Can I accept USDT on multiple chains with one integration?

Yes, if those (chain, asset) pairs are supported and you have eligible receiving addresses for them. Create the Payment Order with explicit USDT pairs such as TRON, Ethereum, or Solana, then render the returned instruction for the customer's selected network.

Is a USDT payment confirmed when the customer sends a transaction hash?

No. A transaction hash is an investigation and progress signal. It is not a final receipt for a business order. Fulfill only after your backend verifies and deduplicates a payment.finalized webhook.

What happens if the customer sends USDT on the wrong chain?

The order does not advance unless the transfer matches an accepted chain, asset, address, amount, and open order. Because you control the receiving addresses, any recovery or refund happens through your own treasury or support process, outside the automatic order lifecycle.

Start with one network, then expand deliberately

The safest way to launch USDT payments is to start with one chain, one address policy, one webhook endpoint, and one idempotent payment.finalized fulfillment path. Once that loop survives duplicate delivery, expiry, and support misses in Sandbox, add more USDT networks with the same matching and reconciliation discipline.

To put this flow into practice, import receiving addresses, then use Payment Orders to create your first chain-specific USDT order in Sandbox. Before launch, turn the wrong amount or chain FAQ into a support runbook for payment exceptions.

Related articles

Build reliable crypto payment webhook handling with raw-body signature verification, event deduplication, idempotent fulfillment, and replay-safe recovery.

Accept USDC payments into merchant-controlled wallets with payment orders, chain monitoring, finality, and reliable webhook fulfillment.

Compare custodial gateways, direct wallet transfers, and non-custodial payment infrastructure for reliable stablecoin collection.