To accept USDT on Tron (TRC-20) through the Crypto Merchant REST API, create an invoice with POST /invoices and "coin": "USDT.TRON", show the returned pay_address and pay_amount to the customer (or send them to checkout_url), and fulfil the order when the signed invoice.paid webhook arrives. This tutorial walks through each call. Last checked: 5 November 2026.
What do I need before the first call?
You need a live account with USDT on Tron turned on, a secret API key and a webhook endpoint. All three are set up in the portal.
- 1
Create an account
Sign up with no identity documents; the Terms of Service still apply. The account is live as soon as setup is finished, with no payment to go live.
- 2
Turn on USDT on Tron
Do this under your accepted coins. If it is off, the API answers
400 asset_not_accepted. - 3
Create a secret key
Under Developers. It starts with
cm_live_and is shown once. Keep it on your server, never in a web page or app. - 4
Add a webhook endpoint
Also under Developers. Each endpoint gets its own
whsec_signing secret.
Confirm the coin ID exists on your account:
curl https://cryptomerchant.online/api/v1/coins \
-H "Authorization: Bearer $CM_SECRET_KEY"Look for "id": "USDT.TRON" with "decimals": 6. That ID is the only Tron-specific value you pass to the API.
How do I create a USDT TRC-20 invoice?
Call POST /invoices with the price in your own currency and "coin": "USDT.TRON". Because the coin is pre-selected, the deposit address is assigned and the rate locked at once, so the response already contains the address and the exact USDT amount.
curl https://cryptomerchant.online/api/v1/invoices \
-H "Authorization: Bearer $CM_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{
"amount": "49.90",
"currency": "USD",
"coin": "USDT.TRON",
"order_id": "1042",
"description": "Annual plan",
"customer_email": "[email protected]",
"redirect_url": "https://yourstore.example/orders/1042",
"metadata": { "plan": "annual" }
}'The API returns 201 and the invoice object. These are the fields that matter (shortened; the values marked … depend on the invoice):
{
"id": "inv_…",
"object": "invoice",
"status": "awaiting_payment",
"amount": "49.9",
"currency": "USD",
"amount_usd": "49.9",
"order_id": "1042",
"coin": { "id": "USDT.TRON", "symbol": "USDT", "network": "Tron" },
"pay_address": "T…",
"pay_amount": "…",
"network_fee_amount": "…",
"network_fee_usd": "…",
"rate_usd": "1",
"received_amount": "0",
"checkout_url": "https://cryptomerchant.online/pay/i/inv_…",
"expires_at": "2026-11-04T12:30:00.000Z",
"paid_at": null
}Save id with your order. Then either redirect the customer to checkout_url, where the hosted page shows the address, a QR code, the countdown and live status, or show pay_address and pay_amount in your own interface. If you build your own page, show three things clearly: the exact amount, the address, and the words "USDT on Tron (TRC-20)". USDT sent on a network Crypto Merchant doesn't support, or to another address, may be lost for good.
- The minimum:
amountmust be worth at least 0.50 US dollars (400 amount_too_smallotherwise). - The amount to send: stablecoins count as one US dollar each, and
pay_amountadds the gas fee your customer pays for moving this payment, shown asnetwork_fee_amountandnetwork_fee_usd. It is never more than $5. USDT on Tron costs more than most coins because Tron burns energy for token transfers. Always showpay_amount, not your price. - Your own IDs:
metadatacomes back unchanged in every webhook. - Letting the customer choose: leave
coinout and the customer picks a network on the hosted page. The invoice then starts asopen, withpay_addressset tonull.
How do I know the payment arrived?
Your webhook endpoint receives invoice.confirming when the transfer is first seen on Tron, and invoice.paid after 19 confirmations. Fulfil orders on invoice.paid only.
| Status | Webhook event | What it means | What to do |
|---|---|---|---|
open | invoice.created | Created, no coin chosen yet | Nothing |
awaiting_payment | None | Coin chosen, address assigned, rate locked | Show the address and amount |
confirming | invoice.confirming | Transfer seen on Tron, waiting for 19 confirmations | Show “confirming”; do not ship |
paid | invoice.paid | Confirmed; the amount minus 1% is in your balance | Fulfil the order |
underpaid | invoice.underpaid | Short by more than your tolerance | Wait for a top-up or refund |
expired | invoice.expired | Time ran out, nothing seen | Offer a new invoice |
cancelled | invoice.cancelled | You cancelled it before any payment | Nothing |
refunded | invoice.refunded | The customer got back what was due to them | Close the order |
Every event is an HTTP POST with a JSON body of id, type, created and data, where data is the full invoice object. You can also poll, which is a good double-check before shipping something valuable:
curl https://cryptomerchant.online/api/v1/invoices/$INVOICE_ID \
-H "Authorization: Bearer $CM_SECRET_KEY"The Tron facts behind the wait:
| USDT on Tron | Value |
|---|---|
| Token standard | TRC-20 |
| Token contract | TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t |
| Coin ID in the API | USDT.TRON |
| Decimals | 6 |
| Confirmations Crypto Merchant waits for | 19 |
| Typical wait | About a minute |
| Coin the network charges fees in | TRX |
What a customer's wallet charges to send USDT on Tron changes with the network; today's figure is on crypto network fees today. A TRC-20 transfer to an address that has never held USDT uses about twice the energy, because the token contract has to create its balance. A business's first invoices get brand-new deposit addresses, so a customer paying one of those may see their wallet quote the higher fee.
How do I verify the webhook signature?
Compute HMAC-SHA256 over the timestamp, a full stop and the raw request body, with your whsec_ secret as the key, and compare it to v1 in constant time. Reject anything older than five minutes.
The handler below does the three things the webhooks docs ask for: it checks the signature, saves the event, and replies 200 before doing any work. It needs Node.js 18 or later and Express.
import crypto from "node:crypto";
import express from "express";
import { db } from "./db.js"; // your database client
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
String(header ?? "").split(",").map((p) => p.trim().split("=")),
);
if (!parts.t || !parts.v1) throw new Error("missing signature");
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
throw new Error("bad signature");
}
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) {
throw new Error("timestamp too old");
}
return JSON.parse(rawBody);
}
const app = express();
app.post(
"/webhooks/crypto-merchant",
express.raw({ type: "application/json" }), // keep the raw body
async (req, res) => {
let event;
try {
event = verify(req.body.toString("utf8"), req.get("CM-Signature"), process.env.CM_WEBHOOK_SECRET);
} catch {
return res.sendStatus(400);
}
try {
// 1. Save first. A repeat delivery has the same id and is ignored.
await db.query(
"INSERT INTO webhook_events (id, type, payload) VALUES ($1, $2, $3) ON CONFLICT (id) DO NOTHING",
[event.id, event.type, event],
);
} catch {
return res.sendStatus(500); // not saved: Crypto Merchant retries
}
// 2. Reply, then work.
res.sendStatus(200);
handle(event).catch(console.error);
},
);
async function handle(event) {
const inv = event.data;
if (event.type === "invoice.paid" && inv.coin?.id === "USDT.TRON") {
// Mark order inv.order_id as paid. Running this twice must change nothing.
}
}
app.listen(3000);- Keep the raw body. Use
express.raw, notexpress.json: re-serialised JSON does not match the signature. - Reply within 10 seconds. Any other outcome is retried after 1 minute, then 5 and 15 minutes, then 1, 3, 6, 12 and 24 hours, about two days in all.
- Expect repeats. Events can arrive twice or out of order, so key your table on the event
idand never move an order backwards frompaid.
What happens if the customer sends the wrong amount?
Small underpayments within your tolerance count as paid. Larger ones make the invoice underpaid. Overpayments are held for the customer.
- Short by no more than the tolerance you set in your settings (at least $1 is recommended): the invoice becomes
paidas normal. - Short by more: you get
invoice.underpaid. The customer can send the rest to the same address, or ask for a refund of what they sent to an address they enter. The network fee for the refund comes out of the refund. A refund sendsinvoice.refunded. - Too much: the invoice is
paid,overpaid_amountshows the extra in USDT, and the extra is held for the customer, who withdraws it from the invoice page. It is not added to your balance.
For a return after a paid invoice, an Admin creates a private refund link in the portal. There is no refund call in the API. See how to refund a crypto payment.
What does it cost?
1% of each paid invoice, deducted in USDT when the payment is credited. There is no setup fee, no monthly fee and no minimum; expired and cancelled invoices cost nothing.
Moving coins (sweeps, payouts and swaps) carries a gas fee, from the gas balance or the coins being moved. Tron charges that movement in TRX; Crypto Merchant pays the TRX, so you never have to hold it. Swaps into a holding coin use live market rates and include a 1% service margin. Every cost is on Pricing, and the total fees on a $20 USDT-Tron payment works through a small invoice step by step.
How do I get the USDT out?
Check your balance with the API, then pay out from the portal whenever you want. There is no minimum.
curl https://cryptomerchant.online/api/v1/balances \
-H "Authorization: Bearer $CM_SECRET_KEY"The response has one row per coin, for example { "coin": "USDT.TRON", "symbol": "USDT", "network": "Tron", "amount": "…", "usd": "…" }, plus total_usd and gas_balance_usd.
Payouts are made in the portal by an Admin, not through the API. They go per coin, so USDT on Tron goes as it is to the TRC-20 address you saved (check every character; transfers cannot be reversed), or swapped into one holding coin, automatically every 24 hours or when you request a payout. Your webhook gets payout.sent or payout.failed, and swap.completed after a swap. Payouts are crypto only; there is no bank settlement. See payouts and swaps.
More detail: accept USDT on Tron (TRC-20), the API reference, wallets, sweeps and gas and the general crypto payment API guide.