API Reference

The StablePay merchant API lets you create payment orders and receive signed webhooks when payments confirm on-chain. All requests are authenticated with HMAC-SHA256 signatures.

Authentication

Every request must include these headers:

HeaderDescription
SP-Key-IDYour public key identifier (pk_live_...)
SP-TimestampUnix seconds. Requests older than 5 minutes are rejected.
SP-NonceA unique random string per request (replay protection)
SP-Signaturesha256=<hex> — HMAC-SHA256 of the canonical string
Idempotency-KeyOptional. Safe retries for order creation (24h)

The canonical string (newline-joined) that you sign:

METHOD
/path
sorted_query_string
sha256_hex(raw_body)
timestamp
nonce
const crypto = require("crypto");

function sign({ method, path, query, body, timestamp, nonce, secret }) {
  const bodyHash = crypto.createHash("sha256").update(body || "").digest("hex");
  const canonical = [method, path, query, bodyHash, timestamp, nonce].join("\n");
  return crypto.createHmac("sha256", secret).update(canonical).digest("hex");
}

// headers: SP-Key-ID, SP-Timestamp, SP-Nonce, SP-Signature: sha256=<hex>

Chains & Tokens

Use the chain and token_id values from the table below when creating an order. Settlement is judged once confirmations are reached (per-chain). Available chains and tokens ultimately depend on your plan.

ChainFamilyTokens (token_id : symbol)Confirmations
TRONTrontronTron
TRXtron-trxTRXUSDTtron-usdtUSDT
19
ETHEREUMEthereumethereumEVM
ETHethereum-ethETHUSDTethereum-usdtUSDTUSDCethereum-usdcUSDC
20
BSCBSCbscEVM
BNBbsc-bnbBNBUSDTbsc-usdtUSDTUSDCbsc-usdcUSDC
15
POLYGONPolygonpolygonEVM
USDCpolygon-usdcUSDCUSDT0polygon-usdt0USDT0
128
SOLANASolanasolanaSolana
USDCsolana-usdcUSDCUSDTsolana-usdtUSDT
32

Native coins (TRX / BNB / ETH) can also be received, though merchants mostly collect stablecoins. token_id is the exact value used at order creation.

Create an order

POSThttps://your-domain.com/api/v1/orders

Request body:

{
  "chain": "tron",                  // tron | ethereum | bsc | polygon
  "token_id": "tron-usdt",          // namespaced token id
  "fiat_currency": "USD",
  "fiat_amount": "20.00",
  "merchant_order_id": "order-1001",  // your reference (optional)
  "metadata": { "cart": "abc" },      // optional
  "success_url": "https://shop.example.com/ok",
  "cancel_url": "https://shop.example.com/cancel"
}

Response 201:

{
  "order_id": "do_9f2...",
  "chain": "tron",
  "token_id": "tron-usdt",
  "fiat_currency": "USD",
  "fiat_amount": "20.00",
  "payable_amount": "20.004217",   // unique suffix identifies this order
  "pay_address": "TQmZ8...",
  "status": "pending",
  "expires_at": "2026-08-16T15:30:00.000Z",
  "checkout_token": "ct_...",
  "checkout_url": "https://your-domain.com/checkout/ct_..."
}

Redirect your customer to checkout_url. The payable_amount includes a tiny unique suffix so concurrent orders of the same amount can be told apart on-chain. Orders expire after 30 minutes.

Retrieve an order

GEThttps://your-domain.com/api/v1/orders/{order_id}

Returns the current order status, received amount, transaction hash and confirmation count. Poll this or (better) use webhooks.

Webhooks

Configure your endpoint in Dashboard → Developers. We send signed POST requests for order.detected, order.paid and order.expired events.

HeaderDescription
SP-Webhook-IDEvent id (evt_...) — use for deduplication
SP-Webhook-TimestampUnix seconds
SP-Webhook-Signaturesha256=<hex> — HMAC over "<timestamp>.<raw_body>"

Example order.paid payload:

{
  "id": "evt_8a1...",
  "event_version": "v1",
  "type": "order.paid",
  "created_at": "2026-08-16T15:02:00.000Z",
  "data": {
    "order_id": "do_9f2...",
    "merchant_order_id": "order-1001",
    "status": "paid",
    "chain": "tron",
    "token_id": "tron-usdt",
    "payable_amount": "20.004217",
    "received_amount": "20.004217",
    "fiat_currency": "USD",
    "fiat_amount": "20.00",
    "tx_hash": "8b3d...",
    "confirmations": 19
  }
}

Verify against the raw body bytes; do not re-serialize the JSON.

// Verify against the RAW body bytes; do not re-serialize the JSON.
const expected = crypto
  .createHmac("sha256", webhookSecret)
  .update(`${timestamp}.${rawBody}`)
  .digest("hex");

const provided = signature.replace("sha256=", "");
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(provided));

Errors

All errors follow the same envelope:

{
  "error": {
    "code": "auth.bad_signature",
    "message": "Signature mismatch",
    "request_id": "req_..."
  }
}