TronTransferAPI Docs
Open the App

TronTransfer API

Energy-sponsored USDT (TRC-20) transfers on TRON. You keep custody — you sign every transaction; we rent and delegate the TRON energy your transfer needs, so your wallet pays one small flat TRX fee instead of burning 6.5–13.1 TRX of its own. Wallet short of TRX? The zero-TRX endpoints let the sender pay the fee in USDT instead.

Sending often? A prepaid API account charges each transfer from a TRX balance you top up, so every send is one signature — no fee transaction, no fee-confirmation wait. Create a key at trontransfer.com/account or with POST /account/keys. IP allow-lists, withdrawals, questions: [email protected]
Building an AI agent? Load the machine-readable spec at /openapi.json (OpenAPI 3.1, works for tool calling) and give your model /llms-full.txt, the complete flow, example and safety rules in one plain-text file. Your agent's own wallet signs; we never receive keys.

How a sponsored send works

A sponsored transfer is two signed transactions you submit together:

  1. Fee tx — a plain TRX transfer of the quoted fee to our collectAddress.
  2. USDT tx — your normal transfer(to, amount) call on the USDT contract.

The relay then, in order: broadcasts the fee and confirms it on-chain (proves payment) → rents + delegates energy to your wallet → broadcasts your USDT tx, which burns the delegated energy (≈0 TRX beyond the fee).

Non-custodial. Your USDT never leaves your wallet until you sign the transfer. A signed TRON tx can only move funds from its own owner, and the relay validates the fee tx pays exactly collectAddress — it can never redirect your money.

Quickstart (TronWeb)

import TronWeb from "tronweb";
const BASE = "https://trontransfer.com";
const tw = new TronWeb({ fullHost: "https://api.trongrid.io" }); // + your signer

async function sendUsdt(from, to, amountUsdt) {
  // 1) config — where the fee goes + the USDT contract
  const cfg = await (await fetch(`${BASE}/config`)).json();

  // 2) the exact fee for THIS recipient (warm vs cold pricing)
  const rcpt  = await (await fetch(`${BASE}/recipient?addr=${to}`)).json();
  const price = rcpt.pricing || cfg.pricing;          // { feeSun, feeTrx, profitable, ... }
  if (!price.profitable) throw new Error("Discounted transfer unavailable right now");

  // 3) build BOTH txs
  const feeTx = await tw.transactionBuilder.sendTrx(cfg.collectAddress, price.feeSun, from);
  const units = Math.round(amountUsdt * 1e6);         // USDT = 6 decimals
  let usdtTx = (await tw.transactionBuilder.triggerSmartContract(
    cfg.usdtContract, "transfer(address,uint256)", { feeLimit: 100_000_000 },
    [{ type: "address", value: to }, { type: "uint256", value: units }], from
  )).transaction;

  // 4) extend expiration so the relay can rent energy before the tx expires
  const feeExt  = await tw.transactionBuilder.extendExpiration(feeTx, 300);
  const usdtExt = await tw.transactionBuilder.extendExpiration(usdtTx, 300);

  // 5) sign BOTH (TronWeb returns the fully-signed tx object)
  const feeSigned  = await tw.trx.sign(feeExt);
  const usdtSigned = await tw.trx.sign(usdtExt);

  // 6) submit
  const res = await fetch(`${BASE}/v1/transfers`, {
    method: "POST", headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ feeTx: feeSigned, usdtTx: usdtSigned }),
  });
  const out = await res.json();
  if (!out.ok || !out.usdtTxid) throw new Error(out.message || "send failed");
  return out; // { ok:true, usdtTxid, feeTxid, ... }
}
Embedded / MPC wallets (no TronWeb signer): raw-sign each transaction's 32-byte txID (secp256k1, no EIP-191 prefix), assemble { ...rawTx, signature: [r‖s‖v] } yourself — picking the recovery byte that ecrecovers to the tx owner — and submit those. This is how card platforms integrate it server-side.

Server-side (Node.js)

Same flow — the only difference is the signer. Instead of a browser wallet, TronWeb holds a private key and signs silently. Ideal for payout backends, exchange withdrawals, remittance tooling and bots that custody their own keys.

// npm i tronweb    ·    Node 18+ (global fetch)
const { TronWeb } = require("tronweb");
const BASE = "https://trontransfer.com";

