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.

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://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": { "id": "6f1c2a4e-…", "external_order_id": "1043", "status": "paid", "amount_confirmed": "49.99", … }
}
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.

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

SDKs & plugins

Official Node.js and PHP libraries and a WooCommerce plugin are coming soon. Until then, the plain HTTP examples above work from any language.