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.

Base URLhttps://api.veltpay.exampleplaceholder host

Introduction

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:

HeaderValue
X-Api-KeyYour key_id (identifies the key, not secret).
X-TimestampRequest time as Unix seconds. Must be within ±300s of server time (replay protection).
X-SignatureLowercase hex HMAC-SHA256 over the canonical string below, keyed by your secret.

The signature is computed over a newline-joined canonical string:

signature formula
text
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:

sign.js
Node.js
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,
});
sign.go
Go
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

Requests whose 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

POST/v1/orders

Create an order to receive a payment. Velt assigns a fresh TRON deposit address and a hosted checkout_url. Request body fields:

FieldTypeRequiredDescription
order_refstringRequiredYour reference for this order. Echoed back and in webhooks.
amountstringRequiredDecimal string in USDT, e.g. "49.99". Never a float.
ttl_secondsintOptionalSeconds until the order expires. Defaults to the account setting.
metadataobjectOptionalArbitrary key/value pairs stored with the order and returned verbatim.

Signed request (headers abbreviated — see Authentication):

create order
cURL
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:

201 Created
JSON
{
  "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

GET/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.

GET /v1/orders/ord_3sK9wQ2bN7
JSON
{
  "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"
}
GET/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.

GET /v1/orders?limit=2&offset=0
JSON
{
  "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:

pendingpaid_unconfirmedconfirmedcompleted

Two outcomes leave the happy path: underpaid (a partial payment) and expired (the window closed).

StatusMeaning
pendingOrder created; waiting for the buyer to send USDT to the address.
paid_unconfirmedThe full amount was detected on-chain but is not yet final.
confirmedEnough confirmations reached the finality threshold; the payment is settled.
completedSettled and swept to your treasury — the terminal success state.
underpaidFunds arrived but amount_paid is less than the amount due.
expiredThe TTL elapsed before the full amount was received.

Confirmations & finality

Each order carries a 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:

HeaderValue
X-Velt-EventThe event name, e.g. order.confirmed.
X-Velt-TimestampUnix seconds when the event was sent.
X-Velt-SignatureHex HMAC-SHA256 over timestamp + "." + raw_body, keyed by your endpoint secret.

Events you may receive:

EventSent when
order.pendingOrder created and awaiting payment.
order.paid_unconfirmedFull amount detected on-chain, not yet final.
order.underpaidReceived amount is below the amount due.
order.confirmedFinality threshold reached — safe to fulfill.
order.expiredTTL elapsed without full payment.

The payload wraps the event name, the full order object, and a send timestamp:

webhook payload
JSON
{
  "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:

verify-webhook.js
Node.js
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

If your endpoint does not return a 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.

GET/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.

GET /v1/public/orders/8f1d2a6e-…
JSON
{
  "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?

Prefer your own UI? Use the same public endpoint (or the authenticated 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.