// Your key signs — no browser wallet. Keep it in env/KMS, never in code.
const tw = new TronWeb({
  fullHost: "https://api.trongrid.io",
  privateKey: process.env.TRON_PRIVATE_KEY,
});
const from = tw.defaultAddress.base58;                // the key's own address

async function sendUsdt(to, amountUsdt) {
  // 1) config + this recipient's exact fee (warm vs cold)
  const cfg   = await (await fetch(`${BASE}/config`)).json();
  const rcpt  = await (await fetch(`${BASE}/recipient?addr=${to}`)).json();
  const price = rcpt.pricing || cfg.pricing;
  if (!price.profitable) throw new Error("Discounted transfer unavailable right now");

  // 2) build BOTH txs: the TRX fee + the USDT transfer
  let feeTx   = await tw.transactionBuilder.sendTrx(cfg.collectAddress, price.feeSun, from);
  const units = Math.round(amountUsdt * 1e6);         // USDT = 6 decimals
  let usdtTx  = (await tw.transactionBuilder.triggerSmartContract(
    cfg.usdtContract, "transfer(address,uint256)", { feeLimit: 100_000_000 },
    [{ type: "address", value: to }, { type: "uint256", value: units }], from
  )).transaction;

  // 3) extend expiration so the relay can rent energy in time
  feeTx  = await tw.transactionBuilder.extendExpiration(feeTx, 300);
  usdtTx = await tw.transactionBuilder.extendExpiration(usdtTx, 300);

  // 4) sign BOTH with your private key (you never broadcast)
  const feeSigned  = await tw.trx.sign(feeTx);
  const usdtSigned = await tw.trx.sign(usdtTx);

  // 5) submit — the relay broadcasts
  const res = await fetch(`${BASE}/v1/transfers`, {
    method: "POST", headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ feeTx: feeSigned, usdtTx: usdtSigned }),
  });
  const out = await res.json();
  if (!out.ok || !out.usdtTxid) throw new Error(out.message || "send failed");
  return out; // { ok:true, usdtTxid, feeTxid, ... }
}
Older TronWeb? Some versions export the class directly — use const TronWeb = require("tronweb") instead of the destructured { TronWeb }. Everything else is identical.

Endpoints

GET/config

Static config + default pricing. Call once and cache briefly.

{
  "collectAddress": "TWYPXnJmykjfdXdTwzQeDhYaoXTFQVw666",
  "usdtContract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
  "trxUsd": 0.3253,
  "defaultRegion": "sea",
  "regions": ["sea", "global"],
  "pricing": { "feeTrx": 5.476, "feeSun": 5476000, "feeUsd": 1.78,
               "networkUsd": 2.23, "energy": 65000, "cold": false,
               "profitable": true, "savePct": 20 }
}

GET/recipient?addr=<base58>[&from=<base58>]

Recipient-specific info and the exact fee for a transfer to addr. The fee depends on whether the recipient already holds USDT — "warm" (65k energy) vs "cold" (131k). Also returns safety signals.

fieldmeaning
valid, activated, isContract, isUsdtContractaddress checks
blacklistedon a known blacklist — do not send
hasReceivedUsdt, hasSentUsdt, usdtBalance, ageDaysactivity / trust signals
pricingthe fee to use — { feeSun, feeTrx, feeUsd, profitable, cold, energy }
pricingByRegionper-region pricing (sea, global)

Always build the fee tx for pricing.feeSun. Warm ≈ 5.48 TRX; cold ≈ 10.76 TRX.

POST/v1/transfers

The sponsored send. Body: the two fully-signed tx objects. (With a prepaid API key in Authorization: Bearer, send only usdtTx: the fee is charged from your balance.)

Request

{ "feeTx":  { …signed TRX transfer… },
  "usdtTx": { …signed USDT transfer()… },
  "region": "sea" }

Success — 200

{ "ok": true, "status": "completed",
  "usdtTxid": "…", "feeTxid": "…",
  "amountUsdt": 200, "feeTrx": 5.476, "energyFeeTrx": 0 }

Failure — 4xx (validation) or 200 { ok:false } (mid-flight)

{ "ok": false, "code": "FEE_TOO_LOW",
  "message": "Fee too low: paid 2.10 TRX, need at least 5.48 TRX for this transfer.",
  "detail": { "paidTrx": 2.1, "requiredTrx": 5.48 } }
