easyway
Create account

Accept USDT TRC-20 payments from your own code

This guide adds "Pay with USDT (TRC-20)" to a shop or app: your server creates an invoice, the customer pays on the hosted payment page, and a webhook tells your server when the money is final. You need an API key and a webhook URL, nothing on TRON itself. The examples use Node.js, PHP and Python; any language that can send HTTPS requests works.

Before you start

WhatWhere
A test API key with the invoices:write scopeDashboard → Developers. Test keys start with sk_test_; keep the key on your server only.
A test webhook endpointDashboard → Developers → Webhooks, in test mode. Copy its signing secret.
The key in EASYWAY_API_KEYEvery example below reads it from the environment.

1. Create an invoice

Create one invoice per order. currency: "USD" with allowed_assets: ["USDT_TRC20"] offers only USDT on TRON; stablecoins are priced at exactly 1 USD, so 149 USD is quoted as 149 USDT. Use your order number as the Idempotency-Key: if the request times out and you send it again, you get the same invoice back instead of a second one.

Node.js (18+, built-in fetch)

// 149 USD, payable only in USDT on TRON
const res = await fetch("https://api.easyway.cash/v1/invoices", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.EASYWAY_API_KEY}`,
    "Idempotency-Key": "order-2048",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    "amount": "149.00",
    "currency": "USD",
    "allowed_assets": [
      "USDT_TRC20"
    ],
    "order_id": "2048",
    "success_url": "https://shop.example.com/thanks",
    "cancel_url": "https://shop.example.com/cart"
  }),
});
if (!res.ok) throw new Error(`easyway ${res.status}: ${(await res.json()).error}`);
const data = await res.json();

PHP (cURL)

// 149 USD, payable only in USDT on TRON
$ch = curl_init("https://api.easyway.cash/v1/invoices");
curl_setopt_array($ch, [
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_POST => true,
  CURLOPT_POSTFIELDS => json_encode([
    "amount" => "149.00",
    "currency" => "USD",
    "allowed_assets" => ["USDT_TRC20"],
    "order_id" => "2048",
    "success_url" => "https://shop.example.com/thanks",
    "cancel_url" => "https://shop.example.com/cart",
  ]),
  CURLOPT_HTTPHEADER => [
    "Authorization: Bearer " . getenv("EASYWAY_API_KEY"),
    "Idempotency-Key: order-2048",
    "Content-Type: application/json",
  ],
]);
$data = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) {
  throw new Exception("easyway: " . $data["error"]);
}

Python (requests)

# 149 USD, payable only in USDT on TRON
import os, requests

res = requests.post(
    "https://api.easyway.cash/v1/invoices",
    headers={
        "Authorization": f"Bearer {os.environ['EASYWAY_API_KEY']}",
        "Idempotency-Key": "order-2048",
    },
    json={
        "amount": "149.00",
        "currency": "USD",
        "allowed_assets": ["USDT_TRC20"],
        "order_id": "2048",
        "success_url": "https://shop.example.com/thanks",
        "cancel_url": "https://shop.example.com/cart",
    },
)
res.raise_for_status()
data = res.json()

The response is the invoice with status pending, an id (inv_…) and a payment_url. Save the id next to your order. It expires after your account's invoice lifetime (60 minutes unless you changed it, or set lifetime per invoice). Every field is described in the invoices section of the docs.

2. Send the customer to the payment page

Redirect to payment_url (an HTTP 303 after the checkout form, or a link). The page shows a TRON address that belongs to this invoice alone, the exact amount and a QR code, and updates itself while the payment confirms. After paying, the customer gets a button back to your success_url. That visit is not proof of payment: anyone can open the URL.

Tell customers that a TRC-20 transfer needs a little TRX in their own wallet for bandwidth or energy; that is TRON's charge to the sender, not ours. More about what the payer sees on the USDT TRC-20 page.

3. Fulfil the order on the webhook

easyway POSTs signed events to your webhook URL. First check the signature over the raw body: the webhook signatures guide has handlers for Express, Next.js, PHP and Flask. Then act on the event:

EventWhenWhat to do
invoice.confirmingThe transfer is on-chain but not final yet.Optional: show "payment detected".
invoice.paidAfter 19 confirmations, about a minute on TRON.Fulfil the order. This is the only event that means money.
invoice.underpaidLess than the amount arrived. remaining_amount says how much is missing.Wait for a top-up on the same page, fulfil partly, or refund. Your call.
invoice.expiredNobody paid in time.Release the stock. A payment that arrives later still flips the invoice to paid, so keep handling it.

The logic after the signature check, in Node.js (the same steps apply in any language):

// Runs after the signature check (see the webhook signatures guide).
async function handleEvent(event) {
  if (await db.seenEvent(event.id)) return;          // retries repeat the same event id
  const inv = event.data.invoice;
  if (!inv) return;                                   // payout.* and webhook.test carry no invoice
  const order = await db.orderById(inv.order_id);
  // Only the invoice you created (and saved) for this order may change it.
  if (!order || order.invoiceId !== inv.id) return;

  if (event.type === "invoice.confirming") await db.setOrderState(order.id, "payment_detected");
  if (event.type === "invoice.paid") await db.setOrderState(order.id, "paid"); // ship it, unlock it, top it up
  if (event.type === "invoice.underpaid") await db.setOrderState(order.id, "underpaid", inv.remaining_amount);
  await db.rememberEvent(event.id);
}

Answer 2xx within 10 seconds and do slow work afterwards. Failed deliveries are retried for about 45 hours, and events can arrive out of order, so decide from data.invoice.status or fetch the invoice with GET /v1/invoices/:id when in doubt.

4. Test it in sandbox mode

With a sk_test_ key, invoices look the same but never touch the blockchain. Switch the dashboard to test mode, open the invoice and press Simulate payment: full, to get invoice.paid, or partial, to get invoice.underpaid. It runs the real crediting code and sends your real webhooks. The dashboard lists every delivery with its status code, so you can see what your handler answered.

5. Go live

StepDetail
Live keyCreate a sk_live_ key with only invoices:write (and invoices:read if you poll). Add an IP allowlist if your servers have fixed IPs.
Live webhookA separate HTTPS endpoint with its own secret. Test and live secrets differ.
FeesA flat 0.5% per payment, taken in USDT: on 149 USDT the fee is 0.745 and 148.255 is credited. No monthly fee.
Getting the money outPayments are credited to your easyway balance, which easyway holds until you withdraw to a wallet on your whitelist. A USDT TRC-20 withdrawal costs a 1 USDT network fee; the minimum is 0.5 USDT. See pricing.

Want other coins later? Drop allowed_assets and the customer can choose any enabled coin and network on the same page, with no other code change. The list is in the API docs and at GET /v1/assets.

Create a merchant accountRead the API docs
Accept USDT TRC-20 payments: Node.js, PHP, Python | easyway