TRCGATEWAY.COM API

Accept USDT on TRON (TRC20). Customers pay your own wallet address directly; TRCGATEWAY.COM creates the invoice, recognises the payment on chain and notifies you.

How payments are matched

A TRC20 transfer carries no memo, so every open invoice on your address gets a unique amount: your price plus a small offset (for example 25.00 → 25.01 when another 25.00 invoice is still open). The checkout page shows the customer the exact amount to send. When a confirmed transfer of exactly that amount reaches your address inside the invoice window, the invoice becomes paid — usually about a minute after the customer sends.

Pay button (no code)

Create a button in Dashboard → Pay buttons and paste its two lines anywhere in your HTML:

<script src="https://trcgateway.com/embed.js" async></script>
<a href="https://trcgateway.com/b/btn_XXXX" data-usdtpay-button="btn_XXXX">Pay with USDT</a>

Clicking opens the checkout in an overlay. Without JavaScript the link opens the hosted page. Listen for the result in the page:

document.addEventListener('usdtpay:paid', (e) => {
  console.log('paid invoice', e.detail.invoice, 'tx', e.detail.tx_id);
});

Add data-success-url="https://…" to the link to redirect after payment. You can also open a checkout you created through the API: <a data-usdtpay-invoice="inv_…">Pay</a> or USDTPay.open('inv_…').

Never trust the browser event alone to deliver goods — confirm with a webhook or by retrieving the invoice from your server.

Authentication

Create a secret key in Dashboard → API & webhooks and send it as a bearer token. Keys are shown once; keep them on your server.

Authorization: Bearer sk_...

Base URL: https://trcgateway.com/v1. Requests and responses are JSON. Amounts are decimal strings with up to 6 decimals (USDT has 6).

Create an invoice

POST /v1/invoices

curl https://trcgateway.com/v1/invoices \
  -H "Authorization: Bearer sk_..." \
  -H "Content-Type: application/json" \
  -d '{
    "amount": "25.00",
    "order_id": "ORDER-1001",
    "description": "Pro plan, 1 month",
    "success_url": "https://yoursite.com/thanks",
    "metadata": { "user_id": 42 }
  }'
FieldRequiredDescription
amountyesPrice in USDT (string or number).
order_idnoYour reference, unique per account. Sending the same order_id again returns the existing invoice (safe retries) — status 200 instead of 201.
descriptionnoShown to the customer.
customer_emailnoStored with the invoice.
expires_innoMinutes, 10–1440. Default 60.
success_urlnoWhere the checkout sends the customer after payment.
metadatanoAny JSON object up to 4 KB, returned in webhooks.
fee_payernomerchant (default) or customer — the latter adds the fee to the amount the customer sends.

Redirect the customer to checkout_url from the response, or show pay_amount and address in your own UI.

Retrieve, list, cancel

GET  /v1/invoices/{id}
GET  /v1/invoices?status=paid&limit=20&starting_after=inv_…
POST /v1/invoices/{id}/cancel
GET  /v1/balance
GET  /v1/transfers?status=unmatched

The invoice object

{
  "id": "inv_8Zc…",
  "object": "invoice",
  "status": "pending",            // pending | paid | expired | cancelled
  "currency": "USDT",
  "network": "TRON",
  "amount": "25.00",              // your price
  "fee": "0.125",
  "fee_payer": "merchant",
  "pay_amount": "25.01",          // what the customer must send, exactly
  "pay_amount_units": 25010000,
  "amount_received": "0.00",
  "address": "T…",                // your receiving address
  "order_id": "ORDER-1001",
  "metadata": { "user_id": 42 },
  "checkout_url": "https://trcgateway.com/pay/inv_8Zc…",
  "tx_id": null,
  "late": false,                  // paid after expiry (inside the grace window)
  "manual": false,                // you attached the transfer by hand
  "expires_at": "…", "paid_at": null, "created_at": "…"
}

When a transfer is attached manually, amount_received may differ from pay_amount.

Webhooks

Set your endpoint in the dashboard. We send a POST with a JSON body:

{
  "id": "evt_…",
  "type": "invoice.paid",          // invoice.paid | invoice.expired | ping
  "created": 1767225600,
  "data": { …invoice object… }
}

Respond with any 2xx within 10 seconds. Other responses are retried after 1 min, 5 min, 15 min, 1 h, 3 h, 6 h, 12 h and 24 h. Events can arrive more than once — use id (also in the USDTPay-Event-Id header) to deduplicate, and treat invoice.paid as idempotent.

Verifying signatures

Every request carries USDTPay-Signature: t=<unix time>,v1=<hex> where v1 = HMAC-SHA256(secret, t + "." + raw_body). Compute it over the raw request body, compare in constant time, and reject timestamps older than 5 minutes.

Node.js (Express):

const crypto = require('crypto');
app.post('/usdtpay/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('USDTPay-Signature') || '';
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
  const expected = crypto.createHmac('sha256', process.env.USDTPAY_WEBHOOK_SECRET)
    .update(parts.t + '.' + req.body).digest('hex');
  const ok = parts.v1 && expected.length === parts.v1.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1)) &&
    Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  if (!ok) return res.status(400).end();
  const event = JSON.parse(req.body);
  if (event.type === 'invoice.paid') { /* deliver order event.data.order_id */ }
  res.sendStatus(200);
});

PHP:

$body = file_get_contents('php://input');
parse_str(str_replace(',', '&', $_SERVER['HTTP_USDTPAY_SIGNATURE'] ?? ''), $p);
$expected = hash_hmac('sha256', $p['t'] . '.' . $body, getenv('USDTPAY_WEBHOOK_SECRET'));
if (!hash_equals($expected, $p['v1'] ?? '') || abs(time() - (int)$p['t']) > 300) {
  http_response_code(400); exit;
}
$event = json_decode($body, true);
if ($event['type'] === 'invoice.paid') { /* deliver $event['data']['order_id'] */ }
http_response_code(200);

Fees

0.5% of each paid invoice, minimum 0.10 USDT, charged to your prepaid fee balance (we never touch the payment itself). Top up in Billing. When the balance is used up, new invoices are refused with 402 fee_balance_low until you top up. You can make customers pay the fee with fee_payer: "customer".

Errors

{ "error": { "code": "amount_out_of_range", "message": "…" } }
StatusCodes
400invalid_request, amount_out_of_range
401unauthenticated, invalid_api_key
402fee_balance_low
403merchant_suspended
404not_found
409no_payout_address, order_id_conflict, not_cancellable
429rate_limited (300 requests/minute per account)
503amount_slots_exhausted (extremely many open invoices for one amount — retry later)