All API routes (except public status endpoints) require a Bearer token in the
Authorization header. Keys have three scope levels:
Scope
Capabilities
readonly
Read events, invoices, webhooks, API key metadata. Cannot write anything.
merchant
Create and cancel invoices + all readonly operations.
admin
Full control: key management, webhook registration, sweeps + all above.
Authorization: Bearer <your-api-key>
Invoice Endpoints
POST/v1/invoicesmerchant
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)
Field
Type
Required
Description
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.
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/webhooksadmin
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)
Field
Type
Required
Description
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/webhooksreadonly+
List all registered webhook endpoints. Secrets are not returned in list responses.
DELETE/v1/webhooks/:idadmin
Delete a webhook endpoint. Returns 204 on success.
POST/v1/webhooks/testadmin
Send a signed webhook.test event to a registered endpoint to verify
connectivity and HMAC verification.
On-chain payment seen but not yet in a solid (finalized) block.
paid
Payment confirmed in a solid block. Both RPCs agree. Terminal.
underpaid
Amount received is less than the required USDT amount.
overpaid
Amount received exceeds the required USDT amount.
expired
TTL elapsed with no confirmed payment. Terminal.
canceled
Explicitly canceled via API. Terminal.
overdue
Past 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.
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');
}