TxRoutePayDocs

TxRoutePay API

Accept USDT on TRON (TRC20) and BNB Smart Chain (BEP20). Each invoice gets its own deposit address. When the customer pays, a forwarder contract sends the money straight to your payout wallet and takes the platform fee in the same transaction — TxRoutePay never holds your funds.

The API is plain JSON over HTTPS. Amounts are always decimal strings (for example "49.99") so no precision is lost.

Quickstart

  1. Create an account and add a payout wallet (Settings → Payout wallets).
  2. Create a test API key in Developers.
  3. Create an invoice and redirect the customer to checkout_url.
  4. Add a webhook endpoint and mark the order paid on invoice.paid.
const res = await fetch("https://txroutepay.com/v1/invoices", {
  method: "POST",
  headers: {
    authorization: `Bearer ${process.env.TXROUTE_KEY}`,
    "content-type": "application/json",
    "idempotency-key": "order-1043",
  },
  body: JSON.stringify({ external_order_id: "1043", amount: "49.99", currency: "USDT", network: "tron-mainnet" }),
});
const invoice = await res.json();
// redirect the customer to invoice.checkout_url

Authentication

Send your key in the Authorization header: Bearer pk_test_… or Bearer pk_live_…. Keys are shown once when created; we only store a hash.

Permissions (scopes)

invoices:writescope · optional
Create invoices.
invoices:readscope · optional
Retrieve and list invoices.
refunds:writescope · optional
Record refunds.

IP allowlist

Optionally restrict a key to your server IPs or CIDR ranges. Requests from other IPs get 401.

Keep keys on your server only. Never put them in a mobile app or browser code.

Test mode

Test keys only work with testnets: tron-nile and bsc-testnet. Live keys only work with tron-mainnet and bsc-mainnet. Test mode is available while your account is in review.

Set up your account (no code)

Everything a shop owner needs — no developer required.

  1. Create your account. Go to txroutepay.com/portal/signup and enter your business name, website (or Facebook page) and what you sell. Our team reviews it, usually within one business day. You can use test mode while you wait.
    Sign-up page
    Sign-up page
  2. Add your payout wallet. Open Settings → Payout wallets. Paste a TRON address (starts with T) for TRC20 and/or a BNB Smart Chain address (starts with 0x) for BEP20. Payments go straight to these addresses.
    Settings → Payout wallets: one address per network
    Settings → Payout wallets: one address per network
  3. Turn on two-factor authentication for your login. Changing a payout wallet later needs 2FA, email approval and a 24-hour wait — this protects your money if someone gets your password.
  4. Get paid with a link. Open Payments → New payment link, enter the amount and what it is for, then copy the link and send it on WhatsApp, Facebook or email. The customer picks TRC20 or BEP20, scans the QR code and pays.
    Payments → New payment link
    Payments → New payment link
    Your link is ready: copy it, share it on WhatsApp, or show the QR code
    Your link is ready: copy it, share it on WhatsApp, or show the QR code
  5. See the payment. The payment shows as Paid in your dashboard and the USDT is already in your wallet — there is nothing to withdraw. You also get an email.
    Home: volume, what reached your wallets, and setup progress
    Home: volume, what reached your wallets, and setup progress
Use a wallet you control, such as Trust Wallet. Double-check the address and the network: TRC20 and BEP20 addresses are different. If you use an exchange address, make sure the exchange accepts deposits sent by smart contracts.

What your customer sees

1. Choose TRC20 or BEP20
1. Choose TRC20 or BEP20
2. Scan the QR code or copy the address
2. Scan the QR code or copy the address
Each payment link is for one payment and expires (60 minutes by default, up to 24 hours). Create a new link for each order, or use a payment button for a link that never expires.

Payment buttons (no code)

