api reference
VeltPay API
Accept USDT on TRON (TRC20) through a signed REST API. Create an order, show the buyer a per-order deposit address, and react to on-chain confirmation through signed webhooks — or hand off to hosted checkout.
https://api.veltpay.exampleplaceholder hostIntroduction
VeltPay is a USDT (TRC20) payment gateway. You integrate over a signed REST API: each order is issued its own TRON deposit address, the gateway watches the chain for the incoming transfer, and you are notified as the payment moves from detected to confirmed. No private keys ever leave your custody — Velt is watch-only.
- Per-order addresses. Every order gets a unique TRON address, so on-chain payments map cleanly to a single invoice.
- On-chain confirmation. Velt tracks confirmations and moves the order through a clear status lifecycle until it reaches finality.
- Signed webhooks. Every event is delivered with an HMAC signature you verify before trusting it, with retries and backoff on failure.
- Hosted checkout. Skip building a payment screen — redirect the buyer to a hosted page with QR, address, countdown, and live status.
All API responses are JSON. Amounts are decimal strings (never floats) to avoid rounding error, and the settlement asset is always USDT on the TRON chain.
Authentication (HMAC)
Every merchant API request is signed. Create an API key in the dashboard to get a key_id and a secret. The secret is shown once at creation — store it somewhere safe; Velt never displays it again.
Send three headers on every request:
| Header | Value |
|---|---|
X-Api-Key | Your key_id (identifies the key, not secret). |
X-Timestamp | Request time as Unix seconds. Must be within ±300s of server time (replay protection). |
X-Signature | Lowercase hex HMAC-SHA256 over the canonical string below, keyed by your secret. |
The signature is computed over a newline-joined canonical string:
signature = hex(
HMAC_SHA256(
secret,
ts + "\n" + METHOD + "\n" + path + "\n" + sha256hex(body)
)
)
// ts Unix seconds, the same value sent as X-Timestamp
// METHOD uppercase HTTP method, e.g. POST
// path request path incl. query, e.g. /v1/orders
// body the exact raw request body bytes ("" for GET)Build the same canonical string on your side and HMAC it with the secret. A worked example in Node and Go:
import crypto from "node:crypto";
function sign({ secret, method, path, body = "" }) {
const ts = Math.floor(Date.now() / 1000).toString();
const bodyHash = crypto.createHash("sha256").update(body).digest("hex");
const canonical = [ts, method.toUpperCase(), path, bodyHash].join("\n");
const signature = crypto
.createHmac("sha256", secret)
.update(canonical)
.digest("hex");
return { ts, signature };
}
const body = JSON.stringify({ order_ref: "inv_1001", amount: "49.99" });
const { ts, signature } = sign({
secret: process.env.VELT_SECRET,
method: "POST",
path: "/v1/orders",
body,
});
await fetch("https://api.veltpay.example/v1/orders", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-Api-Key": process.env.VELT_KEY_ID,
"X-Timestamp": ts,
"X-Signature": signature,
},
body,
});func sign(secret, method, path, body string) (ts, sig string) {
ts = strconv.FormatInt(time.Now().Unix(), 10)
sum := sha256.Sum256([]byte(body))
bodyHash := hex.EncodeToString(sum[:])
canonical := strings.Join(
[]string{ts, strings.ToUpper(method), path, bodyHash}, "\n")
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(canonical))
return ts, hex.EncodeToString(mac.Sum(nil))
}Replay protection
X-Timestamp drifts more than 300 seconds from server time are rejected. Sign each request fresh — never cache a signature — and keep your server clock in sync (NTP).Create an order
/v1/ordersCreate an order to receive a payment. Velt assigns a fresh TRON deposit address and a hosted checkout_url. Request body fields:
| Field | Type | Required | Description |
|---|---|---|---|
order_ref | string | Required | Your reference for this order. Echoed back and in webhooks. |
amount | string | Required | Decimal string in USDT, e.g. "49.99". Never a float. |
ttl_seconds | int | Optional | Seconds until the order expires. Defaults to the account setting. |
metadata | object | Optional | Arbitrary key/value pairs stored with the order and returned verbatim. |
Signed request (headers abbreviated — see Authentication):
curl -X POST https://api.veltpay.example/v1/orders \
-H "Content-Type: application/json" \
-H "X-Api-Key: $VELT_KEY_ID" \
-H "X-Timestamp: 1717079400" \
-H "X-Signature: 9f2c…e1b7" \
-d '{
"order_ref": "inv_1001",
"amount": "49.99",
"ttl_seconds": 1800,
"metadata": { "customer_id": "cus_88421" }
}'201 Created — the order, with its deposit address and checkout URL:
{
"id": "ord_3sK9wQ2bN7",
"order_ref": "inv_1001",
"status": "pending",
"amount": "49.99",
"amount_paid": "0",
"currency": "USDT",
"chain": "TRON",
"address": "TGq4...n8Yd",
"tx_hash": null,
"confirmations": 0,
"expires_at": "2026-05-30T12:30:00Z",
"checkout_url": "https://pay.veltpay.example/c/8f1d2a6e-4c3b-4f0a-9e21-7b6c5d4e3f2a"
}Retrieve & list orders
/v1/orders/{id}Fetch a single order by its id. The order object is the same shape returned at creation; amount_paid, status, tx_hash, and confirmations update as the payment progresses.
{
"id": "ord_3sK9wQ2bN7",
"order_ref": "inv_1001",
"status": "paid_unconfirmed",
"amount": "49.99",
"amount_paid": "49.99",
"currency": "USDT",
"chain": "TRON",
"address": "TGq4...n8Yd",
"tx_hash": "6b1f...a90c",
"confirmations": 7,
"expires_at": "2026-05-30T12:30:00Z",
"checkout_url": "https://pay.veltpay.example/c/8f1d2a6e-4c3b-4f0a-9e21-7b6c5d4e3f2a"
}/v1/orders?limit=&offset=List orders newest-first. Page with limit and offset (the response echoes both). Each entry in data is a full order object.
{
"data": [
{ "id": "ord_3sK9wQ2bN7", "order_ref": "inv_1001", "status": "paid_unconfirmed", "amount": "49.99", "...": "…" },
{ "id": "ord_2pH4vR8cM1", "order_ref": "inv_1000", "status": "completed", "amount": "120.00", "...": "…" }
],
"limit": 2,
"offset": 0
}Order status lifecycle
The happy path moves through four states in order:
Two outcomes leave the happy path: underpaid (a partial payment) and expired (the window closed).
| Status | Meaning |
|---|---|
| pending | Order created; waiting for the buyer to send USDT to the address. |
| paid_unconfirmed | The full amount was detected on-chain but is not yet final. |
| confirmed | Enough confirmations reached the finality threshold; the payment is settled. |
| completed | Settled and swept to your treasury — the terminal success state. |
| underpaid | Funds arrived but amount_paid is less than the amount due. |
| expired | The TTL elapsed before the full amount was received. |
Confirmations & finality
confirmations count. An order stays paid_unconfirmed until it reaches the configurable finality threshold (around 19 confirmations on TRON), then becomes confirmed. Wait for confirmed (or the order.confirmed webhook) before releasing goods — never paid_unconfirmed alone.Webhooks
Register an endpoint to receive order events as they happen. Velt POSTs a JSON body to your URL with three headers:
| Header | Value |
|---|---|
X-Velt-Event | The event name, e.g. order.confirmed. |
X-Velt-Timestamp | Unix seconds when the event was sent. |
X-Velt-Signature | Hex HMAC-SHA256 over timestamp + "." + raw_body, keyed by your endpoint secret. |
Events you may receive:
| Event | Sent when |
|---|---|
order.pending | Order created and awaiting payment. |
order.paid_unconfirmed | Full amount detected on-chain, not yet final. |
order.underpaid | Received amount is below the amount due. |
order.confirmed | Finality threshold reached — safe to fulfill. |
order.expired | TTL elapsed without full payment. |
The payload wraps the event name, the full order object, and a send timestamp:
{
"event": "order.confirmed",
"data": {
"id": "ord_3sK9wQ2bN7",
"order_ref": "inv_1001",
"status": "confirmed",
"amount": "49.99",
"amount_paid": "49.99",
"currency": "USDT",
"chain": "TRON",
"address": "TGq4...n8Yd",
"tx_hash": "6b1f...a90c",
"confirmations": 19,
"expires_at": "2026-05-30T12:30:00Z"
},
"sent_at": "2026-05-30T12:08:41Z"
}Verify the signature over the raw request body (before any JSON parsing) and compare in constant time:
import crypto from "node:crypto";
function verifyWebhook(rawBody, headers, endpointSecret) {
const ts = headers["x-velt-timestamp"];
const sig = headers["x-velt-signature"];
const expected = crypto
.createHmac("sha256", endpointSecret)
.update(ts + "." + rawBody) // sign timestamp + "." + raw body
.digest("hex");
const a = Buffer.from(sig, "hex");
const b = Buffer.from(expected, "hex");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Retries & backoff
2xx quickly, Velt retries delivery with exponential backoff. Make handlers idempotent — key off data.id + event — since the same event may arrive more than once.Hosted checkout
Don't want to build a payment screen? Redirect the buyer to the checkout_url returned on the order. The hosted page shows the QR code, the deposit address with a copy button, a live countdown, and the current status — no integration beyond the redirect.
/v1/public/orders/{id}The checkout page polls this unauthenticated public endpoint to refresh status. It is safe to expose because the id in the checkout link is an unguessable UUID and the response carries only display-safe fields — no secrets, no merchant data.
{
"status": "pending",
"amount": "49.99",
"amount_paid": "0",
"currency": "USDT",
"chain": "TRON",
"address": "TGq4...n8Yd",
"confirmations": 0,
"expires_at": "2026-05-30T12:30:00Z"
}Build your own?
GET /v1/orders/{id}) to poll status, and render the address as a TRC20 QR yourself.Next steps
Create an API key in the dashboard, then send your first signed POST /v1/orders.