Judge success by the ok field, never the HTTP status. Business failures return HTTP 200 with { ok:false, code }, and a gateway timeout can return a non-JSON body while the transfer is still processing — on a parse failure, treat the outcome as unknown and reconcile (poll /receipt), never blindly resend.

POST /process is a legacy alias of /v1/transfers (same behavior).

GET/receipt?txid=<usdt_txid>

Solidified receipt — proves the energy was sponsored.

{ "result": "SUCCESS", "energyUsed": 64895, "energyFee": 0, "netFee": 0 }

energyFee: 0 confirms the transfer burned delegated (sponsored) energy, not the sender's TRX.

AML / address screening (optional, paid)

bitok = local/SEA (cheaper); elliptic = worldwide (deeper). Recently-scanned addresses return cached at no extra cost.

Fee paid in USDT (zero-TRX)

For senders who hold USDT but little or no TRX. The wallet signs two ordinary USDT transfer() calls and sends them to us unbroadcast:

  1. Fee tx (feeTx) — the quoted fee in USDT to feeAddress from the quote.
  2. Payment (payTx) — the USDT you're sending, to the recipient.

We supply the energy and bandwidth for both, so the sender's TRX balance can be 0. In order: bandwidth for both + energy for the fee transfer → broadcast feeTx and wait until it's in a block → energy for the payment → broadcast payTx. Typically 15–45 s.

Conditions. The sender's account must already be activated on TRON (any address that has ever received TRX) and hold at least amount + fee in USDT. Very rarely the wallet's daily bandwidth allowance is used up (BANDWIDTH_SHORT): try again tomorrow or use the TRX-fee flow. The USDT fee is higher than the TRX fee of /v1/transfers; it depends on live prices and on whether the recipient already holds USDT, so always use the quote. Check /zt/status before offering the option.

GET/zt/status[?sender=<base58>]

Whether the mode is available. Pass sender to ask for a specific wallet. Only offer the option when enabled is true.

{ "ok": true, "enabled": true, "paused": false, … }

GET/zt/quote?sender=<base58>&recipient=<base58>

{ "ok": true, "sender": "T…", "recipient": "T…",
  "feeAddress": "TAEHWm1AWH9ZisXg5HpJeJ8q7nbq8i4pXZ",
  "feeUsdt": <number>,          // the fee in USDT, for display
  "feeUnits": "<string>",       // the fee in USDT base units (6 decimals)
  "energy": <number>, "validS": 120 }

POST/zt/process

Request — two signed, unbroadcast USDT transfer() transactions from the same sender

{ "feeTx": { …signed transfer(feeAddress, feeUnits)… },
  "payTx": { …signed transfer(recipient, amount)… } }