A payment button is one permanent link for a product, a service, a donation or a custom amount. Every customer who opens it gets their own payment, so you can post it once and use it for months.

  1. Open Payment buttons in the dashboard and press Create payment button.
  2. Enter what it is for and the price. Leave the price empty to let the customer type the amount (donations, tips, custom invoices) — you can set a minimum.
  3. Press Share. Copy the link, share it on WhatsApp, print the QR code, or copy the ready-made HTML button into your website builder (WordPress, Wix, Shopify, Blogger …).
    Share: link, QR code and a copy-paste HTML button
    Share: link, QR code and a copy-paste HTML button
  4. When a customer pays, the payment shows in Payments like any other, marked “Payment button”, and the USDT is already in your wallet.
Payment buttons: how many times each one was paid
Payment buttons: how many times each one was paid

What your customer sees

Enter the amount (if you did not set a price) and an email for the receipt
Enter the amount (if you did not set a price) and an email for the receipt
Then choose the network and pay
Then choose the network and pay
Turn a button off any time (the link stops working); payments already made are not affected. Test buttons work on testnets only.

Accept payments on your website

For shops with their own website. The flow is the same in every language:

  1. The customer clicks Pay with USDT on your site.
  2. Your server creates an invoice with your secret API key and saves the invoice id with the order.
  3. Your server redirects the customer to the invoice checkout_url. They pay there.
  4. We send a signed webhook to your server. You check the signature, fetch the invoice from the API and mark the order paid.
Developers: create an API key and add your webhook URL
Developers: create an API key and add your webhook URL

Checklist before going live

  • Test everything first with a pk_test_ key on tron-nile / bsc-testnet, then switch to a pk_live_ key and tron-mainnet / bsc-mainnet.
  • Keep the API key and webhook secret on the server (environment variables). Never in HTML or JavaScript sent to the browser.
  • Always verify the X-Gateway-Signature header, then re-fetch the invoice — trust the API, not the browser redirect.
  • Treat paid, overpaid and paid_late as paid. Make "mark order paid" safe to run twice (webhooks can repeat).
  • Answer the webhook with 200 within 10 seconds; do slow work afterwards.

“Pay with USDT” button

Put this on your checkout or order page. Each button opens a page on your server (here /pay-usdt; on PHP hosting use /pay-usdt.php), which creates the invoice and redirects to our checkout — see the PHP, Node.js or Python example. Replace 1043 with the real order id.

<a class="txr-pay" href="/pay-usdt?order=1043&network=trc20">Pay with USDT <small>TRC20</small></a>
<a class="txr-pay" href="/pay-usdt?order=1043&network=bep20">Pay with USDT <small>BEP20</small></a>

<style>
  .txr-pay { display: inline-flex; align-items: center; gap: 8px; padding: 12px 20px; border-radius: 10px;
             background: #2f5bea; color: #fff; font: 600 15px system-ui, sans-serif; text-decoration: none; }
  .txr-pay:hover { background: #2449c9; }
  .txr-pay small { font-weight: 500; opacity: .8; }
</style>
Don’t want to touch code? Use a payment button from the dashboard — it gives you a ready HTML button that needs no server.

PHP example

Works on any PHP 7.4+ host with cURL (most shared hosting). Two files: one starts the payment, one receives the webhook.

pay-usdt.php

<?php
// pay-usdt.php — your server creates the invoice, then sends the customer to the checkout.
$apiKey  = getenv('TXROUTEPAY_KEY');            // pk_live_… (never put it in the page)
$orderId = $_GET['order'];                       // your own order id
$order   = load_order($orderId);                 // your code: read the order from your database
$network = ($_GET['network'] ?? 'trc20') === 'bep20' ? 'bsc-mainnet' : 'tron-mainnet';

$ch = curl_init('https://txroutepay.com/v1/invoices');
curl_setopt_array($ch, [
  CURLOPT_POST => true,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_HTTPHEADER => [
    "Authorization: Bearer $apiKey",
    'Content-Type: application/json',
    "Idempotency-Key: order-$orderId-$network",
  ],
  CURLOPT_POSTFIELDS => json_encode([
    'external_order_id' => "$orderId-$network",
    'amount'   => number_format($order['total'], 2, '.', ''),   // "49.99"
    'currency' => 'USDT',
    'network'  => $network,
    'metadata' => ['description' => "Order #$orderId", 'customer_email' => $order['email']],
  ]),
]);
$invoice = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) >= 300) { http_response_code(502); exit('Payment could not be started'); }

