Authentication
All API routes (except public status endpoints) require a Bearer token in the Authorization header. Keys have three scope levels:
ScopeCapabilities
readonlyRead events, invoices, webhooks, API key metadata. Cannot write anything.
merchantCreate and cancel invoices + all readonly operations.
adminFull control: key management, webhook registration, sweeps + all above.
Authorization: Bearer <your-api-key>
Invoice Endpoints
POST /v1/invoices merchant
Create a new payment invoice. Returns the invoice with a deposit address and a hosted checkout URL. Supports Idempotency-Key header to prevent duplicate creation on network retries.
Request body (JSON)
FieldTypeRequiredDescription
eventId string required ID of the event this invoice belongs to.
priceFiat string required Fiat price as a decimal string, e.g. "50.00".
fiatCurrency string required ISO 4217 currency code, e.g. "USD".
ttlMinutes integer optional Invoice validity in minutes (1–1440). Defaults to 30.
expiresInSeconds number optional Alias for ttlMinutes (seconds). Ignored when ttlMinutes is present.
metadata object optional Arbitrary key-value pairs stored with the invoice.
// Request { "eventId": "evt_abc", "priceFiat": "50.00", "fiatCurrency": "USD", "ttlMinutes": 60 } // Response 201 { "data": { "id": "inv_...", "status": "pending", "amountUsdt": "50.500000", "depositAddress": "T...", "expiresAt": "2026-01-01T01:00:00.000Z", "hostedUrl": "https://pay.example.com/pay/inv_..." } }
GET /v1/invoices readonly+
List invoices. Supports filtering and cursor-based pagination.
Query parameters
ParamTypeDescription
eventIdstringFilter by event ID.
statusstringFilter by invoice status (see lifecycle below).
qstringFull-text search on invoice fields.
metadata.KEYstringFilter by metadata field, e.g. metadata.orderId=123.
cursorstringPagination cursor from previous response.
limitintegerPage size (1–100, default 20).
GET /v1/invoices/:id readonly+
Get a single invoice with its detected payments and confirmation counts.
POST /v1/invoices/:id/cancel merchant
Cancel a pending invoice. Returns 409 if the invoice is not in a cancellable state.
Public Endpoints (no auth required)
GET /v1/public/invoices/:id public
Sanitized invoice status for checkout page polling. Returns a limited subset of invoice fields — no fiat price, no metadata.
// Response 200 — sanitized fields only { "data": { "id": "inv_...", "status": "pending", "amountUsdt": "50.500000", "depositAddress": "T...", "expiresAt": "2026-01-01T01:00:00.000Z", "network": "TRON", "paidAt": null } }
GET /pay/:invoiceId public
Hosted checkout page. Polls /v1/public/invoices/:id to update status in real-time. Use hostedUrl from the invoice create response to redirect your customer here.
Webhook Admin Endpoints
POST /v1/webhooks admin
Register a webhook endpoint. The server generates a random signing secret unless you provide one. The secret is returned only once at creation — store it securely.
Request body (JSON)
FieldTypeRequiredDescription
url string required HTTPS endpoint URL. Must be publicly reachable. Private IPs are rejected.
eventId string optional Scope deliveries to a single event. Omit to receive all events.
secret string optional Custom signing secret. Auto-generated (40 hex chars) if not provided.
GET /v1/webhooks readonly+
List all registered webhook endpoints. Secrets are not returned in list responses.
DELETE /v1/webhooks/:id admin
Delete a webhook endpoint. Returns 204 on success.
POST /v1/webhooks/test admin
Send a signed webhook.test event to a registered endpoint to verify connectivity and HMAC verification.
Request body (JSON)
FieldTypeRequiredDescription
endpointId string required ID of the registered webhook endpoint to test.
Invoice Lifecycle & Statuses
Invoices move through the following statuses:
pending payment_detected paid
pending payment_detected paid underpaid overpaid expired canceled overdue
StatusMeaning
pendingWaiting for payment. Deposit address assigned.
payment_detectedOn-chain payment seen but not yet in a solid (finalized) block.
paidPayment confirmed in a solid block. Both RPCs agree. Terminal.
underpaidAmount received is less than the required USDT amount.
overpaidAmount received exceeds the required USDT amount.
expiredTTL elapsed with no confirmed payment. Terminal.
canceledExplicitly canceled via API. Terminal.
overduePast expiry but a partial payment was detected.
Webhook Integration
The server POSTs a JSON payload to your endpoint for every invoice lifecycle event. Each delivery includes an eventUid for idempotency — store it to deduplicate retries.
Event payload shape
{ "eventUid": "invoice.paid:inv_abc:wh_3f9a:3", "eventType": "invoice.paid", "version": 3, // ...invoice-specific payload fields... }
eventUid format: {eventType}:{invoiceId}:{endpointId}:{version}. This value is stable across retry attempts — use it as your idempotency key. The version is a monotonic counter per (invoice, endpoint) pair and increments only when a new lifecycle event fires, not per delivery attempt.
Retry schedule: failed deliveries are retried up to 9 times with exponential backoff: 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h, 24 h, 24 h (~92 h total window). After exhausting retries the delivery moves to a dead-letter queue and an alert is logged.
X-Stablerails-Signature
Every webhook POST is signed with HMAC-SHA256 using the endpoint secret. The signature is in the X-Stablerails-Signature header:
X-Stablerails-Signature: t=<unixSeconds>,v1=<hex-hmac-sha256> // Signed payload = "<t>.<rawBody>" (timestamp + dot + the exact POST body) // Algorithm: HMAC-SHA256 // Tolerance: 300 seconds (5 minutes)
Verification (Node.js)
// Node.js — verify the HMAC signature (constant-time compare) const crypto = require('crypto'); function verifyStablerails(rawBody, header, secret, toleranceSec = 300) { // 1. Parse t= and v1= from the header value const tMatch = header.match(/(?:^|,)t=(d+)/); const v1Match = header.match(/(?:^|,)v1=([0-9a-f]+)/); if (!tMatch || !v1Match) throw new Error('Malformed signature'); const ts = Number(tMatch[1]); // 2. Reject stale timestamps (> 5 minutes) if (Math.abs(Date.now() / 1000 - ts) > toleranceSec) throw new Error('Stale timestamp'); // 3. Recompute: HMAC-SHA256("<ts>.<rawBody>", secret) const payload = ts + '.' + rawBody; const expected = crypto.createHmac('sha256', secret) .update(payload, 'utf8').digest('hex'); // 4. Constant-time compare (prevents timing attacks) const buf1 = Buffer.from(expected, 'hex'); const buf2 = Buffer.from(v1Match[1], 'hex'); if (buf1.length !== buf2.length || !crypto.timingSafeEqual(buf1, buf2)) throw new Error('Signature mismatch'); }