Guide

How to accept USDT payments on Tron, Ethereum, and Solana

USDT is the asset most of your customers actually hold, and it lives on more than one network. The same dollar of Tether can arrive as TRC-20 on Tron, ERC-20 on Ethereum, or SPL on Solana — three different addresses, three different fee profiles, one stablecoin. This guide shows how to accept all three with halfin: price the bill in dollars, let the platform lock the rate and pin the payable USDT amount, take the deposit on whichever network the customer reaches for, and confirm settlement from a signed webhook rather than the checkout screen.

01

USDT is one asset on three networks — and that matters at checkout

Tether is not a single thing your integration accepts. The token is issued natively on several chains, and a customer holding "USDT" holds it on a specific one. The most common is TRC-20 on Tron, because the transfer fee is small and predictable; ERC-20 on Ethereum is what many exchanges and DeFi-native users withdraw by default; SPL on Solana is fast and cheap and increasingly common. halfin runs real gates for all three, so a USDT invoice can be paid on any of them.

The network the customer pays on is load-bearing, not cosmetic. A USDT address on Tron is not a USDT address on Ethereum, and sending TRC-20 USDT to an ERC-20 address loses the funds. This is the single most common way a crypto payment goes wrong, and it is the thing your checkout has to get right: the customer must see, and pay to, the address for the network they actually chose. halfin handles the address-per-network mapping for you — your job is to surface the choice clearly and not let a customer pay the wrong rail.

Because all three are dollar-pegged, USDT is the easy case for fiat-anchored billing. A customer paying a $49 invoice in USDT pays roughly 49 USDT regardless of network, with only the small network fee differing between rails. That makes USDT the lowest-friction stablecoin to bill in — there is no volatility conversation to have with the customer, just a choice of which network they want to move it on.

NetworkUSDT standardWhat it's good for
TronTRC-20Lowest, most predictable transfer fee — the default for cost-sensitive payers
EthereumERC-20What many exchanges and DeFi users withdraw by default; higher network fee
SolanaSPLFast finality and low fee for customers who already hold value on Solana
02

Step 1 — Decide which USDT networks you'll settle on

Before you write a line of integration code, decide which of the three USDT rails you want to receive. You do not have to enable all of them, and there is a real trade-off. TRC-20 on Tron is the rail to offer if you only offer one — the fee is low enough that customers paying small amounts are not surprised by it, and it is what a large share of USDT holders default to. ERC-20 widens reach to Ethereum-native and exchange-withdrawal users at the cost of a heavier network fee that the customer pays. SPL on Solana adds a fast, cheap rail for customers already on Solana.

Enable the networks you are comfortable receiving and reconciling. Each rail you turn on is one more address type your support team may field questions about and one more network whose deposits land in your balance. A common starting shape is TRC-20 plus one of the others — TRC-20 to cover the cost-sensitive majority, ERC-20 or SPL to cover the customers who hold USDT somewhere else. You can widen later without changing how you create invoices.

Whatever set you pick, the invoice you create in the next step draws from it. The customer chooses their network at pay time from the rails you enabled; you do not have to predict, per customer, which one they will use.

  • TRC-20 (Tron) — enable this first; lowest-friction USDT rail for most payers.
  • ERC-20 (Ethereum) — adds exchange-withdrawal and EVM-native reach; higher fee.
  • SPL (Solana) — fast, cheap rail for customers already holding on Solana.
  • You can enable a subset and widen later — invoice creation doesn't change.
03

Step 2 — Create a fiat-anchored invoice in USD

You price in dollars; the customer pays in USDT. Do not hardcode a USDT amount into the invoice — anchor it to the fiat figure you actually bill and let halfin compute the payable USDT. A fiat-anchored invoice carries the USD amount as the source of truth; halfin quotes the USDT amount and locks that quote when the invoice activates, so the customer pays the dollar value even if they sit on the page for a few minutes. With a dollar-pegged asset the quote is close to one-to-one, but the lock and the bounded expiry still apply, which keeps your reconciliation clean.

The create call below is fiat-anchored: you send amount_fiat and fiat_currency as strings — monetary values are strings end to end, never floats — and halfin returns an invoice with a payable USDT amount, the per-network deposit addresses, and a hosted checkout URL. Pass an idempotency_key derived from your own order identifier so a retried request from your backend returns the existing invoice instead of billing the customer twice. The exact response schema lives in the API reference at docs.thehalfin.com; what matters here is that one authenticated call gives you a USDT-payable invoice tied to the USD figure you billed.

If your backend is TypeScript, the same call is available through the @halfin/sdk-merchant client with typed request and response shapes — use whichever fits your stack. Either way, store the returned invoice id against your order, hand the customer the checkout URL, and wait for the signed webhook in Step 4 before you treat the order as paid.

curl -X POST https://api.thehalfin.com/api/v1/invoices \
  -H "X-API-Key: $HALFIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_fiat": "49.00",
    "fiat_currency": "USD",
    "deferred": true,
    "description": "Order #7f3a91",
    "idempotency_key": "order_7f3a91"
  }'