save_invoice_id($orderId, $invoice['id']);       // your code: remember which invoice belongs to the order
header('Location: ' . $invoice['checkout_url']);

txroutepay-webhook.php

<?php
// txroutepay-webhook.php — set this URL in Developers → Webhooks.
$secret = getenv('TXROUTEPAY_WEBHOOK_SECRET');   // whsec_…
$apiKey = getenv('TXROUTEPAY_KEY');
$raw    = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_GATEWAY_SIGNATURE'] ?? '';

// 1. Check the signature: t=<unix>,v1=<hex>[,v1=<hex>]
$t = null; $sigs = [];
foreach (explode(',', $header) as $part) {
  [$k, $v] = array_pad(explode('=', $part, 2), 2, '');
  if ($k === 't') $t = $v;
  if ($k === 'v1') $sigs[] = $v;
}
$expected = hash_hmac('sha256', "$t.$raw", $secret);
$ok = $t && abs(time() - (int)$t) <= 300;
$ok = $ok && count(array_filter($sigs, fn($s) => hash_equals($expected, $s))) > 0;
if (!$ok) { http_response_code(400); exit('bad signature'); }

// 2. Read the event, then fetch the invoice from the API (the source of truth)
$event = json_decode($raw, true);
if (in_array($event['type'], ['invoice.paid', 'invoice.overpaid', 'invoice.paid_late'], true)) {
  $ch = curl_init('https://txroutepay.com/v1/invoices/' . urlencode($event['data']['invoice_id']));
  curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ["Authorization: Bearer $apiKey"]]);
  $inv = json_decode(curl_exec($ch), true);

  if (in_array($inv['status'], ['paid', 'overpaid', 'paid_late'], true)) {
    mark_order_paid($inv['external_order_id'], $inv['amount_confirmed']);  // your code; make it safe to run twice
  }
}
http_response_code(200);                         // answer within 10 seconds

Node.js example

Node 18+ with Express. The webhook route uses express.raw so the signature is checked on the exact bytes we sent.

// npm install express
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const API = "https://txroutepay.com/v1";
const KEY = process.env.TXROUTEPAY_KEY;                 // pk_live_…
const SECRET = process.env.TXROUTEPAY_WEBHOOK_SECRET;   // whsec_…
const PAID = ["paid", "overpaid", "paid_late"];
const app = express();

// 1. "Pay with USDT" → create the invoice on your server, redirect to the checkout
app.get("/pay-usdt", async (req, res) => {
  const order = await loadOrder(req.query.order);                     // your code
  const network = req.query.network === "bep20" ? "bsc-mainnet" : "tron-mainnet";
  const r = await fetch(`${API}/invoices`, {
    method: "POST",
    headers: { authorization: `Bearer ${KEY}`, "content-type": "application/json",
               "idempotency-key": `order-${order.id}-${network}` },
    body: JSON.stringify({ external_order_id: `${order.id}-${network}`, amount: order.total.toFixed(2),
                           currency: "USDT", network, metadata: { description: `Order #${order.id}` } }),
  });
  if (!r.ok) return res.status(502).send("Payment could not be started");
  const invoice = await r.json();
  await saveInvoiceId(order.id, invoice.id);                          // your code
  res.redirect(invoice.checkout_url);
});

