Step-by-step guide to connect your backend to TronDealer. Register your business via API, assign deposit wallets on 12 networks (EVM, TRON, Solana, Bitcoin, Litecoin, TON), validate HMAC X-Signature-256 and understand the deposit lifecycle for USDT, USDC and native coins.
This guide documents the full integration of TronDealer, the stablecoin payment gateway operated by QvaPay. By the end you will have a backend able to accept USDT and USDC (plus a few more stablecoins and native coins) on 12 networks, with automatic deposit detection, signed webhook notifications and fund sweeping to your payout destination.
Coming from another processor (Coinbase Commerce, BitPay, NOWPayments)? TronDealer's API covers the same flow — registration, checkout, webhook, reconciliation — but with zero price volatility (customers pay directly in stablecoins) and first-class support for Hispanic markets via QvaPay.
ngrok, cloudflared or any tunnel in dev).The V2 API is organized into six subsystems. Each has its own route prefix, its own deposit wallets and its own confirmation threshold; the request and response shapes are the same across all of them.
| Subsystem | Route prefix | Networks | Deposit assets | Default confirmations |
|---|---|---|---|---|
| EVM | /api/v2/ | BSC, Ethereum, Polygon, Arbitrum, Base, Avalanche, Robinhood Chain | USDT + USDC everywhere except Robinhood Chain (USDG only); PYUSD on Ethereum; native BNB, ETH and POL on BSC, Ethereum and Polygon | 10 (BSC, ETH, Base, Avalanche), 20 (Polygon), 6 (Arbitrum, Robinhood) |
| TRON | /api/v2/tron/ | TRON | USDT (TRC20), native TRX | 19 |
| Solana | /api/v2/sol/ | Solana | USDT, USDC, USDG (SPL), native SOL | 1 (finalized) |
| Bitcoin | /api/v2/btc/ | Bitcoin | BTC | 2 |
| Litecoin | /api/v2/ltc/ | Litecoin |
One EVM wallet address is valid on all seven EVM networks at once — you assign it once and pass network only where a request has to read a single chain. The other five subsystems generate their own address format.
Native coins are accepted as deposits, but the webhook amount for them is the USD equivalent at detection time, with amount_native and price_usd alongside, so your accounting stays dollar-denominated. Tiny native amounts (roughly under $1) are ignored entirely. Stablecoin deposits below 0.50 are ignored too.
The live catalog — chains, assets, contract addresses, decimals and confirmation thresholds — is served without auth at GET /api/v2/networks. Prefer it over hardcoding.
Optimism was retired on 2026-09-15: opt is no longer an accepted network value (400). If you integrated it earlier, remove it from your checkout; funds already in flight were swept normally.
A payment moves through four states. Whatever you build should understand this progression:
webhook_secret.Registration is public and requires no manual approval. register-public needs a Cloudflare Turnstile CAPTCHA token obtained from the frontend widget. Testing from a backend with no browser? Use the web form at /onboard, or POST /api/v2/clients/register-open — same body without turnstile_token, anonymous, limited to one registration per IP per hour.
none (Turnstile token)Use a unique label per order (e.g. your order ID) when calling POST /wallets/assign, and single_use: true if the wallet should retire after the payment. You get a dedicated wallet per transaction and 1:1 reconciliation: order-to-destination-address match is trivial.
data.tx_hash plus the chain locator the payload carries (vout_index on Bitcoin/Litecoin, lt on TON), plus to_address. If a webhook is delivered twice (retry) you won't double-credit.data.amount (USD). For native coins keep amount_native and price_usd for the audit trail.x-api-key must live only on your backend. Never ship it in a frontend or mobile bundle. Every clients/me/* route accepts it too, so a native app can talk to the API directly if the key lives in the device keychain.With the flow live, the rest is operational hardening: monitor webhook deliveries and failed sweeps from the dashboard (Webhooks and per-network tabs), tune min_confirmations against your risk tolerance, and declare any missed deposit with wallets/check-hash instead of opening a ticket.
The full V2 reference — every route, request and response — lives in the interactive API docs and the OpenAPI spec. Selling with WordPress? The WooCommerce plugin wraps this whole flow with no code.
| LTC |
| 6 |
| TON | /api/v2/ton/ | TON | USDT (Jetton), native GRAM | 1 |
curl -X POST https://www.trondealer.com/api/v2/clients/register-public \
-H "Content-Type: application/json" \
-d '{
"name": "My Shop",
"webhook_url": "https://my-backend.com/webhooks/trondealer",
"webhook_secret": "a-long-random-secret",
"min_confirmations": 10,
"payout_method": "wallet",
"sweep_wallet_evm": "0xABC...123",
"sweep_wallet_tron": "TXyz...789",
"turnstile_token": "..."
}'The response includes client.api_key prefixed with td_ followed by 64 hex chars. Store it in your secret vault: this header authenticates every subsequent call and cannot be recovered.
At registration you decide how to receive funds after sweep: wallet (your own addresses — one per subsystem you plan to use: sweep_wallet_evm, sweep_wallet_tron, sweep_wallet_sol, sweep_wallet_btc, sweep_wallet_ltc, sweep_wallet_ton; at least one is required), qvapay (qvapay_account: username or email) or zelle (zelle_contact: email or phone). Everything can be changed later with PATCH /api/v2/clients/me or from the dashboard Settings tab. Wallet payouts carry a 0.4% fee (+ $0.30 fixed below $10), deducted on-chain before forwarding; see Pricing.
Clients generate wallets on demand. On the EVM route the same address works on BSC, Ethereum, Polygon, Arbitrum, Base, Avalanche and Robinhood Chain; no network is needed to assign it. For the other networks call the subsystem's own route: /api/v2/tron/wallets/assign, /api/v2/sol/wallets/assign, /api/v2/btc/wallets/assign, /api/v2/ltc/wallets/assign, /api/v2/ton/wallets/assign — same body, chain-native address in the response.
x-api-key: td_...curl -X POST https://www.trondealer.com/api/v2/wallets/assign \
-H "x-api-key: td_..." \
-H "Content-Type: application/json" \
-d '{ "label": "Order #A-1024", "single_use": true }'If you pass the same label twice, TronDealer returns the existing active wallet (200 OK). Treat label as your idempotency key: order ID, user ID, whatever fits.
single_use: true marks the wallet inactive after its first successful sweep, so it drops out of the scan set. Use it for one-wallet-per-invoice checkouts; leave it off for a wallet a returning customer will pay repeatedly. Wallets with no activity for 120 days are deactivated automatically, and you can toggle any wallet yourself with PATCH /api/v2/clients/me/wallets/{id} (or the subsystem-prefixed equivalent) and { "status": "active" | "inactive" }. Each client can hold up to 100 active wallets per subsystem.
The private key is stored encrypted in our database and never exposed via API. TronDealer orchestrates every on-chain operation (sweep, gas funding) for you.
Before marking an order as paid you can verify live balances for a wallet. On the EVM route each request reads one chain — pass network; omitted means eth. The other subsystems take just the address.
x-api-key: td_...curl -X POST https://www.trondealer.com/api/v2/wallets/balance \
-H "x-api-key: td_..." \
-H "Content-Type: application/json" \
-d '{ "network": "bsc", "address": "0xABC...123" }'Typical response:
{
"success": true,
"wallet": { "address": "0xABC...123", "label": "Order #A-1024", "status": "active" },
"balances": {
"bsc": { "BNB": "0.012", "USDT": "50.0", "USDC": "0.0" }
}
}Balances are live on-chain reads and rate limited to one request per minute per client per subsystem (429 with code: "RATE_LIMITED" and a Retry-After header). In practice you don't need to poll it: the confirmation webhook (step 5) fires automatically when a deposit lands. Use this for manual reconciliation or internal dashboards.
To show a customer's history or reconcile deposits against your ERP, list transactions tied to their wallets.
x-api-key: td_...const res = await fetch('https://www.trondealer.com/api/v2/wallets/transactions', {
method: 'POST',
headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' },
body: JSON.stringify({
address: '0xABC...123',
status: 'confirmed', // detected | confirmed | notified | swept
limit: 20,
offset: 0,
}),
})
const { transactions, total } = await res.json()Each transaction carries the chain's natural identifier (tx_hash on EVM, tx_id on TRON, tx_signature on Solana, tx_hash + vout_index on Bitcoin and Litecoin, tx_hash + lt on TON), from_address, to_address, asset, decoded amount, current confirmations, status, network (EVM), , and timestamps.
This is the critical part of any payment gateway integration: making sure the webhooks you receive are authentic and not injected by a third party with knowledge of your URL.
If you registered a webhook_secret, every POST includes an X-Signature-256 header with the HMAC-SHA256 signature of the full body. You must:
HMAC-SHA256(secret, raw_body) as hex.The body has a fixed envelope: event (transaction.incoming, transaction.confirmed, transaction.swept, optionally transaction.confirmation_update), timestamp, and data with the deposit — tx_hash, from_address, to_address, asset, amount, amount_native, price_usd, confirmations, wallet_label, network, plus the chain locator (block_number, slot, vout_index, lt). For stablecoins amount equals amount_native; for native coins amount is the USD equivalent.
A classic string-equality check is vulnerable to timing attacks. Use crypto.timingSafeEqual in Node, hmac.compare_digest in Python, or hash_equals in PHP. Small detail, big deal.
If your webhook returns a non-2xx status, TronDealer retries with exponential backoff. Respond 200 as fast as possible and process the event asynchronously (queue) if your logic is heavy.
Each deposit walks through these states. Build your business logic around confirmed (or notified, which implies confirmed) — never around detected.
| State | Trigger | Recommended action |
|---|---|---|
detected | Scanner sees the deposit | Do not mark order as paid yet |
confirmed | ≥ min confirmations for that network | Candidate to release product/service |
notified | Webhook delivered with 2xx | Your system acknowledged receipt |
swept | Funds forwarded to your payout destination | Consolidated, ready for accounting |
Each step has its webhook event: transaction.incoming fires on detected (retried until delivered), transaction.confirmed fires when the threshold is met (5 attempts with backoff — this is the one to credit balances from), and transaction.swept fires best-effort after the sweep.
While a deposit is still pending you can also receive transaction.confirmation_update events each time the confirmation count advances — same payload as transaction.incoming plus a min_confirmations field, so you can render a "6 of 10 confirmations" progress bar. It is off by default: enable it with PATCH /api/v2/clients/me and {"notify_confirmation_updates": true}. Available on the EVM networks, Bitcoin and Litecoin. Delivery is fire-and-forget (no retries, ordering vs transaction.confirmed not guaranteed) — treat it as cosmetic, render the highest count seen, and never credit balances from it.
Confirmation thresholds default to the values in the networks table above. On the EVM networks your client-level min_confirmations overrides that default: bump it to 20–30 if you handle larger amounts, or lower it to 6 for better UX. TRON (19), Solana, Bitcoin, Litecoin and TON use their own fixed thresholds. The value is set at registration, via PATCH /api/v2/clients/me, or from the dashboard Settings tab.
import crypto from 'node:crypto'
import express from 'express'
const app = express()
const SECRET = process.env.WEBHOOK_SECRET
// IMPORTANT: raw body, not json()
app.post(
'/webhooks/trondealer',
express.raw({ type: 'application/json' }),
(req, res) => {
const sig = req.headers['x-signature-256']
const expected = crypto
.createHmac('sha256', SECRET)
.update(req.body)
.digest('hex')
if (!sig || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send('invalid signature')
}
const { event, data } = JSON.parse(req.body.toString())
// event: 'transaction.confirmed' | ...
// data.tx_hash, data.amount (USD), data.asset, data.network, data.wallet_label
if (event === 'transaction.confirmed') console.log('Payment confirmed:', data)
res.status(200).send('ok')
}
)