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
- Pay button (no code)
- Authentication
- Create an invoice
- Retrieve / list / cancel
- The invoice object
- Webhooks
- Verifying signatures
- Fees
- Errors
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.
- Invoices expire after 60 minutes by default (
expires_in, 10–1440). A payment of the exact amount that arrives up to 12 hours after expiry is still matched; the invoice is markedlate: true. - Transfers that match no invoice (wrong amount, e.g. an exchange deducted its withdrawal fee) appear under Unmatched transfers in the dashboard, where you can attach them to an invoice.
- Use a receiving address dedicated to TRCGATEWAY.COM so unrelated income is never confused with an invoice.
- Network: TRON mainnet.
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 }
}'
| Field | Required | Description |
|---|---|---|
amount | yes | Price in USDT (string or number). |
order_id | no | Your reference, unique per account. Sending the same order_id again returns the existing invoice (safe retries) — status 200 instead of 201. |
description | no | Shown to the customer. |
customer_email | no | Stored with the invoice. |
expires_in | no | Minutes, 10–1440. Default 60. |
success_url | no | Where the checkout sends the customer after payment. |
metadata | no | Any JSON object up to 4 KB, returned in webhooks. |
fee_payer | no | merchant (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": "…" } }
| Status | Codes |
|---|---|
| 400 | invalid_request, amount_out_of_range |
| 401 | unauthenticated, invalid_api_key |
| 402 | fee_balance_low |
| 403 | merchant_suspended |
| 404 | not_found |
| 409 | no_payout_address, order_id_conflict, not_cancellable |
| 429 | rate_limited (300 requests/minute per account) |
| 503 | amount_slots_exhausted (extremely many open invoices for one amount — retry later) |