// 2. Webhook: keep the raw body for the signature check
export function verify(raw, header, secret) {
  const parts = String(header).split(",").map((p) => p.split("="));
  const t = parts.find(([k]) => k === "t")?.[1];
  if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${raw}`).digest("hex");
  return parts.some(([k, v]) => k === "v1" && v?.length === expected.length
    && timingSafeEqual(Buffer.from(v), Buffer.from(expected)));
}

app.post("/webhooks/txroutepay", express.raw({ type: "application/json" }), async (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verify(raw, req.get("x-gateway-signature"), SECRET)) return res.status(400).send("bad signature");
  const event = JSON.parse(raw);
  if (PAID.includes(event.type.replace("invoice.", ""))) {
    const inv = await (await fetch(`${API}/invoices/${event.data.invoice_id}`,
      { headers: { authorization: `Bearer ${KEY}` } })).json();     // the API is the source of truth
    if (PAID.includes(inv.status)) await markOrderPaid(inv.external_order_id, inv.amount_confirmed); // your code
  }
  res.sendStatus(200);
});

app.listen(3000);

Python example

Python 3.9+ with Flask and requests. The same logic works in Django or FastAPI.

# pip install flask requests
import hashlib, hmac, os, time
import requests
from flask import Flask, abort, redirect, request

API = "https://txroutepay.com/v1"
KEY = os.environ["TXROUTEPAY_KEY"]                    # pk_live_…
SECRET = os.environ["TXROUTEPAY_WEBHOOK_SECRET"]      # whsec_…
PAID = {"paid", "overpaid", "paid_late"}
app = Flask(__name__)

@app.get("/pay-usdt")
def pay_usdt():
    order = load_order(request.args["order"])                       # your code
    network = "bsc-mainnet" if request.args.get("network") == "bep20" else "tron-mainnet"
    r = requests.post(f"{API}/invoices", timeout=20,
        headers={"Authorization": f"Bearer {KEY}", "Idempotency-Key": f"order-{order['id']}-{network}"},
        json={"external_order_id": f"{order['id']}-{network}", "amount": f"{order['total']:.2f}",
              "currency": "USDT", "network": network, "metadata": {"description": f"Order #{order['id']}"}})
    if r.status_code >= 300:
        abort(502)
    invoice = r.json()
    save_invoice_id(order["id"], invoice["id"])                     # your code
    return redirect(invoice["checkout_url"])

def verify(raw: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = [p.split("=", 1) for p in (header or "").split(",") if "=" in p]
    t = next((v for k, v in parts if k == "t"), None)
    if not t or abs(time.time() - int(t)) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
    return any(k == "v1" and hmac.compare_digest(v, expected) for k, v in parts)

@app.post("/webhooks/txroutepay")
def webhook():
    raw = request.get_data()
    if not verify(raw, request.headers.get("X-Gateway-Signature", ""), SECRET):
        abort(400)
    event = request.get_json()
    if event["type"].removeprefix("invoice.") in PAID:
        inv = requests.get(f"{API}/invoices/{event['data']['invoice_id']}", timeout=20,
                           headers={"Authorization": f"Bearer {KEY}"}).json()   # source of truth
        if inv["status"] in PAID:
            mark_order_paid(inv["external_order_id"], inv["amount_confirmed"])  # your code
    return "", 200

Hosted checkout

Every invoice has a checkout_url. The page shows the amount, a QR code, the address with copy buttons, a countdown and live status, with your logo and brand color (Settings → Checkout & branding). Set a return URL to bring the customer back to your shop after payment.

Payment statuses

awaiting_payment
Waiting for the customer to send.
confirming
Payment seen, waiting for network confirmations.
paid
Full amount confirmed.
underpaid
Less than asked was confirmed.
overpaid
More than asked was confirmed (counts as paid).
paid_late
Paid after the deadline (counts as paid).
expired
No payment arrived in time.
Never trust the browser redirect alone. Mark an order paid only after the invoice.paid webhook, or after fetching the invoice from your server.

Create an invoice

POST/v1/invoices

Needs scope invoices:write. Returns 201 with the invoice (or 200 with the existing one when the same Idempotency-Key is reused).

Body parameters

external_order_idstring · required
Your order id (max 128 characters). Unique per account.
amountstring · required
Decimal string in USDT, e.g. "49.99".
currencystring · required
Always "USDT" for now.
networkstring · required
tron-mainnet, bsc-mainnet (live) or tron-nile, bsc-testnet (test).
expires_in_minutesinteger · optional
5 to 1440. Default 60.
metadataobject · optional
Your own data, returned in webhooks. description is shown on the checkout; customer_email lets us send a receipt.

Headers

Idempotency-Keyheader · optional
Any unique string (for example your order id). Retries with the same key return the same invoice instead of a duplicate.
curl https://txroutepay.com/v1/invoices \
  -H "Authorization: Bearer pk_test_…" \
  -H "Idempotency-Key: order-1043" \
  -H "Content-Type: application/json" \
  -d '{
    "external_order_id": "1043",
    "amount": "49.99",
    "currency": "USDT",
    "network": "tron-nile",
    "expires_in_minutes": 60,
    "metadata": { "description": "Jamdani saree", "customer_email": "rina@example.com" }
  }'

Response · 201

{
  "id": "6f1c2a4e-…",
  "external_order_id": "1043",
  "status": "awaiting_payment",
  "network": "tron-nile",
  "currency": "USDT",
  "address": "TQm1…7xLp",
  "amount": "49.99",
  "amount_detected": "0",
  "amount_confirmed": "0",
  "fee_bps": 100,
  "checkout_url": "https://pay.txroutepay.com/pay/6f1c2a4e-…",
  "expires_at": "2026-10-17T11:00:00.000Z",
  "paid_at": null,
  "created_at": "2026-10-17T10:00:00.000Z",
  "metadata": { "description": "Jamdani saree" }
}

Retrieve an invoice

GET/v1/invoices/:id

Needs invoices:read. Returns the invoice object above with its current status.

List invoices

GET/v1/invoices?status=paid&limit=25
statusquery · optional
Filter by status.
networkquery · optional
Filter by network id.
limitquery · optional
1–100, default 25.
beforequery · optional
Pass next_cursor from the previous page.

Returns { "data": [ … ], "next_cursor": "…" | null }, newest first.

Record a refund

POST/v1/invoices/:id/refunds

Needs refunds:write. Crypto payments cannot be pulled back, so you send the refund from your own wallet, then record it here so reports and receipts stay correct. Only paid invoices can be refunded, and the total can’t exceed the amount received.

amountstring · required
Decimal string.
reasonstring · required
2–100 characters.
tx_hashstring · optional
Hash of your refund transaction.
notestring · optional
Internal note.
email_customerstring · optional
Send the customer a refund receipt.
curl https://txroutepay.com/v1/invoices/6f1c2a4e-…/refunds \
  -H "Authorization: Bearer pk_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "amount": "10.00", "reason": "Item out of stock", "tx_hash": "0x…", "email_customer": "rina@example.com" }'

Errors

Errors return JSON: { "error": "code", "message": "…" }.

400 invalid_request
The body failed validation. details lists each problem.
400 invalid_amount
Amount is zero, negative, or has too many decimals.
400 unsupported_network
That network is not enabled.
400 wrong_mode
A test key used a live network, or a live key used a testnet.
400 wallet_not_configured
Add a payout wallet for this network in the dashboard first.
401 unauthorized
API key missing, revoked, or the request IP is not on the key’s allowlist.
403 forbidden
Live keys work after your account is approved.
403 missing_permission
The key does not have the scope this endpoint needs.
404 not_found
No invoice with that id for your account.
409 duplicate_order
An invoice with this external_order_id already exists (use an Idempotency-Key to retry safely).
503 network_not_ready
The network is temporarily not accepting new invoices. Retry shortly.

Webhook events

Add endpoints in Developers → Webhooks. We POST JSON to your URL for each event:

{
  "id": "evt_…",
  "type": "invoice.paid",
  "created_at": "2026-10-17T10:03:12.000Z",
  "data": {
    "invoice_id": "6f1c2a4e-…",
    "status": "paid",
    "amount_expected_base_units": "49990000",
    "amount_confirmed_base_units": "49990000",
    "tx_hash": "a91e…"
  }
}
invoice.created
A new invoice was created (API or payment link).
invoice.payment_detected
A transfer to the deposit address was seen. Not final yet — do not ship.
invoice.paid
The full amount is confirmed. Mark the order paid on this event.
invoice.underpaid
Less than the amount was received. The customer can send the rest.
invoice.overpaid
More than the amount was received. The order is paid; consider refunding the extra.
invoice.paid_late
Payment arrived after the invoice expired. Decide whether to accept it.
invoice.expired
No payment before the deadline.
invoice.payment_reorged
A detected transfer disappeared in a chain reorganisation. Rare; the status is recalculated.
test.ping
Sent when you press “Send test event” in the dashboard.

Amounts in webhooks are in the token’s smallest unit (USDT has 6 decimals on TRON and 18 on BSC). The easiest way: call Retrieve an invoice with data.invoice_id and use its decimal amount_confirmed and external_order_id.

Use the X-Gateway-Event-Id header (same as id) to ignore duplicates.

Verify signatures

Every request has X-Gateway-Signature: t=<unix time>,v1=<hex>. v1 is HMAC-SHA256 of t + "." + raw body with your endpoint secret (whsec_…). For 24 hours after you rotate a secret, a second v1 signed with the old secret is included — accept the request if any v1 matches.

import { createHmac, timingSafeEqual } from "node:crypto";

// Header "X-Gateway-Signature: t=<unix>,v1=<hex>[,v1=<hex>]"
export function verify(rawBody, header, secret) {
  const parts = header.split(",").map((p) => p.split("="));
  const t = parts.find(([k]) => k === "t")?.[1];
  if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false; // 5 min replay window
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return parts.some(([k, v]) => k === "v1" && v.length === expected.length
    && timingSafeEqual(Buffer.from(v), Buffer.from(expected)));
}

Retries

Answer with any 2xx within 10 seconds. Otherwise we retry after 10 s, 30 s, 2 min, 10 min, 30 min, 1 h, 3 h, 6 h, 12 h and 24 h, then give up and show the delivery as failed in your dashboard (you can resend it there).

WooCommerce plugin

For WordPress shops. Customers pick USDT at checkout, pay on our page, and the order is marked paid automatically. Works with the classic checkout and the new Checkout block.

Download the plugin (.zip)

  1. In WordPress open Plugins → Add New → Upload Plugin, choose the zip and press Activate.
  2. Open WooCommerce → Settings → Payments → TxRoutePay (USDT).
  3. In the TxRoutePay dashboard open Developers, create an API key and paste it into the plugin (pk_test_… to try it, pk_live_… for real money).
  4. Copy the Webhook URL shown in the plugin settings. In Developers → Webhooks add that URL, then paste the signing secret (whsec_…) back into the plugin.
  5. Choose TRC20, BEP20 or both, enable the gateway and save. Place a test order to check everything.
Your store currency must be US Dollar (USD): the order total is charged 1:1 in USDT. Requires WordPress 6.0+, WooCommerce 8+ and PHP 7.4+.

What happens to orders

Pending payment
Customer was sent to the payment page.
Processing
Paid in full (also overpaid or paid late). The transaction hash is saved on the order.
On hold
Less than the total arrived. The customer can send the rest; you decide.
Cancelled
The payment expired unpaid. A late payment still completes the order.

Other platforms (Shopify, Laravel, custom PHP …) can use the PHP, Node.js or Python examples, or a payment button.