Stablecoin Checkout: Integrate USDC and USDT Payments
Build a stablecoin checkout for USDC and USDT with clear payment instructions, wallet flows, finality, webhooks, exception handling, and conversion tracking.
A stablecoin checkout turns a business order into precise onchain payment instructions, helps a customer pay in USDC or USDT, and gives the merchant backend verified evidence for fulfillment. A complete integration connects amount, token, network, destination, expiry, wallet interaction, and payment status. A connect-wallet button covers only one part of that flow.
If you are still choosing an acceptance model, start with the business guide to accepting stablecoin payments. This article focuses on the next decision: how to build the customer journey, recover interrupted attempts, and release an order safely.
What is a stablecoin checkout, and which integration model fits?
A stablecoin checkout connects an order to a wallet transfer and merchant fulfillment. A payment link is an entry point, a wallet signs and submits the transfer, and a payment API creates orders and reports their state. Those components have different responsibilities.
Evaluate interface hosting separately from custody. A provider can operate the payment page while the stablecoin goes directly to a merchant-controlled address. StableOps uses that model: the merchant keeps the private keys and signs refunds. It does not provide automatic fiat conversion or bank settlement.
| Integration model | Where the customer pays | Merchant implementation | Useful when | Validate before choosing |
|---|---|---|---|---|
| Hosted checkout | Provider-operated page | Backend creation, redirect, events, order recovery | A standard payment journey meets the product need | Branding, mobile wallets, return paths, custody, exception states |
| Embedded component | Component inside the merchant site | Component integration, page state, backend order and fulfillment | Staying on site matters and component limits are acceptable | Wallet compatibility, loading failures, mobile browsers, upgrade costs |
| Custom payment API flow | Merchant-operated payment interface | Instructions, wallet adapters, status queries, recovery, fulfillment | The business needs a specialized journey | Token identity, precision, manual payments, maintenance effort |
These are general integration models, not a claim that every provider ships all three. StableOps offers hosted sessions, while a custom interface can use Payment Orders and the wallet SDK. An embedded experience requires a separate assessment of the component and the work your team must own.
Use stablecoin payment links for a public, fixed-price entry point. Create a one-time session for carts, customer-specific prices, or dynamic taxes. For accounts receivable, define the invoice-to-payment relationship before choosing the checkout surface.
What should a USDC or USDT checkout display?
Give the payer one complete instruction they can inspect before signing and recognize after returning from a mobile wallet. Critical facts should remain visible without a hover tooltip or a particular wallet extension.
| Checkout information | Required presentation | Failure it prevents |
|---|---|---|
| Product and order | Product, merchant identity, support reference | Paying the wrong business order |
| Exact payable amount | Returned order.amount and token | Sending the base amount after automatic amount allocation |
| Network and environment | Full network name and mainnet or testnet label | Sending the right symbol on the wrong chain |
| Token identity | Inspectable contract or mint address | Confusing native, bridged, or unrelated same-symbol tokens |
| Destination | Full address from the selected active instruction | Copying another chain's or expired attempt's address |
| Expiry | Server-returned order.expiresAt and countdown | Treating a locally extended timer as a valid payment window |
| Fees | Required net receipt and who pays network or withdrawal fees | Deducting a withdrawal fee from the amount owed |
| State and help | Waiting, submitted, detected, confirming, final, support contact | Calling a transaction hash a completed payment |
In a StableOps Payment Order, amount and expiry are top-level fields. paymentInstructions supplies candidate chain, asset, and address combinations. Do not read amount or expiry from an instruction, or combine fields from different candidates.
Identify the asset by its network and contract, not its symbol. Circle's USDC contract list separates mainnet and testnet identities. Tether's supported protocols lists USD₮ information by network. Issuer support does not establish support in your payment provider or wallet, so verify your own allowed combinations too.
With amountMode: 'auto', StableOps may adjust the amount by a token's smallest unit to resolve a shared-address conflict. Hosted checkout displays checkout.paymentOrder.amount; custom checkout must use the same returned value. exact preserves the fixed amount. Both modes require exact payment and do not automatically accept short, excess, or split transfers.
How do you create a checkout session on the server?
Authenticate the customer and validate the price and business order before creating a payment attempt. Persist the idempotency key and the entire creation request. Retrying that attempt must reuse both, rather than reserving another address whenever the page reloads.
This minimal Next.js handler uses merchant-owned database functions. loadAuthorizedCheckoutAttempt must verify order ownership and persist the request snapshot on first creation. saveCheckoutMapping saves the relation needed for event handling. Neither function is a StableOps SDK method. Use a Sandbox API key and Base Sepolia USDC for this example.
// app/api/checkout/route.ts
import { StableOps } from '@stableops/api-sdk'
import {
loadAuthorizedCheckoutAttempt,
saveCheckoutMapping,
} from '@/lib/checkout-store'
export const runtime = 'nodejs'
const stableops = new StableOps({
apiKey: process.env.STABLEOPS_API_KEY!,
})
export async function POST(req: Request) {
// Load the persisted amount, expiry, and complete request snapshot.
const attempt = await loadAuthorizedCheckoutAttempt(req)
const checkout = await stableops.checkoutSessions.create(
attempt.checkoutRequest,
{ idempotencyKey: attempt.id },
)
if (!checkout.url) return new Response('checkout unavailable', { status: 502 })
await saveCheckoutMapping({
attemptId: attempt.id,
checkoutSessionId: checkout.id,
paymentOrderId: checkout.paymentOrder.id,
requestedAmount: checkout.paymentOrder.requestedAmount,
payableAmount: checkout.paymentOrder.amount,
})
return Response.json(
{ url: checkout.url },
{ headers: { 'Cache-Control': 'no-store' } },
)
}Persist attempt.checkoutRequest with fields like these when the attempt is first created. Values come from the authorized order and server configuration, not a browser-supplied final price or arbitrary redirect destination.
import type { CreateCheckoutSessionInput } from '@stableops/api-sdk'
const checkoutRequest: CreateCheckoutSessionInput = {
merchantOrderId: attempt.merchantOrderId,
amount: order.amount,
amountMode: 'auto',
acceptedAssets: [{ chain: 'base-sepolia', asset: 'USDC' }],
expiresAt: attempt.expiresAt,
title: order.title,
successUrl: order.successUrl,
cancelUrl: order.cancelUrl,
walletConnectProjectId: process.env.WALLETCONNECT_PROJECT_ID || undefined,
}The snapshot includes amount, title, expiry, allowed assets, redirects, and WalletConnect configuration. Recomputing the expiry or reading changed configuration on a retry produces a different request under the same key. Once an attempt expires, verify that the business order remains unpaid, then create a distinct attempt and key while preserving every attempt's mapping to that order.
The frontend can redirect with window.location.assign(url). A one-time URL contains the clientSecret required to access that payment session. Give it only to the current payer and keep it out of shared caches, public order listings, and analytics.
Merchant backend Checkout and payer wallet Chain and payment operations
Persist order + attempt
Create session --------> Display complete instructions
Customer checks and signs ------> Broadcast transfer
Show submitted Detect, match, confirm
Verify and dedupe <--------------------------------------- payment.finalized
Commit order update + durable fulfillment job
Fulfill idempotently --> Customer order page shows server resultThe return page reads the merchant backend's stored order state. successUrl and cancelUrl are browser destinations, not authority to mark an order paid or cancel a broadcast transfer. The Next.js USDC integration guide covers the complete application structure.
How should checkout recover wallet errors and interrupted payments?
Preserve what already happened and avoid unnecessary repeat transfers. First distinguish an attempt that has not broadcast from a transaction whose result is unknown. That distinction determines the recovery action.
| Situation | Payer feedback | Recovery behavior |
|---|---|---|
| No installed or detected wallet | Offer a mobile or manual route | Keep the attempt rather than requiring a specific installation |
| Customer rejects connection, network switch, or signing | Say the action was canceled | Wait for a deliberate retry instead of reopening prompts |
| Wrong wallet network | Show the required network name | Recheck account, network, and balances after switching |
| Network-switch request is pending | Ask the payer to finish in the wallet | Disable duplicate switch requests |
| Insufficient native fee balance | Explain token balance and fee-asset needs separately | Let the payer fund the wallet without reducing the amount owed |
| Broadcast succeeded, but the page closed or a query timed out | Show that payment is being checked | Restore the attempt and query the backend before suggesting another transfer |
| Payment window ended without detection | Explain that these instructions expired | Stop presenting them as payable and investigate late transfers separately |
| Wrong token, wrong network, short or excess payment | Show that review is required | Preserve evidence without advancing normal success state |
MetaMask's network-management documentation distinguishes missing networks, rejected requests, and pending requests. Show a response appropriate to the cause. A wallet error also does not establish whether the server has detected a separate transfer.
Ethereum's gas documentation explains that its transaction fees are paid in ETH. Other networks have their own fee assets and rules. Keep that requirement separate from token balance and from exchange withdrawal fees. The stablecoin payment fee guide explains total payer and merchant costs.
For an unknown broadcast result, support should inspect the network, transaction hash, actual token, destination, and amount first. Closing the page, disconnecting a wallet, or clicking cancel cannot reverse an already broadcast transaction.
Why must verified webhooks authorize fulfillment?
Browser redirects and wallet receipts originate in the customer environment and are progress signals. Verified server events connect the matched order to its payment state. For shipping, irreversible access, and final ledger entries, a StableOps integration should wait for payment.finalized.
The handler below verifies the exact raw body before parsing and hands the event to durable storage. recordAndApplyPaymentEvent is a merchant implementation boundary, not an in-memory set or a direct call to a shipping API.
// app/api/webhooks/stableops/route.ts
import {
EVENT_ID_HEADER,
SIGNATURE_HEADER,
verifySignature,
} from '@stableops/api-sdk/webhooks'
import { recordAndApplyPaymentEvent } from '@/lib/checkout-store'
export const runtime = 'nodejs'
export async function POST(req: Request) {
const rawBody = await req.text()
const verified = verifySignature({
secrets: [process.env.STABLEOPS_WEBHOOK_SECRET!],
header: req.headers.get(SIGNATURE_HEADER) ?? undefined,
rawBody,
})
if (!verified.ok) return new Response('invalid signature', { status: 400 })
const eventId = req.headers.get(EVENT_ID_HEADER)
if (!eventId) return new Response('missing event id', { status: 400 })
let event: unknown
try {
event = JSON.parse(rawBody)
} catch {
return new Response('invalid event body', { status: 400 })
}
// Validate event shape and order mapping, then commit event + business changes.
await recordAndApplyPaymentEvent({ eventId, event })
return new Response('accepted')
}The storage boundary validates event structure and, in one transaction, inserts a unique event ID, checks the saved order mapping, and applies a legal state transition. That mapping must bind the saved Payment Order ID to the merchant's business reference, rather than granting access from a customer-supplied identifier. Only a first, verified payment.finalized event can insert an irreversible fulfillment job. Give that job a business-order uniqueness constraint and use the business order ID as the downstream idempotency key, so separate paid attempts cannot deliver the same order twice.
Duplicate events return success without side effects. A failed database commit returns failure so delivery can retry. Acknowledging first and retaining the work only in memory loses orders when the process crashes. The payment-confirmation guide explains the state boundary, and the webhook fulfillment guide covers atomic event storage and durable jobs.
How do you measure stablecoin checkout conversion?
Measure stages within the same created-session cohort, deduplicate by session, and set an observation cutoff. A browser visit, a payment attempt, and a business order are different denominators. Label the report accordingly.
This is a suggested merchant analytics model, not a list of fields in StableOps' built-in dashboard.
| Suggested event | Exact trigger | Recording source | Question it answers |
|---|---|---|---|
checkout_started | Backend created the session | Merchant backend | How many valid attempts began? |
checkout_loaded | Page retrieved that session's instructions | Checkout interface | Did redirect or loading fail? |
wallet_connected | Customer approved wallet connection | Wallet interface | Where does the wallet path lose customers? |
transaction_submitted | Wallet returned a submitted transaction hash | Wallet interface | Was a transfer attempted? This is not receipt evidence. |
payment_detected | Backend matched a transfer to the order | Verified event or server query | Did the submitted payment match? |
payment_finalized | Payment reached final fulfillment state | Verified event or server query | How many payments completed? |
order_fulfilled | Business job completed and persisted its result | Merchant backend | Which paid orders remain undelivered? |
Manual transfers can reach detection without a wallet-connection or frontend-submission event. Treat the table as stages you can observe, not a mandatory sequence for every payer. Do not record private keys, session secrets, or complete one-time checkout URLs in analytics.
For a hypothetical weekly cohort, suppose 1,000 distinct sessions were created, 900 loaded, 450 finalized by the cutoff, and 440 were fulfilled:
Page load rate = loaded sessions / created sessions = 900 / 1,000 = 90%
Payment completion rate = finalized sessions / created sessions = 450 / 1,000 = 45%
Paid-session fulfillment rate = fulfilled sessions / finalized sessions = 440 / 450 ≈ 97.8%These are illustrative figures, not product performance. A production report also reconciles multiple attempts for one business order and tracks paid orders that still need fulfillment. More page interactions alone do not prove better payment outcomes.
StableOps currently records checkout stages including page viewed, session loaded, payment started, transaction submitted, and payment completed. See the Checkout documentation for its exact denominator and cohort. The suggested wallet-connected, payment-detected, and business-fulfilled stages may need your own instrumentation or server records. Marketing analytics is not a financial ledger.
What should you verify before launching a stablecoin checkout?
- API keys and webhook secrets stay on the server, with separate Sandbox and Live configuration.
- The backend validates customer identity, order ownership, price, and return destinations.
- Creation retries reuse the entire persisted request, idempotency key, and expiry.
- The interface shows the returned exact amount, network, token identity, destination, and deadline.
- Mobile switching, rejected requests, insufficient fee balances, and manual payments have recovery paths.
- An unknown broadcast result restores the original attempt rather than asking for an immediate repeat payment.
- Raw-body verification, event deduplication, state changes, and fulfillment jobs share a durable boundary.
- Late events cannot regress state, and separate attempts cannot fulfill one business order twice.
- Wrong-network, wrong-token, amount, and late-payment exceptions retain evidence and a support route.
- Visiting the success URL directly cannot ship an order.
- Funnel reports specify denominator, deduplication, cutoff, and manual-payment behavior.
Use the three-layer crypto-payment testing method to validate these conditions before adding more networks. Start in the Sandbox Playground, observe one real testnet order, and connect the same instructions and final fulfillment boundary to your product using the Checkout documentation.
What are common questions about stablecoin checkout?
Does hosted checkout mean the provider holds the money?
Not necessarily. Interface hosting and custody are separate decisions. StableOps operates the checkout page while stablecoins reach merchant-controlled addresses. Private keys, treasury operations, and refund signing remain with the merchant.
Should I use a one-time session or a payment link?
Use reusable links for public, fixed-price services. Create one-time sessions when a cart, customer, invoice, or dynamic price must be bound to a specific attempt with its own order and state.
Must every payer connect a wallet?
No. Customers can transfer manually using the active order's instructions. StableOps hosted checkout provides browser-wallet and manual routes. The WalletConnect mobile entry requires session configuration and compatibility checks for the selected wallet and network.
Can one checkout accept both USDC and USDT?
Yes, with combinations actually supported by your payment service. The selected network, token, amount, and destination must belong to the same valid instruction. A token symbol alone cannot authorize a substitution.
Can I ship when the customer reaches the success URL?
No. Anyone can visit that URL, and a payer may return before finality. Authorize irreversible fulfillment from a verified, deduplicated payment.finalized event. The page displays the business state already stored by your backend.
Will processing continue after the payer closes the page?
A broadcast transfer can still be detected and confirmed, and webhook delivery does not depend on an open browser. Restore the server's order state when the customer returns instead of treating lost frontend state as a reason to pay again.
Does a checkout API automatically convert funds or issue refunds?
That depends on the provider's funds and settlement model. StableOps does not custody funds or automatically convert to fiat or settle to a bank. A merchant-signed refund is a separate transaction whose state must also be tracked.
Protocol sources and StableOps product behavior were checked on October 1, 2026. The snippets illustrate integration boundaries, and merchant database functions require implementation for your own order model. Recheck official token information, wallet compatibility, and current service support before going live.
Related articles
Learn how stablecoin invoicing works, issue bills payable in USDC or USDT, match on-chain payments, handle exceptions, and reconcile every paid invoice.
Learn how stablecoin payment processors handle checkout, confirmation, webhooks, and reconciliation, then compare fees, custody, APIs, and operational controls.
Calculate the true cost of USDC and USDT payments across network fees, provider pricing, treasury, off-ramp spreads, exceptions, and reconciliation.
Learn how to accept stablecoin payments with USDC or USDT, compare custody models, and launch reliable checkout, webhooks, refunds, and reconciliation.