DOCUMENTATION
Build with SMetaGateway.
Everything you need to create USDT TRC20 invoices from your server, send buyers to checkout and confirm payments with signed webhooks.
Overview
SMetaGateway is a noncustodial invoice layer for USDT on TRON. Merchants register a public receiving address; buyers pay that address directly. The gateway assigns each invoice a unique exact amount, tracks its status, records accounting data and notifies your server.
All API endpoints live under /api/v1 on the same origin as this website. Amounts are always decimal strings with up to six fractional digits — never floating-point numbers.
- Your server → SMetaGateway1POST /api/v1/invoices with an Idempotency-Key
- SMetaGateway → Your server2Invoice with payable_amount and checkout_url
- Your server → Buyer3Redirect the buyer to checkout_url
- Buyer → SMetaGateway4Buyer opens checkout and scans the QR code
- Buyer → Your TRON wallet5Buyer sends the exact USDT amount to your address
- SMetaGateway6Gateway finds the exact amount on-chain and confirms the invoice
- SMetaGateway → Your server7Signed invoice.confirmed webhook
- Your server → SMetaGateway8GET /api/v1/invoices/{id} to verify, then fulfil the order
Data pathMoney path
Quick start
1. Create a workspace and add a TRON receiving address. Copy its wallet ID.
2. Open Developers → API keys and create a key with an expiry. The secret starts with smg_ and is shown only once, so store it securely.
3. Create an invoice from your server with the request below. Save the invoice ID and the exact payable_amount.
4. Redirect the buyer to checkout_url. Mark the order paid only after a verified invoice.confirmed webhook and a server-side invoice lookup.
curl -X POST "https://smg.sabco24.com/api/v1/invoices" \
-H "Authorization: Bearer $SMETA_API_KEY" \
-H "Idempotency-Key: order-1001" \
-H "Content-Type: application/json" \
-d '{"merchant_order_id":"1001","amount":"49.95","wallet_id":"WALLET_ID","description":"Example order","expires_in_minutes":20}'Authentication
Server integrations authenticate with a bearer API key: Authorization: Bearer smg_…. Keys carry the scopes invoices:read, invoices:write and ledger:read, and can expire after 30, 90 or 365 days. An expired key returns 401 api_key_expired; roll it in the dashboard to get a new secret. Keep keys on your server; never ship them to browsers or mobile apps.
The first-party dashboard uses an HttpOnly session cookie. Account, wallet registration, API key and webhook management require that owner session and cannot be performed with an API key.
curl "https://smg.sabco24.com/api/v1/invoices/INVOICE_ID" \
-H "Authorization: Bearer $SMETA_API_KEY"Amounts and identification
Send the base price as a decimal string such as "49.95". The gateway adds a positive identification adjustment — 0.000001 to 0.009999 USDT below 100 USDT, or 0.000001 to 0.999999 USDT at 100 USDT and above — and returns base_amount, adjustment and payable_amount separately.
The exact payable total is reserved for 90 days for the same network, token and receiving address across all merchants. Unused totals are preferred; a reused total sets the invoice to review instead of automatic confirmation.
Reserved for 90 days per network, token and receiving address
{
"base_amount": "49.950000",
"adjustment": "0.003821",
"payable_amount": "49.953821"
}Invoices and idempotency
POST /api/v1/invoices requires an Idempotency-Key header. Repeating the same request with the same key returns the original invoice with idempotent_replay: true; reusing a key with different details returns 409 idempotency_conflict.
Each merchant_order_id can have one invoice. Store the returned invoice ID and payable_amount with your order and compare them when a webhook arrives.
POST /invoices + Idempotency-Key- New key201 — new invoice created
- Same key, same bodyOriginal invoice, idempotent_replay: true
- Same key, different body409 idempotency_conflict
{
"invoice": {
"id": "6f1c…",
"merchant_order_id": "1001",
"base_amount": "49.950000",
"adjustment": "0.003821",
"payable_amount": "49.953821",
"status": "pending",
"mode": "live",
"checkout_url": "https://smg.sabco24.com/checkout/…",
"expires_at": "2026-10-21T12:20:00.000Z",
"reserved_until": "2027-01-19T12:00:00.000Z"
},
"idempotent_replay": false
}Checkout and invoice status
checkout_url is a capability link: anyone holding it can view payer-safe details, so treat it as confidential. Checkout shows the merchant name, amount breakdown, receiving address, QR code and expiry, never your metadata or internal IDs.
Invoice status is one of pending, confirmed, expired or review. Checkout expires after expires_in_minutes (default 20, maximum 1440); the amount reservation lasts 90 days independently.
pending- Exact amount received
confirmed - Checkout time ran out
expired - Amount reused or needs manual check
review
Signed webhooks
Register a public HTTPS endpoint (port 443) in the dashboard. Its signing secret is shown once. Events: invoice.created, invoice.confirmed, invoice.expired, invoice.review and webhook.test. Deliveries retry up to eight times and can be retried manually.
Each request has X-SMeta-Event-Id, X-SMeta-Timestamp and X-SMeta-Signature headers. Compute HMAC-SHA256 over timestamp + "." + raw body, compare in constant time, reject timestamps older than five minutes, then parse JSON. Process each event ID once.
- 1Read the raw body and X-SMeta-* headers
- 2Timestamp within 5 minutes?Reject (4xx)
- 3HMAC-SHA256(timestamp + "." + body)
- 4Constant-time compare with X-SMeta-SignatureReject (4xx)
- 5Event ID already processed?Return 2xx, skip
- 6Update the order and return 2xx
import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody: Buffer, headers: Record<string, string>, secret: string) {
const timestamp = headers['x-smeta-timestamp'];
const signature = headers['x-smeta-signature'] ?? '';
if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return null;
const expected = 'sha256=' + createHmac('sha256', secret)
.update(timestamp + '.').update(rawBody).digest('hex');
const a = Buffer.from(expected), b = Buffer.from(signature);
if (a.length !== b.length || !timingSafeEqual(a, b)) return null;
const event = JSON.parse(rawBody.toString('utf8'));
return event.id === headers['x-smeta-event-id'] ? event : null;
}Errors and rate limits
Errors return a JSON body with error.code and error.message. Common statuses: 400 validation, 401 authentication, 403 scope or origin, 404 not found, 409 conflict or review, 429 rate limited.
Invoice creation is limited to 120 requests per minute per merchant. Retry 429 and 5xx responses with backoff and the same Idempotency-Key.
{
"error": {
"code": "idempotency_conflict",
"message": "This idempotency key was used with different invoice details."
},
"mode": "live"
}Integrations
The dashboard includes 32 copy-ready recipes covering Node.js, NestJS, PHP, Laravel, Python, Django, Go, Java, C#, Ruby, React, Next.js, Vue and commerce platforms such as WooCommerce, Magento and Shopware. The downloadable WooCommerce plugin is a preview and does not yet accept live API keys.
Recipes are examples, not published SDKs. Frontend examples call your own backend, which holds the API key.
Payment confirmation
The gateway reads TRON mainnet through TronGrid for confirmed USDT TRC20 transfers to every receiving address with an open invoice, and rechecks each transfer against a successful solidified receipt. A transfer of the exact payable_amount between invoice creation and expiry confirms the invoice, and its transaction hash is stored as tx_hash.
A transfer that arrives after expiry, matches a previously used amount, or targets an invoice already in review sets the status to review and is never confirmed automatically. Detection usually takes a few minutes. Zero commission and a noncustodial design do not by themselves establish tax exemption or regulatory authorization.