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 on BSC, Ethereum and Polygon, with automatic deposit detection, signed webhook notifications and fund sweeping to a destination wallet.
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.
- A backend that can make HTTPS requests and parse JSON.
- A public HTTPS URL to receive webhooks (use
ngrok,cloudflaredor any tunnel in dev). - Basic understanding of HMAC-SHA256 signature verification.
Flow overview
A payment moves through four states. Whatever you build should understand this progression:
- detected — TronDealer sees the deposit on the relevant network (BSC, ETH or Polygon).
- confirmed — The transaction accumulates enough on-chain confirmations (default 15 on BSC).
- notified — A webhook POST fires to your configured URL, signed with HMAC if you set a
webhook_secret. - swept — Funds are automatically swept to your main wallet or QvaPay account.
The 6 steps
- 1
1. Register your business via API
Registration is public and requires no manual approval. You need a Cloudflare Turnstile CAPTCHA token obtained from the frontend widget, but if you are testing from backend you can register through the web form at
/onboardand then authenticate.POSThttps://trondealer.com/api/v2/clients/register-publicAuthnone (Turnstile token)curl -X POST https://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": 15, "sweep_wallet": "0xABC...123", "payout_method": "wallet", "turnstile_token": "..." }'The response includes
client.api_keyprefixed withtd_followed by 64 hex chars. Store it in your secret vault: this header authenticates every subsequent call and cannot be recovered.Pick your payout_method carefullyAt registration you decide how to receive funds after sweep:
wallet(your personal EVM address),qvapay(QvaPay account by username or email) orzelle(email or phone). Changing it later requires editing settings in the dashboard. - 2
2. Assign a multi-chain wallet
Clients generate wallets on demand. The same address works on BSC, Ethereum and Polygon (the three V2-supported networks).
POSThttps://trondealer.com/api/v2/wallets/assignAuthx-api-key: td_...curl -X POST https://trondealer.com/api/v2/wallets/assign \ -H "x-api-key: td_..." \ -H "Content-Type: application/json" \ -d '{ "label": "Order #A-1024" }'If you pass the same
labeltwice, TronDealer returns the existing wallet (200 OK). Treatlabelas your idempotency key: order ID, user ID, whatever fits.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.
- 3
3. Query the on-chain balance
Before marking an order as paid you can verify live balances for a wallet. This endpoint queries the USDT and USDC contracts across the three networks and returns decoded balances.
POSThttps://trondealer.com/api/v2/wallets/balanceAuthx-api-key: td_...balance.sh curl -X POST https://trondealer.com/api/v2/wallets/balance \ -H "x-api-key: td_..." \ -H "Content-Type: application/json" \ -d '{ "address": "0xABC...123" }'Typical response:
balance-response.json { "success": true, "balances": { "bsc": { "native": "0.012", "usdt": "50.0", "usdc": "0.0" }, "eth": { "native": "0.0", "usdt": "0.0", "usdc": "25.5" }, "pol": { "native": "0.0", "usdt": "0.0", "usdc": "0.0" } } }In most cases you don't need to poll it: the confirmation webhook (step 5) fires automatically when a deposit is detected. Use this for manual reconciliation or internal dashboards.
- 4
4. List transactions with filters and pagination
To show a customer's history or reconcile deposits against your ERP, list transactions tied to their wallets.
POSThttps://trondealer.com/api/v2/wallets/transactionsAuthx-api-key: td_...list-transactions.js const res = await fetch('https://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:
tx_hash,block_number,from_address,to_address,asset(USDT/USDC), decodedamount, currentconfirmations,status,network,webhook_sent, and timestamps. - 5
5. Configure and verify the HMAC webhook
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 anX-Signature-256header with the HMAC-SHA256 signature of the full body. You must:- Read the raw body (before JSON-parsing).
- Compute
HMAC-SHA256(secret, raw_body)as hex. - Compare against the header value using a constant-time comparison.
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 = JSON.parse(req.body.toString()) // event.tx_hash, event.amount, event.asset, event.network, event.status console.log('Payment confirmed:', event) res.status(200).send('ok') } )Never use plain `==` comparisonA classic string-equality check is vulnerable to timing attacks. Use
crypto.timingSafeEqualin Node,hmac.compare_digestin Python, orhash_equalsin PHP. Small detail, big deal.RetriesIf 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.
- 6
6. Understand the end-to-end lifecycle
Each deposit walks through these states. Build your business logic around
confirmed(ornotified, which implies confirmed) — never arounddetected.State Trigger Recommended action detectedScanner sees the Transfer log Do not mark order as paid yet confirmed≥ min_confirmations blocks Candidate to release product/service notifiedWebhook delivered with 2xx Your system acknowledged receipt sweptFunds moved to main wallet Consolidated, ready for accounting Default confirmations are 15 (BSC, fast). Bump it to 20–30 if you handle larger amounts, or lower it to 6–10 for better UX. Value is per-client and set at registration or via
/dashboard/settings.
Reconciliation and best practices
Use a unique label per order (e.g. your order ID) when calling POST /wallets/assign. You get a dedicated wallet per transaction and 1:1 reconciliation: order-to-destination-address match is trivial.
- Idempotency: use
tx_hash + log_indexas a unique key in your database. If a webhook is delivered twice (retry) you won't double-credit. - Timeouts: don't flag an order as "failed" just because the wallet is empty after 60s. BSC deposits take ~15s, ETH several minutes.
- Logs: persist the raw webhook body alongside the signature. It will save you hours when debugging.
- Security: the
x-api-keymust live only on your backend. Never ship it in a frontend or mobile bundle.
FAQ
Next steps
With the flow live, the rest is operational hardening: monitor webhook deliveries at /dashboard/webhooks, set up alerts for failed sweeps at /dashboard/sweeps, and tune min_confirmations against your risk tolerance.
If you need endpoints not yet covered by V2 (TRON, Solana, Bitcoin), the Swagger docs list the V1 endpoints by chain.