feeTxpayTx
Contract / callUSDT TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t, transfer(address,uint256), one contract call
tofeeAddress from the quotethe recipient (not the fee address)
amountfeeUnits from the quotewhat you're sending (6 decimals)
Signerthe same sender, for both
TRX attached / memocall_value 0; no memo (a memo costs TRX)
fee_limit30 TRX works (it's a cap; the energy is supplied)
Expirationbetween 150 s and 15 min away when it reaches us; build with ~5–8 min

Answer — when both transfers are in a block

{ "ok": true, "status": "completed", "sender": "T…", "recipient": "T…",
  "amount": "10000000", "feeUnits": "…", "feeTxid": "…", "payTxid": "…" }

If that takes longer than 40 s you get { "ok": false, "status": "processing", "code": "PROCESSING", "feeTxid": "…", … }: the job keeps running on the server — poll /zt/job. Never broadcast the two transactions yourself, and never sign a new pair while one is processing. Sending the same pair again (a retry, a double click) is safe: it never spends twice, it returns or resumes the same job.

const BASE = "https://trontransfer.com", USDT = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
// sender = the connected address; amountUnits = USDT × 1e6
const q = await (await fetch(`${BASE}/zt/quote?sender=${sender}&recipient=${to}`)).json();
if (!q.ok) throw new Error(`${q.code}: ${q.message}`);
const build = async (dest, units) => {
  let { transaction: tx } = await tronWeb.transactionBuilder.triggerSmartContract(USDT, "transfer(address,uint256)",
    { feeLimit: 30e6, callValue: 0 }, [{ type: "address", value: dest }, { type: "uint256", value: units }], sender);
  // expiry ~8 min after the node built it, whatever the node's default is
  const ext = Math.round((tx.raw_data.timestamp + 480000 - tx.raw_data.expiration) / 1000);
  if (ext) tx = await tronWeb.transactionBuilder.extendExpiration(tx, ext);
  return tronWeb.trx.sign(tx);                        // sign only; never broadcast
};
const feeTx = await build(q.feeAddress, q.feeUnits), payTx = await build(to, amountUnits);
let out = await (await fetch(`${BASE}/zt/process`, { method: "POST", headers: { "content-type": "application/json" },
  body: JSON.stringify({ feeTx, payTx }) })).json();
while (out.status === "processing") {
  await new Promise((r) => setTimeout(r, 4000));
  out = await (await fetch(`${BASE}/zt/job?fee=${feeTx.txID}`)).json();
}

GET/zt/job?fee=<fee txid>

The current answer for that send, in the same shape as /zt/process: completed, pay_failed, processing, or failed with a code. { "ok": false, "code": "NOT_FOUND" } if the relay never accepted that pair. Poll every 3–5 s; most jobs end in under a minute, and every job ends within ~25 minutes of its transactions being signed.

Reading an answer

All responses are HTTP 200 JSON — check ok and status, never the HTTP status.

Nothing-charged list (mirror it exactly):

DISABLED MISCONFIGURED PAUSED RATE_LIMITED BUSY REFUSED BAD_ADDRESS PRICE_UNAVAILABLE
INVALID_JSON TX_MISSING TX_TAMPERED NOT_A_TRANSFER WRONG_TOKEN VALUE_ATTACHED MEMO_NOT_ALLOWED
FEE_LIMIT_TOO_LOW ZERO_AMOUNT BAD_SIGNATURE PERMISSION TX_EXPIRING SENDER_MISMATCH
FEE_WRONG_DESTINATION PAY_TO_FEE_ADDRESS SAME_TX FEE_TOO_LOW FEE_TOO_HIGH ACCOUNT_INACTIVE
INSUFFICIENT_USDT WOULD_FAIL BANDWIDTH_SHORT ENERGY_UNAVAILABLE ENERGY_TIMEOUT FEE_NOT_PAID
codemeaning
DISABLED · PAUSEDthe mode is off or paused right now — use the TRX-fee flow or retry later
ACCOUNT_INACTIVEthe sender's account isn't activated on TRON
INSUFFICIENT_USDTnot enough USDT for amount + fee
BANDWIDTH_SHORTthe wallet's daily bandwidth allowance is used up — try tomorrow or pay in TRX
FEE_TOO_LOW · FEE_TOO_HIGHfee outside 95 %–300 % of a fresh quote (requiredUsdt included) — re-quote and re-sign
TX_EXPIRING · TX_TAMPEREDexpires too soon / changed after signing, not built on a real recent block, or expiry more than 15 min away — rebuild
RATE_LIMITED · BUSYtoo many attempts / a send for this wallet is already running
ENERGY_UNAVAILABLE · ENERGY_TIMEOUTresources couldn't be supplied; nothing was broadcast (unlike ENERGY_UNAVAILABLE on /v1/transfers, no fee was taken)
UNCONFIRMEDwe couldn't confirm what happened on-chain — unknown: check both txids before doing anything

Prepaid API accounts

For developers and agents that send often. Instead of signing a TRX fee transaction for every transfer, you hold a TRX balance with us and send each transfer with an API key: the relay charges the fee from the balance, rents the energy and broadcasts your signed USDT transfer. The on-chain flow above keeps working exactly as before without a key.

Prepaid accounts pay the same per-transfer fee as the on-chain flow, charged from your TRX balance instead of a separate fee transaction, so each transfer needs one signature and skips the fee-confirmation wait. The fee follows the live energy market and is higher for recipients that do not yet hold USDT. Accounts that complete 100 or more transfers in the last 30 days pay 15% less; 500 or more, 30% less. A transfer is only accepted when the balance covers the current cold-recipient fee. If the transfer never goes out and nothing was bought for it, the full charge is returned to your balance automatically; once energy or bandwidth was bought for it, or it was broadcast and failed on-chain, only the network cost we paid is kept and the rest is returned. Balances never expire and can be withdrawn on request.

When it fits

For a single occasional send, the on-chain flow (no account) is simpler. Dashboard for humans: trontransfer.com/account.

1. Get a key

curl -X POST https://trontransfer.com/account/keys \
  -H "Content-Type: application/json" -d '{ "label": "payout bot" }'
HTTP 201
{ "ok": true,
  "key": "tt_live_…",                       // shown ONCE — store it server-side
  "account": { "id": 42, "keyPrefix": "tt_live_a1b2", "label": "payout bot",
               "depositCode": "TT-7K4MQ2", "balanceSun": 0, "balanceTrx": 0,
               "status": "active", "createdAt": "…", "lastUsedAt": null },
  "depositAddress": "TWYPXnJmykjfdXdTwzQeDhYaoXTFQVw666" }

2. Deposit TRX

Deposits are plain TRX transfers to depositAddress (the same address as collectAddress in /config; also in GET /account/pricing). We credit a deposit once its transaction is confirmed — the watcher checks about every minute, and POST /account/check-deposits looks right now. Two ways to tell us whose deposit it is:

Memo codeConfirmed wallet
Howput your depositCode (TT-7K4MQ2) in the transaction memo — the "note" / data field (TronLink's note field; TronWeb: transactionBuilder.addUpdateData(tx, code))a wallet a memo-matched deposit came from is listed on your account (depositAddresses: [{ address, added_at, confirmed }] in GET /account/me). Confirm it once with POST /account/deposit-addresses/confirm { "address": "T…" }; from then on plain TRX from that wallet is credited without a memo
Best fora wallet or script you control; the memo is matched case-insensitively, other words around it are fineyour own wallet sending again without a memo. Never confirm an exchange address: an exchange withdrawal comes from a hot wallet shared by many customers
Limitsone code per account, never changesup to 10 wallets per account; a wallet seen with two different accounts' memo codes is treated as shared — it is unbound from both and never credits without a memo again. POST /account/deposit-addresses/remove drops a wallet from the list
TRX only. Send TRX, not USDT or another token, to the deposit address: only TRX transfers are credited automatically. A memo-less deposit from a wallet that isn't confirmed (an exchange withdrawal, for example — most exchanges cannot set a memo) is parked as unmatched, not lost: email support with the transaction ID and your deposit code and it is assigned to your account by hand. Balances never expire; withdrawals on request by email.
// TronWeb: a 100 TRX deposit with the memo code
let tx = await tw.transactionBuilder.sendTrx(depositAddress, 100_000_000, from);
tx = await tw.transactionBuilder.addUpdateData(tx, "TT-7K4MQ2", "utf8");   // the memo
await tw.trx.sendRawTransaction(await tw.trx.sign(tx));   // a deposit IS broadcast by you (unlike a transfer)

3. Send with a key

Same as POST /v1/transfers, with Authorization: Bearer <key> and only usdtTx in the body: your signed, unbroadcast USDT transfer() with the expiration extended to ~5 minutes (the relay rents energy before it broadcasts; at most 15 minutes, else 400 TX_EXPIRATION_TOO_LONG). No feeTx — a fee transaction sent with a key is ignored.

curl -X POST https://trontransfer.com/v1/transfers \
  -H "Authorization: Bearer tt_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "usdtTx": { "txID": "…", "raw_data": { … }, "raw_data_hex": "…", "signature": ["…"] } }'

Node.js (TronWeb) — build, sign, submit

// npm i tronweb    ·    Node 18+ (global fetch)
const { TronWeb } = require("tronweb");
const BASE = "https://trontransfer.com";
const tw = new TronWeb({ fullHost: "https://api.trongrid.io", privateKey: process.env.TRON_PRIVATE_KEY });
const from = tw.defaultAddress.base58;
const headers = { "Content-Type": "application/json", "Authorization": `Bearer ${process.env.TT_API_KEY}` };

async function sendUsdt(to, amountUsdt) {
  // 1) the USDT contract + this recipient's fee (what will be charged) and safety signals
  const cfg  = await (await fetch(`${BASE}/config`)).json();
  const rcpt = await (await fetch(`${BASE}/recipient?addr=${to}`)).json();
  if (rcpt.blacklisted) throw new Error("recipient is blacklisted; do not send");
  if (!(rcpt.pricing || cfg.pricing).profitable) throw new Error("sponsorship paused right now; retry later");

  // 2) build + sign ONLY the USDT transfer — no fee tx, the fee comes off your balance
  let usdtTx = (await tw.transactionBuilder.triggerSmartContract(
    cfg.usdtContract, "transfer(address,uint256)", { feeLimit: 100_000_000 },
    [{ type: "address", value: to }, { type: "uint256", value: Math.round(amountUsdt * 1e6) }], from
  )).transaction;
  usdtTx = await tw.transactionBuilder.extendExpiration(usdtTx, 300);   // ~5 min: energy is rented first
  const usdtSigned = await tw.trx.sign(usdtTx);                          // sign only — never broadcast

  // 3) submit with your key
  const res  = await fetch(`${BASE}/v1/transfers`, { method: "POST", headers, body: JSON.stringify({ usdtTx: usdtSigned }) });
  const text = await res.text();
  let out; try { out = JSON.parse(text); } catch { out = null; }
  if (!out) return { ok: null, status: "unknown", checkTxid: usdtSigned.txID }; // gateway timeout: check /receipt, never resend blindly
  if (res.status === 402) throw new Error(`top up: balance ${out.detail.balanceTrx} TRX, need ${out.detail.requiredTrx} TRX (memo ${out.depositCode} to ${out.depositAddress})`);
  if (!out.ok) throw new Error(`${out.code}: ${out.message}`);
  return out; // { ok:true, status:"completed", usdtTxid, prepaid:{ chargedTrx, balanceTrx, tier } }
}

Success — 200

{ "ok": true, "status": "completed",
  "usdtTxid": "…", "feeTxid": "prepaid:<usdtTx.txID>",      // the ledger reference, not a chain tx
  "amountUsdt": 200, "feeTrx": 5.48, "energyFeeTrx": 0,
  "prepaid": { "chargedTrx": 5.48, "balanceTrx": 494.52,
               "tier": { "count": 12, "windowDays": 30, "minCount": 0, "mult": 1, "discountPct": 0 } } }

Balance too low — 402 (nothing charged)

{ "ok": false, "code": "INSUFFICIENT_BALANCE",
  "message": "Your balance must cover the current cold-recipient fee (10.76 TRX) to send.",
  "detail": { "balanceTrx": 3.2, "requiredTrx": 10.76, "chargeTrx": 5.48 },
  "depositAddress": "TWYPXnJmykjfdXdTwzQeDhYaoXTFQVw666", "depositCode": "TT-7K4MQ2" }

Reading the answer

Judge by ok and code, as for every /v1/transfers call. What happened to the charge:

HTTPcodethe charge
401INVALID_API_KEYnothing charged — the key is wrong or was rotated
402INSUFFICIENT_BALANCEnothing charged — the balance is below the current cold-recipient fee (detail.requiredTrx); top up at depositAddress with depositCode, then resend the same signed tx if it hasn't expired
429RATE_LIMITEDnothing charged — 30 transfers per minute per account, 60 bad-key attempts per 10 minutes per IP; wait
503PREPAID_UNAVAILABLEnothing charged — prepaid accounts are off right now; the on-chain flow still works
400TX_MISSING · USDT_NOT_A_TRANSFER · WRONG_USDT_CONTRACT · TX_TAMPERED · BAD_SIGNATURE · PERMISSION · TX_EXPIRED · TX_EXPIRATION_TOO_LONGnothing charged — fix the transaction and resend. TX_EXPIRATION_TOO_LONG: usdtTx may expire at most 15 minutes from now (detail.maxExpirationMs = 900000); sign with ~300 s
200 ok:falseMARKET_UNPROFITABLEnothing charged — the energy market is too expensive to sponsor; retry later
200 ok:falseNODE_UNAVAILABLEnothing charged — the relay couldn't verify the transaction's permissions with the network; try again in a moment
200 ok:falseDUPLICATEthe same signed usdtTx was already charged; it is never charged twice. The answer carries usdtTxid (and the recorded status when the transfer is yours): check /receipt; to send again, sign a new transaction
200 ok:falseENERGY_UNAVAILABLEno USDT was sent. refunded: true: nothing was bought for the transfer and the charge was returned at once (prepaid.balanceTrx is the new balance). refunded: false: energy was already bought, so the charge is settled from the chain once the tx has expired (settlement) — only our network cost is kept if it never lands
200 ok:falseUSDT_BROADCAST_FAILEDthe node refused the broadcast. Not settled at once — another node may still accept the same bytes — so the charge is settled from the chain once the tx has expired: refunded in full when nothing was bought, our network cost kept otherwise. usdtTxid is in the answer
200 ok:falseINTERNAL_ERRORa relay error after the charge: refunded at once when nothing was bought and the USDT was never handed to a node; otherwise settled from the chain like the two rows above
200 ok:false, status: "pending"USDT_NOT_CONFIRMEDthe USDT was broadcast. If it failed on-chain, only the network cost we paid is kept and the rest is credited back. If it is still pending, poll /receipt; the reconciler settles it from the chain. Never resend.
200 ok:truestatus: "completed"sent; prepaid.chargedTrx kept, prepaid.balanceTrx is what's left

How a charge is settled

what happened to the USDTthe charge
in a block, SUCCESSstands (status: "completed")
in a block, failed on-chainour actual network cost (the energy and bandwidth bought for it) is kept, the rest is refunded
never landed — the tx expired without reaching a blockrefunded in full when nothing was bought for it, our network cost kept otherwise; decided once the tx has expired

Account endpoints

All JSON; { ok:false, code, message } on failure. Bearer = Authorization: Bearer tt_live_….

endpointauthbodyanswer
POST/account/keysnone{ label? }201 { key, account, depositAddress } — 5 per hour per IP
GET/account/pricingnone—{ depositAddress, trxUsd, onchain: { warmTrx, coldTrx, warmUsd, coldUsd }, rules: { tiers, tierWindowDays, minBalanceMult, failedOnchain } }
GET/account/meBearer—{ account, depositAddress, depositAddresses: [{ address, added_at, confirmed }], tier, ledger, deposits, transfers, prices, estimatedTransfers } — prices.charge = your current warmTrx / coldTrx, minBalanceTrx, tier; estimatedTransfers = balance ÷ warm charge
POST/account/deposit-addresses/confirmBearer{ address }{ depositAddresses } — marks a listed wallet as yours; plain TRX from it is then credited without a memo (never an exchange address)
POST/account/deposit-addresses/removeBearer{ address }{ depositAddresses }
POST/account/check-depositsBearer—{ credited: [{ txid, amountTrx, matchedBy }], balanceTrx } — 3 per minute
POST/account/rotateBearer—{ key } — the old key stops working at once
POST/v1/transfersBearer{ usdtTx }the usual answer plus prepaid: { chargedTrx, balanceTrx, tier }

Every endpoint answers 503 PREPAID_UNAVAILABLE while prepaid accounts are switched off; 429 RATE_LIMITED when a limit above is hit.

Pricing

Error codes (/v1/transfers)

HTTPcodemeaning
400INVALID_JSON · TX_MISSINGmalformed request
400FEE_NOT_TRX · FEE_WRONG_DESTINATIONfee tx isn't a TRX transfer to collectAddress
400FEE_TOO_LOW · FEE_TOO_HIGHfee ≠ quoted amount (see detail.requiredTrx)
400USDT_NOT_A_TRANSFER · WRONG_USDT_CONTRACTUSDT tx isn't a transfer() on the USDT contract
400SENDER_MISMATCHthe two txs aren't signed by the same wallet
400TX_EXPIREDa signed tx expired — rebuild with extendExpiration(~300)
400TX_EXPIRATION_TOO_LONGprepaid: usdtTx expires more than 15 min from now (detail.maxExpirationMs) — sign with ~300 s; nothing charged
200 ok:falseMARKET_UNPROFITABLEenergy too pricey to sponsor now — retry later or self-pay
200 ok:falseFEE_BROADCAST_REJECTED · FEE_NOT_CONFIRMEDthe fee tx didn't land — no USDT sent
200 ok:falseENERGY_UNAVAILABLEcouldn't sponsor energy — no USDT sent; fee held (detail.feeHeld). Prepaid: refunded at once unless energy was bought — see reading the answer
200 ok:falseUSDT_BROADCAST_FAILEDUSDT tx rejected after retry (prepaid: the charge is settled from the chain once the tx has expired)
200 ok:falseNODE_UNAVAILABLEprepaid: couldn't verify the tx's permissions with the network — nothing charged; try again in a moment
200 ok:falseDUPLICATEprepaid: this signed tx was already charged — never charged twice; sign a new one to send again
401INVALID_API_KEYprepaid: the Authorization: Bearer key isn't valid — nothing charged
402INSUFFICIENT_BALANCEprepaid: balance below the current cold fee — top up (depositAddress, depositCode); nothing charged
429RATE_LIMITEDprepaid: too many transfers or key attempts — wait a minute; nothing charged
503PREPAID_UNAVAILABLEprepaid accounts are off right now — the on-chain flow still works

Notes & gotchas