# halfin locks the rate at activation, pins the payable USDT amount, and
# returns per-network deposit addresses plus a hosted checkout URL on
# checkout.thehalfin.com. Store the invoice id against your order and wait
# for the signed invoice.paid webhook. Full schema: docs.thehalfin.com.
04

Step 3 — Get the customer to the right USDT address

Now the customer has to pay. Two paths, same invoice underneath. Redirect to halfin's hosted checkout when you want the smallest integration: the create-invoice response carries a checkout URL on checkout.thehalfin.com, you send the customer there, and halfin renders the network choice, the correct address for the rail they pick, the QR code, the exact USDT amount, and the live payment status. Render your own self-hosted checkout when the payment screen must live inside your product — same API, same invoice, but you present the address and amount yourself.

If you build the screen yourself, the network-versus-network rule from the top of this guide is your responsibility to get right. Show the customer which network they are paying on, and show the deposit address that belongs to that network — never let the TRC-20 address sit next to a label that says Ethereum. The amount is the pinned USDT figure from the invoice; render it as the exact string the API returned rather than recomputing it, so the customer pays the amount halfin is expecting. A QR code that encodes the address (and amount where the wallet supports it) cuts down on copy-paste mistakes, which is where most underpayments come from.

Whichever surface you use, do not block your own request waiting for the payment. The customer pays asynchronously from their wallet; your backend learns the outcome from the webhook in the next step, not from the checkout page returning.

  • Hosted checkout: send the customer the checkout URL and let halfin own the address/QR/status.
  • Self-hosted: render the per-network address yourself — never mislabel a rail.
  • Show the pinned USDT amount as the exact string the API returned; don't recompute it.
  • Encode address and amount in a QR to cut copy-paste underpayments.
05

Step 4 — Confirm settlement from a signed webhook

A USDT transfer is not final the moment the wallet says "sent". The transaction has to land in a block and accumulate enough confirmations that a chain reorganization is no longer a realistic risk, and that threshold differs per network. halfin applies a per-chain confirmation threshold with reorg-aware crediting, so an invoice it reports as paid has settled under that network's rules rather than merely being seen in the mempool. For your integration this means one rule: do not release goods or mark the order paid on first sight of a transaction — wait for the paid event.

That event arrives as an HMAC-signed webhook. When the invoice reaches its terminal state, halfin POSTs a signed event to your endpoint independently of whatever the customer's browser did. Verify the signature before you act: recompute the HMAC over the exact raw request bytes — before any JSON parsing or framework middleware reserializes the body — and compare it to the signature header in constant time. Only after that comparison passes do you deserialize the event and treat it as a business fact. An unsigned or mismatched request is not a halfin event and must never fulfil an order; checking the signature first is what stops a forged "paid" callback from giving away goods.

Handle the underpaid and overpaid cases deliberately, because they happen with USDT. A customer who withdraws from an exchange that skims the network fee out of the same balance can arrive a fraction short — that surfaces as invoice.underpaid with the shortfall recorded against the invoice, and you decide whether to request a top-up or settle partially. An overpayment records the excess the same way. Keep the handler idempotent: webhooks are delivered at least once and carry a stable event id, so a redelivered invoice.paid must extend the order exactly once, not twice.

Webhook eventWhat it means for a USDT paymentWhat your handler does
invoice.confirmingA USDT deposit is on-chain and confirmations are accumulatingShow "payment in flight" — do not fulfil yet
invoice.paidConfirmed past the network's threshold; USD figure settledFulfil the order (idempotently)
invoice.underpaidReal USDT arrived but below the amount dueHold the order; request the remainder or settle per policy
invoice.overpaidSettled with a surplus over the amount dueFulfil and flag the excess for refund or credit
invoice.expiredThe quote window closed before sufficient paymentLet the customer start a fresh invoice at the current quote
06

Step 5 — Reconcile, and put the USDT to work

Once an invoice is paid, the settled USDT accrues to your halfin balance, tagged with the asset and the network it arrived on. That tagging is what makes month-end mechanical: every confirmed invoice maps back to the USD figure you billed and the exact rail it was paid on, so a TRC-20 receipt and an ERC-20 receipt for the same $49 plan reconcile against the same fiat anchor without you eyeballing rate screenshots. The same signed webhook stream you consume for invoices is the spine of this reconciliation — log the event id and verification result for every delivery and you can show finance exactly which event settled which order.

From the balance you have two onward moves. If you want to consolidate USDT that landed on several networks into one treasury position, balance conversion handles asset-to-asset and stablecoin rebalancing inside the platform — note that this is treasury movement between digital assets, not a fiat off-ramp; halfin does not cash out to a bank account. If you pay vendors, affiliates, or partners in USDT, payouts send it back out: a single payout for a one-off, or mass payouts when you fan out to many wallets, each line carrying its own idempotency_key so a retried run never double-pays. Collection and payout reconcile through the same event stream, which keeps the whole USDT flow auditable end to end.