Skip to content
Crypto Merchant

Developers

Accept USDT TRC-20 Payments via the Crypto Merchant REST API

Tutorial: create a USDT.TRON invoice with the Crypto Merchant REST API, show the TRC-20 address, verify the invoice.paid webhook, and pay out.

By the Crypto Merchant team9 min read

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. 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. 2

    Turn on USDT on Tron

    Do this under your accepted coins. If it is off, the API answers 400 asset_not_accepted.

  3. 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. 4

    Add a webhook endpoint

    Also under Developers. Each endpoint gets its own whsec_ signing secret.

Confirm the coin ID exists on your account:

List your accepted coins
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.

Create the invoice
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):

The invoice, with the coin pre-selected
{
  "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: amount must be worth at least 0.50 US dollars (400 amount_too_small otherwise).
  • The amount to send: stablecoins count as one US dollar each, and pay_amount adds the gas fee your customer pays for moving this payment, shown as network_fee_amount and network_fee_usd. It is never more than $5. USDT on Tron costs more than most coins because Tron burns energy for token transfers. Always show pay_amount, not your price.
  • Your own IDs: metadata comes back unchanged in every webhook.
  • Letting the customer choose: leave coin out and the customer picks a network on the hosted page. The invoice then starts as open, with pay_address set to null.

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.

StatusWebhook eventWhat it meansWhat to do
openinvoice.createdCreated, no coin chosen yetNothing
awaiting_paymentNoneCoin chosen, address assigned, rate lockedShow the address and amount
confirminginvoice.confirmingTransfer seen on Tron, waiting for 19 confirmationsShow “confirming”; do not ship
paidinvoice.paidConfirmed; the amount minus 1% is in your balanceFulfil the order
underpaidinvoice.underpaidShort by more than your toleranceWait for a top-up or refund
expiredinvoice.expiredTime ran out, nothing seenOffer a new invoice
cancelledinvoice.cancelledYou cancelled it before any paymentNothing
refundedinvoice.refundedThe customer got back what was due to themClose 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:

Read one invoice
curl https://cryptomerchant.online/api/v1/invoices/$INVOICE_ID \
  -H "Authorization: Bearer $CM_SECRET_KEY"

The Tron facts behind the wait:

USDT on TronValue
Token standardTRC-20
Token contractTR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t
Coin ID in the APIUSDT.TRON
Decimals6
Confirmations Crypto Merchant waits for19
Typical waitAbout a minute
Coin the network charges fees inTRX

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.

webhook.js
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, not express.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 id and never move an order backwards from paid.

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 paid as 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 sends invoice.refunded.
  • Too much: the invoice is paid, overpaid_amount shows 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.

Read your balances
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.

Frequently asked questions

Can I skip the hosted page and show the TRC-20 address myself?
Yes. Pass "coin": "USDT.TRON" and pay_address and pay_amount are filled in at once. You then have to show the network, the time left (expires_at) and status changes yourself.
Do I need to hold TRX?
No. Moving USDT on Tron needs TRX for the network fee. Crypto Merchant pays it and charges a gas fee, from the gas balance or the coins being moved.
Why does invoice.paid take about a minute on Tron?
Crypto Merchant waits for 19 confirmations, which usually takes about a minute. invoice.confirming arrives first, as soon as the transfer is seen.
Can I test without real USDT?
Use Send test event under Developers to test your endpoint, and test the API's error paths with an amount under 0.50 US dollars or a coin you do not accept. A real end-to-end test needs one small real payment, which carries the usual fees.
Is there an SDK or a shop plugin?
No. The API is plain JSON over HTTPS with a Bearer key, so fetch or curl is enough. There is no WooCommerce, Shopify or WHMCS plugin yet.

Start accepting crypto today

Free setup, no documents to sign up, 1% per paid invoice, and payouts whenever you want.