Documentation
Everything you need to integrate USDT payments into your application.
Quick Start
Get up and running with USDT payments in three steps.
Register and Get Your API Keys
Create an account at /auth/register. You'll get both a test and live API key.
# Test mode (for development)
Authorization: Bearer test_abc123...
# Live mode (for production)
Authorization: Bearer live_xyz789...
Add Your Wallet Address
In your dashboard, add a USDT wallet address for each network you want to accept. Payments will go directly to these addresses.
T... (34 chars)
0x... (42 chars)
0x... (42 chars)
Create Your First Invoice
Make a POST request to create an invoice. Use
test_
key to simulate payments in sandbox mode.
curl -X POST https://q8qpay.com/api/v1/invoices \
-H "Authorization: Bearer test_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"reference": "ORDER-001",
"assetCode": "USDT_TRC20",
"amountUsdt": 100.0000,
"callbackUrl": "https://your-site.com/webhook"
}'
Authentication
API requests are authenticated using your API key sent in the Authorization header. You'll have two API keys:
Test API Key
test_abc123...
Use for development. Payments can be simulated via sandbox API.
Live API Key
live_xyz789...
Use for production. Real blockchain transactions only.
Keep Your API Keys Secret
Your API keys grant full access to your account. Never commit them to version control, expose them in client-side code, or share them publicly.
Invoices
An invoice represents a payment request. Create an invoice when your customer is ready to pay.
Invoice Lifecycle
Pending
Awaiting payment
Confirmed
Payment received
Expired
No payment before expiry
Exact Match System
Our gateway uses exact matching to confirm payments. All four conditions must be met:
Why amountUsdtExact differs from the amount you requested
Because matching is done on the amount, two open invoices for the same wallet cannot share the same amount — there would be no way to tell which one an incoming transfer was paying. So every invoice gets a tiny upward adjustment that makes its amount unique.
It is applied to every invoice, never only on a clash, so
that no invoice is ever issued at a round price. The most common
payment mistake is sending the round price (50.00)
instead of the exact amount. Because no invoice holds a round amount,
that mistake cannot confirm somebody else's invoice by accident.
Requesting 50.0000 four times on the same wallet:
1st invoice -> "50.0037"
2nd invoice -> "50.0081"
3rd invoice -> "50.0015"
4th invoice -> "50.0062"
- The adjustment is always upward and never exceeds +0.9999 USDT. In practice it is between 0.0010 and 0.0099.
- An issued amount never ends in
00in the last two decimal places. - When an invoice expires or is cancelled, its amount stays reserved for 24 hours, so a customer who pays late cannot confirm a newer invoice by mistake.
- If a payment arrives that matches no invoice exactly but is close to one of yours, we email you the closest invoices. Nothing is confirmed automatically; you check the transaction and confirm it from the invoice page.
- This is not a fee or a markup. Platform fees are billed from your q8qpay account balance and never change the amount your customer pays.
- Live and sandbox behave identically. Sandbox does not modify amounts in any special way.
Always charge the customer amountUsdtExact from the API response.
It is the only authoritative payment amount. A transfer of the
amount you originally requested will not match the invoice and will
not be confirmed.
Webhooks
When a payment is confirmed, expired, or cancelled, we send a POST request with payment details.
Webhook URL Priority
There are two ways to configure webhook URLs:
- Per-Invoice (callbackUrl) — Set when creating an invoice. Takes priority.
- Global (Settings) — Configured in your dashboard settings. Used as fallback.
If you specify a
callbackUrl
when creating an invoice, that URL will be used instead of the
webhook URL in your settings.
Webhook Payload
{
"invoice_id": "550e8400-e29b-41d4-a716-446655440000",
"invoice_reference": "ORDER-001",
"merchant_id": "uuid",
"status": "confirmed",
"asset_code": "USDT_TRC20",
"payout_address": "TXyz...",
"amount_usdt_exact": "100.0000",
"amount_fiat": "100.00",
"fiat_currency": "USD",
"tx_hash": "abc123...",
"metadata": { "orderId": "123" },
"paid_at": "2024-01-15T14:32:00Z",
"timestamp": "2024-01-15T14:32:00Z",
"type": "invoice.confirmed"
}
Signature Verification
const crypto = require('crypto');
function verifySignature(body, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(body))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Usage
const signature = req.headers['x-webhook-signature'];
const isValid = verifySignature(req.body, signature, webhookSecret);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
Create Invoice
/api/v1/invoices
Request Body
| Parameter | Type | Required | Description |
|---|---|---|---|
amountUsdt |
number | Yes | Amount in USDT (min: 0.0001, max: 1,000,000,000, max 4 decimals) |
assetCode |
enum | Yes | USDT_TRC20, USDT_ERC20, or USDT_BSC |
reference |
string | No | Unique order reference (max 50 chars). Auto-generated if not provided. |
callbackUrl |
string | No | Per-invoice webhook URL (max 500 chars). Must be a public http(s) address — private, internal and localhost addresses are refused. |
expirySeconds |
number | No | Invoice validity: 300-7200 seconds (default: 1800) |
metadata |
object | No | Custom JSON data returned in webhook |
walletId |
string | No | Specific wallet ID, a UUID (uses default wallet if not provided) |
title |
string | No | Invoice title shown on payment page (max 200 chars). E.g., "Order #12345" |
description |
string | No | Invoice description shown on payment page (max 500 chars) |
White Label Options
Enable white labeling to build your own custom payment UI instead of using our hosted checkout.
| Parameter | Type | Description |
|---|---|---|
useWhiteLabel |
boolean | Set to
true
to receive extra data for custom UI |
returnUrl |
string | URL to redirect after payment (max 500 chars) |
White Label Response
When
useWhiteLabel: true,
the response includes extra fields:
"whiteLabel": {
"cancelUrl": "https://...",
"statusUrl": "https://...",
"qrData": "TXyz123...",
"network": "tron",
"networkLabel": "Tron (TRC20)",
"title": "Order #12345",
"description": "Premium subscription"
}
Error Codes
| Code | Description |
|---|---|
200 |
Success |
201 |
Created |
400 |
Bad Request — Invalid parameters |
401 |
Unauthorized — Invalid API key |
403 |
Forbidden — Account suspended |
404 |
Not Found |
409 |
Conflict — Duplicate reference |
429 |
Too Many Requests — Rate limited |
500 |
Internal Server Error |
Supported Networks
USDT_TRC20
Tron Network
- Address: T... (34 chars)
- Confirmations: 20
- Avg. time: ~60 seconds
USDT_ERC20
Ethereum Network
- Address: 0x... (42 chars)
- Confirmations: 12
- Avg. time: ~3 minutes
USDT_BSC
Binance Smart Chain
- Address: 0x... (42 chars)
- Confirmations: 15
- Avg. time: ~45 seconds
Cancel Invoice
/api/v1/invoices/:id/cancel
Cancel a pending invoice. Only invoices with
pending
status can be cancelled.
curl -X POST https://q8qpay.com/api/v1/invoices/{invoice_id}/cancel \
-H "Authorization: Bearer test_your_api_key"
Note
Cancelling an invoice will trigger a webhook notification with
status
cancelled
if a callback URL is configured.
White Labeling
Build your own payment experience using our API. Instead of redirecting customers to our hosted payment page, you can create a fully branded checkout flow on your own domain.
How It Works
Key Fields for Custom UI
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"reference": "ORDER-12345",
"assetCode": "USDT_TRC20",
"payoutAddress": "TXyz123...abc456", // Display & QR code
"amountUsdtExact": "150.0000", // charge THIS exact value
"status": "pending",
"expiresAt": "2024-01-15T15:00:00Z", // Countdown timer
"createdAt": "2024-01-15T14:30:00Z"
}
Status Polling
// Poll every 5-10 seconds until status changes
GET /api/v1/invoices/:id
// Check response.status:
"pending" → Keep polling, show countdown
"confirmed" → Payment received! Show success
"expired" → Invoice expired, offer retry
"cancelled" → Invoice was cancelled
Full Customization
Use your own branding, colors, and domain. Your customers never
see our interface. Generate QR codes client-side using libraries
like
qrcode.js.
Customer Support Link
Set a support link so your customers can reach you if something goes wrong with a payment.
Why this matters: q8qpay is non-custodial. Funds move straight from your customer's wallet to yours — we never hold them. That means we cannot issue a refund, reverse a payment, or resend an order. Only you can.
Set it under Dashboard → Settings → Profile Information. It appears on your payment page behind the Need Help? button, alongside your business name.
- Use any URL your customers can reach you on — a contact page, a WhatsApp or Telegram link, a helpdesk, a Discord invite.
- Must start with
http://orhttps://. Other schemes are rejected. - A link rather than an email address, so you can route customers wherever you already handle support — and so your inbox is not scraped off a public page.
If you leave it blank
The payment page still explains that the payment went directly to you, but it can only tell the customer to reach you wherever they placed their order. If they cannot work that out, they have no way to contact anyone — so we strongly recommend setting it.
Sandbox Mode
Test your integration without real blockchain transactions using sandbox mode.
Test vs Live API Keys
Test API Key
test_abc123...
Creates invoices in sandbox mode. Use the simulate payment endpoint to test.
Live API Key
live_xyz789...
Creates real invoices. Monitors actual blockchain transactions.
Simulate Payment
In sandbox mode, use the simulate endpoint to mark an invoice as
paid. The invoice identifier goes in the request body, and you can
pass either the id or
the reference.
POST /api/v1/sandbox/simulate-payment
Authorization: Bearer test_your_api_key
Content-Type: application/json
{
"invoiceId": "2cac1824-3e4b-4c22-b503-51d4b82adf95"
}
// Response
{
"success": true,
"message": "Payment simulated successfully",
"invoiceId": "2cac1824-3e4b-4c22-b503-51d4b82adf95",
"invoiceReference": "TST_MSZ78MQ4_EF02D630",
"txHash": "test_c433e9fe7caf49bd9d9c328c4e93a73d",
"status": "confirmed"
}
Other Sandbox Endpoints
Both take the invoice id
or reference in the body,
and both require a test API key.
# Expire a pending test invoice immediately
POST /api/v1/sandbox/trigger-expiry
{ "invoiceId": "TST_MSZ78MQ4_EF02D630" }
# Send a test webhook to check your endpoint receives and verifies it.
# Both fields optional: url defaults to your test webhook URL, and
# invoiceId fills the payload with a real invoice instead of sample data.
POST /api/v1/sandbox/test-webhook
{
"url": "https://yoursite.com/webhooks/q8qpay",
"invoiceId": "TST_MSZ78MQ4_EF02D630"
}
// test-webhook response
{
"success": true,
"message": "Webhook delivered successfully",
"webhookUrl": "https://yoursite.com/webhooks/q8qpay",
"responseStatus": 200
}
Sandbox Webhooks
Webhooks are sent for sandbox invoices just like live invoices. Your test webhook URL will receive the same payload format, allowing you to fully test your webhook handling.
Best Practices
Store API Keys Securely
Never commit API keys to version control. Use environment variables and secrets management.
Use Idempotent References
Use unique, meaningful references (like order IDs) to prevent duplicate invoices and enable easy reconciliation.
Always Verify Webhook Signatures
Use timing-safe comparison when verifying HMAC signatures to prevent timing attacks.
Handle Webhooks Idempotently
Webhooks may be retried. Design your handler to safely process the same event multiple times.
Test Thoroughly in Sandbox
Test all scenarios including successful payments, expired invoices, and webhook handling before going live.