API reference
Base URL http://localhost:3000/api/v1

Introduction

Stablora is a custodial crypto payment API for shops, SaaS, casinos, games and marketplaces. Accept coins with hosted checkout, give every user a permanent deposit address, pay users out over the API and optionally convert everything to a stablecoin.

Base URLhttp://localhost:3000/api/v1
FormatJSON over HTTPS. Amounts are decimal strings, never floats.
AuthAuthorization: Bearer qk_live_… (live) or qk_test_… (testnets)
WebhooksSigned with HMAC-SHA256 (Stablora-Signature)
Machine-readableOpenAPI 3.1 · llms.txt
Live keys (qk_live_…) settle real funds on mainnets: TRON, BNB Chain, Ethereum, Polygon, Arbitrum, Base, Optimism, Avalanche, Bitcoin, Litecoin, Solana, Dogecoin and Monero. Test keys (qk_test_…) only reach testnets, whose coins have no value. A local development install runs simulated sandbox networks instead. See Environments.

How a payment works

  • Your server creates an invoice (POST /payments) priced in USD. Stablora locks the coin amount at the current market price and assigns a unique deposit address for that order.
  • You redirect the customer to the hosted checkout (paymentUrl) — or, recommended, create a checkout session (POST /checkout-sessions) and let the customer pick the coin and network themselves.
  • The customer sends coins. Stablora waits for confirmations, credits your balance minus the 0.5% processing fee and sends a signed payment.completed webhook.
  • You fulfil the order after verifying the webhook signature and, ideally, re-reading GET /payments/{id}.

Quickstart

Take a first payment in five minutes. Use a test key and a testnet while you build, then switch to a live key.

1. Create an API key

Sign in, open Developers → API keys and create a key with the permissions you need. The secret is shown once. Keep it on your server only.

2. Create a checkout (customer chooses the coin)

bash
curl -X POST http://localhost:3000/api/v1/checkout-sessions \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reference":"order-1042","amount":"49.90","currency":"USD","successUrl":"https://shop.example/thanks"}'

Without options the customer sees every coin and network your key may use. To fix the coin yourself, create an invoice instead: POST /payments with network and asset (e.g. tron / USDT).

3. Send the customer to checkout

Redirect to url from the response. The customer picks a coin, then a network; Stablora locks the amount, shows a deposit address just for this order, a QR code and live status.

4. Pay

Live key: pay from any wallet. Test key: send faucet coins on a testnet (see Testing guide). On a local sandbox install you can simulate the transfer:

bash
curl -X POST http://localhost:3000/api/v1/test/deposits \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"paymentId":"pay_…","amount":"49.911414","transactionHash":"sim_order_1042"}'

5. Receive the webhook

Set your webhook URL in Settings → Webhook destination, verify the signature (see Verify signatures) and fulfil on payment.completed.

Authentication

Send your secret key as a bearer token on every request. Live keys start with qk_live_, test keys with qk_test_. Keys are stored hashed; a lost key cannot be recovered — revoke it and create a new one.

http
Authorization: Bearer qk_live_3f9c…
KeyReachesUse for
qk_live_…Live mainnets only — real fundsYour production store or app
qk_test_…Testnets only — coins without valueDevelopment, staging, CI

A key never reaches the other environment: a test key gets 403 on a live network and a live key on a testnet. Every webhook carries mode (live, testnet or sandbox), so one endpoint can serve both.

Permissions

PermissionAllows
readAll GET requests and POST /payouts/quote.
paymentsCreate customers, wallets, invoices, checkout sessions, payment links, test deposits.
payoutsCreate, approve and cancel payouts and refunds.

Give each system only what it needs: a storefront needs read + payments; only a trusted back office should hold payouts.

IP allowlist

In Security → IP allowlist you can restrict all API keys and dashboard sign-in to specific IPs or CIDR ranges. Each key can additionally have its own list (set when you create it) — for example your payout server’s IP for the key with the payouts permission. A request must pass both lists; an empty list allows any IP. Blocked attempts are recorded in the security log.

Dashboard-only operations

Creating API keys, revealing the webhook secret, team management, two-factor settings and automatic withdrawal rules require a signed-in dashboard session (and a fresh 2FA code when enabled). They cannot be done with an API key.

Never put a secret key in browser or mobile code. The hosted checkout pages need no key.

Live, testnets & sandbox

Build against testnets with a test key, then switch your integration to a live key. The API is identical; only the key and the network ids differ.

LiveTestnetsSandbox (local install)
Keyqk_live_…qk_test_…qk_test_…
Networkstron, bnb, ethereum, polygon, arbitrum, base, optimism, avalanche, bitcoin, litecoin, solana, dogecoin, monerosepolia, base-sepolia, tron-nile, solana-devnet, bitcoin-testnet…The mainnet ids, simulated
AddressesA real deposit address per invoiceReal testnet addressesSimulated qtest_… identifiers
DepositsReal transfers, credited after confirmationsFaucet coins, credited after confirmationsPOST /test/deposits, instant
PayoutsReal signed transactionsReal signed testnet transactionsSimulated, instant

The public platform runs Live and Testnets. The sandbox exists only when you run Stablora locally for development.

Faucets and a go-live checklist: Testing guide.

Simulating edge cases (sandbox)

  • Underpaid / overpaid: send a smaller or larger amount to POST /test/deposits.
  • Late: pay after expires_at.
  • Duplicate delivery: repeat the same transactionHash — it is never credited twice.
  • Chain reorganization: POST /test/deposits/{id}/reverse sends payment.reversed.

Amounts, ids & pagination

Amounts

Amounts are decimal strings in the asset ("49.911414"), never floats. Every object also carries exact integer *_units in the asset’s smallest unit (decimals tells you the scale). Balances are never merged across networks: USDT on TRON and USDT on Ethereum are different balances.

Ids

PrefixObject
pay_Payment (invoice)
cs_Checkout session
pl_Payment link
cus_Customer
wal_Customer wallet
mwl_Merchant top-up wallet
dep_Deposit
out_Payout or refund
swp_Swap
evt_Event

Idempotency

POST /payments, checkout sessions and customers are idempotent by reference / externalId. Payouts and refunds require an Idempotency-Key header: retry with the same key and you get the original payout back; the same key with different details returns 409.

Pagination

List endpoints return { "data": [...], "nextCursor": "…" }, newest first. Pass ?cursor= to get the next page and ?limit= (1–200, default 50). nextCursor is null on the last page.

Rate limits

300 requests per minute per merchant. Public checkout endpoints are limited per IP. Exceeding a limit returns 429.

Create a payment

POST/api/v1/payments

Creates an invoice with its own deposit address. With currency: "USD" the coin amount is calculated from the live market price and locked for the life of the invoice — later price moves never change what the customer owes.

API key · payments permission

Body parameters

referencerequiredstringYour unique order id (max 120). Repeating it returns the same invoice.
amountrequireddecimal stringFiat amount when currency is set (USD, EUR or GBP), otherwise the amount in the asset.
currency"USD" | "EUR" | "GBP"Price the invoice in fiat; the coin amount is locked at the current rate. Omit to charge a fixed crypto amount.
networkrequiredstringNetwork id, e.g. tron, ethereum, solana, sepolia. See Networks & assets.
assetrequiredstringAsset on that network, e.g. USDT.
descriptionstringShown on the checkout page (max 200).
customerIdstringAttach the invoice to a customer (cus_…).
expirationMinutesinteger5–1440, default 60. Funds after expiry are still credited as late.
  • paymentUrl is relative: combine it with the Stablora origin you trust.
  • If you set a processing-fee payer of customer in Settings, amount is grossed up and baseAmount is what you net.

Errors

400Unknown network/asset or invalid amount.
409Reference already used with different details.
503No fresh market price for a USD invoice — retry shortly.
Request
curl -X POST "http://localhost:3000/api/v1/payments" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reference":"order-1042","amount":"49.90","currency":"USD","network":"tron","asset":"USDT","description":"Order #1042"}'
Response · 201
{
  "id": "pay_c18df35f8fe0c08ccd846672",
  "merchant_id": "mch_4e425c9f49d92545a65c80ce",
  "reference": "order-1042",
  "description": "Order #1042",
  "customer_id": null,
  "network": "tron",
  "asset": "USDT",
  "decimals": 6,
  "amount_units": "49911414",
  "received_units": "0",
  "fee_bps": 50,
  "status": "pending",
  "created_at": "2026-09-24T11:03:41.697Z",
  "expires_at": "2026-09-24T12:03:41.697Z",
  "paid_at": null,
  "underpaid_tolerance_bps": 0,
  "fee_payer": "merchant",
  "base_amount_units": "49911414",
  "amount": "49.911414",
  "received": "0",
  "baseAmount": "49.911414",
  "feePayer": "merchant",
  "underpaidTolerancePercent": 0,
  "paymentUrl": "/pay/pay_c18df35f8fe0c08ccd846672",
  "depositAddress": "qtest_tron_3e5f8507d9ffc1d52aaf921d1a8c3f155cd5fc54",
  "refunded": "0",
  "mode": "sandbox",
  "pricing": {
    "currency": "USD",
    "usdAmount": "49.9",
    "priceUsd": "0.999771324867",
    "priceUpdatedAt": "2026-09-24T11:02:10.000Z",
    "quotedAt": "2026-09-24T11:03:41.693Z",
    "lockedUntil": "2026-09-24T12:03:41.697Z",
    "locked": true,
    "source": "CoinGecko"
  }
}

Retrieve a payment

GET/api/v1/payments/{id}

The authoritative state of an invoice. Re-read it after a webhook before fulfilling.

API key · read permission

Path parameters

idrequiredstringPayment id pay_….

Errors

404The id does not exist or belongs to another merchant.
Request
curl "http://localhost:3000/api/v1/payments/pay_c18df35f8fe0c08ccd846672" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "id": "pay_c18df35f8fe0c08ccd846672",
  "merchant_id": "mch_4e425c9f49d92545a65c80ce",
  "reference": "order-1042",
  "description": "Order #1042",
  "customer_id": null,
  "network": "tron",
  "asset": "USDT",
  "decimals": 6,
  "amount_units": "49911414",
  "received_units": "49911414",
  "fee_bps": 50,
  "status": "completed",
  "created_at": "2026-09-24T11:03:41.697Z",
  "expires_at": "2026-09-24T12:03:41.697Z",
  "paid_at": "2026-09-24T11:03:41.744Z",
  "underpaid_tolerance_bps": 0,
  "fee_payer": "merchant",
  "base_amount_units": "49911414",
  "amount": "49.911414",
  "received": "49.911414",
  "baseAmount": "49.911414",
  "feePayer": "merchant",
  "underpaidTolerancePercent": 0,
  "paymentUrl": "/pay/pay_c18df35f8fe0c08ccd846672",
  "depositAddress": "qtest_tron_3e5f8507d9ffc1d52aaf921d1a8c3f155cd5fc54",
  "refunded": "0",
  "mode": "sandbox",
  "pricing": {
    "currency": "USD",
    "usdAmount": "49.9",
    "priceUsd": "0.999771324867",
    "priceUpdatedAt": "2026-09-24T11:02:10.000Z",
    "quotedAt": "2026-09-24T11:03:41.693Z",
    "lockedUntil": "2026-09-24T12:03:41.697Z",
    "locked": true,
    "source": "CoinGecko"
  }
}

List payments

GET/api/v1/payments

Invoices, newest first.

API key · read permission

Query parameters

statusstringFilter by status.
customerIdstringOnly this customer.
limitinteger1–200, default 50.
cursorstringnextCursor from the previous page.
Request
curl "http://localhost:3000/api/v1/payments?status=completed&limit=20" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "pay_c18df35f8fe0c08ccd846672",
      "merchant_id": "mch_4e425c9f49d92545a65c80ce",
      "reference": "order-1042",
      "description": "Order #1042",
      "customer_id": null,
      "network": "tron",
      "asset": "USDT",
      "decimals": 6,
      "amount_units": "49911414",
      "received_units": "49911414",
      "fee_bps": 50,
      "status": "completed",
      "created_at": "2026-09-24T11:03:41.697Z",
      "expires_at": "2026-09-24T12:03:41.697Z",
      "paid_at": "2026-09-24T11:03:41.744Z",
      "underpaid_tolerance_bps": 0,
      "fee_payer": "merchant",
      "base_amount_units": "49911414",
      "amount": "49.911414",
      "received": "49.911414",
      "baseAmount": "49.911414",
      "feePayer": "merchant",
      "underpaidTolerancePercent": 0,
      "paymentUrl": "/pay/pay_c18df35f8fe0c08ccd846672",
      "depositAddress": "qtest_tron_3e5f8507d9ffc1d52aaf921d1a8c3f155cd5fc54",
      "refunded": "0",
      "mode": "sandbox",
      "pricing": {
        "currency": "USD",
        "usdAmount": "49.9",
        "priceUsd": "0.999771324867",
        "priceUpdatedAt": "2026-09-24T11:02:10.000Z",
        "quotedAt": "2026-09-24T11:03:41.693Z",
        "lockedUntil": "2026-09-24T12:03:41.697Z",
        "locked": true,
        "source": "CoinGecko"
      }
    }
  ],
  "nextCursor": "MjAyNi0wOS0yNFQxMTowMzo0MS43MTdafHBheV8xNjVj…"
}

Payment statuses

StatusMeaningFulfil?
pendingWaiting for funds.No
completedFull amount received (or within your underpayment tolerance).Yes
underpaidLess than the amount arrived. More can still be sent until expiry.Review
overpaidMore than the amount arrived. The surplus is in your balance; refund it if needed.Yes, then refund surplus
lateFunds arrived after expires_at. They are credited but flagged.Review
expiredNothing arrived before expires_at.No
reversedA chain reorganization undid the credit. See Reversals.No — hold the order
heldFunds came from a sanctioned address and are frozen pending compliance review. See Address screening.No

Only completed (and an overpaid you accept) should trigger fulfilment. Never trust a status coming from the customer’s browser or a redirect.

Preview a USD quote

POST/api/v1/quotes

Shows how much of an asset a USD amount buys right now, without creating an invoice. Quotes are indicative; the invoice locks its own price at creation.

API key · payments permission

Body parameters

networkrequiredstringNetwork id.
assetrequiredstringAsset.
usdAmountrequireddecimal stringUSD amount.
Request
curl -X POST "http://localhost:3000/api/v1/quotes" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"network":"bitcoin","asset":"BTC","usdAmount":"100"}'
Response · 200
{
  "network": "bitcoin",
  "asset": "BTC",
  "usdAmount": "100",
  "usdCents": "10000",
  "amount": "0.00119688",
  "amountUnits": "119688",
  "priceUsd": "83551.147838655772",
  "priceUpdatedAt": "2026-09-24T11:02:10.000Z",
  "quotedAt": "2026-09-24T11:03:41.731Z",
  "quoteExpiresAt": "2026-09-24T11:04:41.731Z",
  "source": "CoinGecko"
}

Create a checkout session

POST/api/v1/checkout-sessions

A USD order where the customer chooses the coin on the hosted page. When they choose, a normal locked invoice is created for that option. Redirect the customer to url.

API key · payments permission

Body parameters

referencerequiredstringYour order id (max 100). Idempotent.
amountrequireddecimal stringAmount in currency.
currency"USD" | "EUR" | "GBP"Default USD.
descriptionstringShown to the customer.
optionsarrayList of { network, asset } to offer. Default: every coin and network your key may use (a live key: all live options; a test key: all testnets).
successUrlstringWhere the “Return to store” button leads after payment.
customerIdstringAttach resulting invoices to a customer.
expirationMinutesinteger5–1440, default 60.
Request
curl -X POST "http://localhost:3000/api/v1/checkout-sessions" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reference":"cart-5531","amount":"19.99","successUrl":"https://shop.example/thanks","options":[{"network":"tron","asset":"USDT"},{"network":"bitcoin","asset":"BTC"},{"network":"solana","asset":"SOL"}]}'
Response · 201
{
  "id": "cs_cc25b6efa956b4911ea9920a",
  "merchant_name": "Orbit Commerce",
  "brand": {
    "color": null,
    "supportEmail": null
  },
  "description": "cart-5531",
  "usdAmount": "19.99",
  "options": [
    {
      "network": "tron",
      "asset": "USDT",
      "networkName": "TRON",
      "testnet": false
    },
    {
      "network": "bitcoin",
      "asset": "BTC",
      "networkName": "Bitcoin",
      "testnet": false
    },
    {
      "network": "solana",
      "asset": "SOL",
      "networkName": "Solana",
      "testnet": false
    }
  ],
  "expires_at": "2026-09-24T12:03:41.800Z",
  "expired": false,
  "payment": null,
  "url": "http://localhost:3000/checkout/cs_cc25b6efa956b4911ea9920a"
}

Simulate a deposit (sandbox)

POST/api/v1/test/deposits

Simulates an incoming transfer on a sandbox network. Refused on testnets — send real testnet coins there instead.

API key · payments permission

Body parameters

paymentId | walletId | merchantWalletIdrequiredstringWhat the transfer pays: an invoice, a permanent customer wallet or your top-up wallet.
amountrequireddecimal stringAmount in the asset.
assetstringRequired for wallets; taken from the invoice otherwise.
transactionHashrequiredstringUnique id such as sim_order_1042. Replays are ignored.
fromAddressesarraySender addresses, screened against sanctions lists — use a listed address to test payment.held.
Request
curl -X POST "http://localhost:3000/api/v1/test/deposits" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"paymentId":"pay_c18df35f8fe0c08ccd846672","amount":"49.911414","transactionHash":"sim_order_1042"}'
Response · 201
{
  "id": "dep_709ecb039bb5d29ece216883",
  "merchant_id": "mch_4e425c9f49d92545a65c80ce",
  "wallet_id": null,
  "payment_id": "pay_c18df35f8fe0c08ccd846672",
  "customer_id": null,
  "network": "tron",
  "asset": "USDT",
  "decimals": 6,
  "amount_units": "49911414",
  "fee_units": "249558",
  "tx_hash": "sim_order_1042",
  "event_index": 0,
  "created_at": "2026-09-24T11:03:41.744Z",
  "merchant_wallet_id": null,
  "reversed_at": null,
  "reversal_reason": null,
  "amount": "49.911414",
  "fee": "0.249558",
  "net": "49.661856",
  "mode": "sandbox"
}

Simulate a reorg (sandbox)

POST/api/v1/test/deposits/{id}/reverse

Undoes a sandbox deposit as if the block had been reorganized away, and sends payment.reversed (or deposit.reversed). Use it to test your handling.

API key · payments permission

Path parameters

idrequiredstringDeposit id dep_….
Request
curl -X POST "http://localhost:3000/api/v1/test/deposits/dep_709ecb039bb5d29ece216883/reverse" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
Response · 200
{
  "id": "dep_709ecb039bb5d29ece216883",
  "merchant_id": "mch_4e425c9f49d92545a65c80ce",
  "wallet_id": null,
  "payment_id": "pay_c18df35f8fe0c08ccd846672",
  "customer_id": null,
  "network": "tron",
  "asset": "USDT",
  "decimals": 6,
  "amount_units": "49911414",
  "fee_units": "249558",
  "tx_hash": "sim_order_1042",
  "event_index": 0,
  "created_at": "2026-09-24T11:03:41.744Z",
  "merchant_wallet_id": null,
  "reversed_at": "2026-09-24T11:03:41.991Z",
  "reversal_reason": "Simulated chain reorganization (sandbox).",
  "amount": "49.911414",
  "fee": "0.249558",
  "net": "49.661856",
  "mode": "sandbox"
}

Permanent user wallets

For casinos, games, exchanges and top-up flows each of your users gets one permanent deposit address per network. Anything sent to it — any amount, any time — is credited to that user and you receive deposit.confirmed.

  • Create the customer once with your own user id (externalId).
  • Assign a wallet per network. Repeating the call returns the same address.
  • Show the address in your app. Credit your user when you receive deposit.confirmed.

Balance model (Settings → Withdrawals): pooled keeps one merchant balance you pay everyone from (typical for casinos); per customer keeps a separate ledger balance per user.

Create a customer

POST/api/v1/customers

Idempotent by externalId: repeating it returns the existing customer. Different merchants can use the same externalId without sharing anything.

API key · payments permission

Body parameters

externalIdrequiredstringYour user id (max 120).
namestringDisplay name (max 100).
Request
curl -X POST "http://localhost:3000/api/v1/customers" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"externalId":"player-8841","name":"Alex Morgan"}'
Response · 201
{
  "id": "cus_116f187959c5a838e125b339",
  "merchant_id": "mch_4e425c9f49d92545a65c80ce",
  "external_id": "player-8841",
  "name": "Alex Morgan",
  "created_at": "2026-09-24T11:03:41.255Z"
}

List customers

GET/api/v1/customers

Customers, newest first.

API key · read permission

Query parameters

limitinteger1–200, default 50.
cursorstringNext page.
Request
curl "http://localhost:3000/api/v1/customers" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "cus_116f187959c5a838e125b339",
      "merchant_id": "mch_4e425c9f49d92545a65c80ce",
      "external_id": "player-8841",
      "name": "Alex Morgan",
      "created_at": "2026-09-24T11:03:41.255Z"
    }
  ],
  "nextCursor": null
}

Assign a wallet

POST/api/v1/wallets

Returns the customer’s permanent address on a network, creating it on first use.

API key · payments permission

Body parameters

customerIdrequiredstringCustomer cus_….
networkrequiredstringNetwork id. One address serves every asset on that network.
Request
curl -X POST "http://localhost:3000/api/v1/wallets" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"customerId":"cus_116f187959c5a838e125b339","network":"tron"}'
Response · 201
{
  "id": "wal_e3410ad818b1d6e3b45a8637",
  "merchant_id": "mch_4e425c9f49d92545a65c80ce",
  "customer_id": "cus_116f187959c5a838e125b339",
  "network": "tron",
  "address": "qtest_tron_4295b8b9889b70e74451d4e501739c5b7c330163",
  "created_at": "2026-09-24T11:03:41.270Z"
}

List wallets

GET/api/v1/wallets

Wallets, optionally for one customer.

API key · read permission

Query parameters

customerIdstringOnly this customer.
limitinteger1–200, default 50.
cursorstringNext page.
Request
curl "http://localhost:3000/api/v1/wallets?customerId=cus_116f187959c5a838e125b339" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "wal_e3410ad818b1d6e3b45a8637",
      "merchant_id": "mch_4e425c9f49d92545a65c80ce",
      "customer_id": "cus_116f187959c5a838e125b339",
      "network": "tron",
      "address": "qtest_tron_4295b8b9889b70e74451d4e501739c5b7c330163",
      "created_at": "2026-09-24T11:03:41.270Z"
    }
  ],
  "nextCursor": null
}

Customer balances

GET/api/v1/customers/{id}/balances

A customer’s ledger balances per network and asset (per-customer balance model).

API key · read permission

Path parameters

idrequiredstringCustomer cus_….

Errors

404The id does not exist or belongs to another merchant.
Request
curl "http://localhost:3000/api/v1/customers/cus_116f187959c5a838e125b339/balances" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "data": [
    {
      "network": "tron",
      "asset": "USDT",
      "decimals": 6,
      "available": "119.4",
      "reserved": "0"
    }
  ]
}

Balances

GET/api/v1/balances

Your total balances per network and asset. available can be paid out; reserved is held by pending payouts.

API key · read permission
Request
curl "http://localhost:3000/api/v1/balances" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "data": [
    {
      "network": "tron",
      "asset": "USDT",
      "decimals": 6,
      "available": "1809.816856",
      "reserved": "50"
    },
    {
      "network": "bitcoin",
      "asset": "BTC",
      "decimals": 8,
      "available": "0.0412",
      "reserved": "0"
    }
  ]
}

Unallocated balances

GET/api/v1/balances/unallocated

Only the balance that belongs to no customer: the pooled payout pool, top-ups and invoice income. With the per-customer model this is what you can withdraw for yourself.

API key · read permission
Request
curl "http://localhost:3000/api/v1/balances/unallocated" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "data": [
    {
      "network": "tron",
      "asset": "USDT",
      "decimals": 6,
      "available": "1690.416856",
      "reserved": "0"
    }
  ]
}

List deposits

GET/api/v1/deposits

Every credited transfer with fee and net amount. reversed_at is set if a reorganization undid it.

API key · read permission

Query parameters

customerIdstringOnly this customer.
limitinteger1–200, default 50.
cursorstringNext page.
Request
curl "http://localhost:3000/api/v1/deposits" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "dep_709ecb039bb5d29ece216883",
      "merchant_id": "mch_4e425c9f49d92545a65c80ce",
      "wallet_id": null,
      "payment_id": "pay_c18df35f8fe0c08ccd846672",
      "customer_id": null,
      "network": "tron",
      "asset": "USDT",
      "decimals": 6,
      "amount_units": "49911414",
      "fee_units": "249558",
      "tx_hash": "sim_order_1042",
      "event_index": 0,
      "created_at": "2026-09-24T11:03:41.744Z",
      "merchant_wallet_id": null,
      "reversed_at": null,
      "reversal_reason": null,
      "amount": "49.911414",
      "fee": "0.249558",
      "net": "49.661856",
      "mode": "sandbox"
    }
  ],
  "nextCursor": null
}

Get a top-up wallet

POST/api/v1/topup-wallets

Your own deposit address for funding the payout pool on a network (created on first use, then reused). Top-ups carry no processing fee and emit topup.confirmed.

API key · payments permission

Body parameters

networkrequiredstringNetwork id.
Request
curl -X POST "http://localhost:3000/api/v1/topup-wallets" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"network":"tron"}'
Response · 201
{
  "id": "mwl_59a8b99deaaebb5dafcf64c7",
  "merchant_id": "mch_4e425c9f49d92545a65c80ce",
  "network": "tron",
  "address": "qtest_tron_9bef0c0f94c136b5dc51f3d492d49bf77c45cfde",
  "created_at": "2026-09-24T11:03:41.928Z"
}

List top-up wallets

GET/api/v1/topup-wallets

All your top-up addresses.

API key · read permission
Request
curl "http://localhost:3000/api/v1/topup-wallets" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "mwl_59a8b99deaaebb5dafcf64c7",
      "network": "tron",
      "address": "qtest_tron_9bef0c0f94c136b5dc51f3d492d49bf77c45cfde",
      "created_at": "2026-09-24T11:03:41.928Z"
    }
  ]
}

Payouts & withdrawals

Pay your users (casino and game withdrawals, marketplace sellers) or yourself. The full requested amount leaves your balance; the recipient pays the network fee and your withdrawal commission, both deducted from what is sent. Quote first to show the user the net amount.

StatusMeaning
pending_approvalWaiting for you to approve (manual mode, above your automatic limit, or held by a player-protection rule — see hold_reason).
pending_reviewAbove the platform limit; the platform reviews it.
broadcastingSigned and sent to the network (testnets). Wait for confirmation.
completedFinal. tx_hash is set.
cancelled / rejected / failedNothing was sent; the reserved amount returned to your balance.

Approval

Settings → Withdrawals: manual (approve each in the dashboard or with POST /payouts/{id}/approve) or automatic up to a USD limit per withdrawal. Withdrawals without a fresh market price always wait for approval.

Player protection

Without KYC, limits matter. Send customerId with each user withdrawal and configure in Settings: a daily USD limit per player, a maximum number of withdrawals per player per day, and holding the first withdrawal to a new address. A withdrawal that trips a rule is never rejected — it waits for your approval with the reason in hold_reason.

Quote a payout

POST/api/v1/payouts/quote

Network fee, withdrawal commission and the net amount the recipient receives. Sends nothing.

API key · read permission

Body parameters

networkrequiredstringNetwork id.
assetrequiredstringAsset.
amountrequireddecimal stringAmount leaving your balance.

Errors

400Amount does not cover the network fee and withdrawal commission.
Request
curl -X POST "http://localhost:3000/api/v1/payouts/quote" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"network":"tron","asset":"USDT","amount":"50"}'
Response · 200
{
  "network": "tron",
  "asset": "USDT",
  "amount": "50",
  "networkFee": "1",
  "withdrawalFee": "0.25",
  "withdrawalFeeBps": 50,
  "net": "48.75",
  "feeLabel": "SANDBOX FIXTURE: 1 USDT network fee",
  "mode": "sandbox"
}

Create a payout

POST/api/v1/payouts

Reserves the amount and either sends it (automatic approval) or waits for approval. Always send an Idempotency-Key and reuse it when retrying.

API key · payouts permissionIdempotency-Key header required

Body parameters

networkrequiredstringNetwork id.
assetrequiredstringAsset.
amountrequireddecimal stringAmount leaving your balance (the recipient gets net).
addressrequiredstringDestination address, validated for the network.
customerIdstringThe user being paid. Required for per-customer balances and player-protection rules.

Errors

403Destination is on the platform block list.
409Insufficient balance, daily platform limit reached, or the key was reused with different details.
Request
curl -X POST "http://localhost:3000/api/v1/payouts" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: withdrawal-8841" \
  -d '{"network":"tron","asset":"USDT","amount":"50","address":"TXYZ…player-wallet","customerId":"cus_116f187959c5a838e125b339"}'
Response · 201
{
  "id": "out_32310a196885bf86be5926a4",
  "merchant_id": "mch_4e425c9f49d92545a65c80ce",
  "customer_id": "cus_116f187959c5a838e125b339",
  "network": "tron",
  "asset": "USDT",
  "decimals": 6,
  "amount_units": "50000000",
  "address": "TXYZ…player-wallet",
  "status": "pending_approval",
  "idempotency_key": "withdrawal-8841",
  "created_at": "2026-09-24T11:03:41.867Z",
  "completed_at": null,
  "tx_hash": null,
  "network_fee_units": "1000000",
  "withdrawal_fee_units": "250000",
  "net_units": "48750000",
  "usd_cents": "4998",
  "approval": "manual",
  "fee_label": "SANDBOX FIXTURE: 1 USDT network fee",
  "kind": "payout",
  "payment_id": null,
  "reason": null,
  "hold_reason": null,
  "amount": "50",
  "networkFee": "1",
  "withdrawalFee": "0.25",
  "net": "48.75",
  "usdValue": "49.98",
  "mode": "sandbox"
}

Retrieve a payout

GET/api/v1/payouts/{id}

Current status, fees, hold_reason and tx_hash.

API key · read permission

Path parameters

idrequiredstringPayout out_….

Errors

404The id does not exist or belongs to another merchant.
Request
curl "http://localhost:3000/api/v1/payouts/out_32310a196885bf86be5926a4" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "id": "out_32310a196885bf86be5926a4",
  "merchant_id": "mch_4e425c9f49d92545a65c80ce",
  "customer_id": "cus_116f187959c5a838e125b339",
  "network": "tron",
  "asset": "USDT",
  "decimals": 6,
  "amount_units": "50000000",
  "address": "TXYZ…player-wallet",
  "status": "completed",
  "idempotency_key": "withdrawal-8841",
  "created_at": "2026-09-24T11:03:41.867Z",
  "completed_at": "2026-09-24T11:05:02.114Z",
  "tx_hash": "sim_payout_out_32310a19",
  "network_fee_units": "1000000",
  "withdrawal_fee_units": "250000",
  "net_units": "48750000",
  "usd_cents": "4998",
  "approval": "manual",
  "fee_label": "SANDBOX FIXTURE: 1 USDT network fee",
  "kind": "payout",
  "payment_id": null,
  "reason": null,
  "hold_reason": null,
  "amount": "50",
  "networkFee": "1",
  "withdrawalFee": "0.25",
  "net": "48.75",
  "usdValue": "49.98",
  "mode": "sandbox"
}

List payouts

GET/api/v1/payouts

Payouts and refunds, newest first. Poll status=pending_approval to build an approval queue.

API key · read permission

Query parameters

statusstringe.g. pending_approval, completed.
customerIdstringOnly this player/customer.
limitinteger1–200, default 50.
cursorstringNext page.
Request
curl "http://localhost:3000/api/v1/payouts?status=pending_approval" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "out_32310a196885bf86be5926a4",
      "merchant_id": "mch_4e425c9f49d92545a65c80ce",
      "customer_id": "cus_116f187959c5a838e125b339",
      "network": "tron",
      "asset": "USDT",
      "decimals": 6,
      "amount_units": "50000000",
      "address": "TXYZ…player-wallet",
      "status": "pending_approval",
      "idempotency_key": "withdrawal-8841",
      "created_at": "2026-09-24T11:03:41.867Z",
      "completed_at": null,
      "tx_hash": null,
      "network_fee_units": "1000000",
      "withdrawal_fee_units": "250000",
      "net_units": "48750000",
      "usd_cents": "4998",
      "approval": "manual",
      "fee_label": "SANDBOX FIXTURE: 1 USDT network fee",
      "kind": "payout",
      "payment_id": null,
      "reason": null,
      "hold_reason": "Player daily limit of 500 USD exceeded",
      "amount": "50",
      "networkFee": "1",
      "withdrawalFee": "0.25",
      "net": "48.75",
      "usdValue": "49.98",
      "mode": "sandbox"
    }
  ],
  "nextCursor": null
}

Approve a payout

POST/api/v1/payouts/{id}/approve

Approves a pending_approval payout and sends it.

API key · payouts permission

Path parameters

idrequiredstringPayout out_….

Errors

404The id does not exist or belongs to another merchant.
409The payout is no longer pending.
Request
curl -X POST "http://localhost:3000/api/v1/payouts/out_32310a196885bf86be5926a4/approve" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
Response · 200
{
  "id": "out_32310a196885bf86be5926a4",
  "merchant_id": "mch_4e425c9f49d92545a65c80ce",
  "customer_id": "cus_116f187959c5a838e125b339",
  "network": "tron",
  "asset": "USDT",
  "decimals": 6,
  "amount_units": "50000000",
  "address": "TXYZ…player-wallet",
  "status": "completed",
  "idempotency_key": "withdrawal-8841",
  "created_at": "2026-09-24T11:03:41.867Z",
  "completed_at": null,
  "tx_hash": "sim_payout_out_32310a19",
  "network_fee_units": "1000000",
  "withdrawal_fee_units": "250000",
  "net_units": "48750000",
  "usd_cents": "4998",
  "approval": "manual",
  "fee_label": "SANDBOX FIXTURE: 1 USDT network fee",
  "kind": "payout",
  "payment_id": null,
  "reason": null,
  "hold_reason": null,
  "amount": "50",
  "networkFee": "1",
  "withdrawalFee": "0.25",
  "net": "48.75",
  "usdValue": "49.98",
  "mode": "sandbox"
}

Cancel a payout

POST/api/v1/payouts/{id}/cancel

Cancels a payout that has not been sent yet and returns the reserved amount to the balance. Sends payout.cancelled.

API key · payouts permission

Path parameters

idrequiredstringPayout out_….

Errors

404The id does not exist or belongs to another merchant.
409The payout was already sent.
Request
curl -X POST "http://localhost:3000/api/v1/payouts/out_32310a196885bf86be5926a4/cancel" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
Response · 200
{
  "id": "out_32310a196885bf86be5926a4",
  "merchant_id": "mch_4e425c9f49d92545a65c80ce",
  "customer_id": "cus_116f187959c5a838e125b339",
  "network": "tron",
  "asset": "USDT",
  "decimals": 6,
  "amount_units": "50000000",
  "address": "TXYZ…player-wallet",
  "status": "cancelled",
  "idempotency_key": "withdrawal-8841",
  "created_at": "2026-09-24T11:03:41.867Z",
  "completed_at": null,
  "tx_hash": null,
  "network_fee_units": "1000000",
  "withdrawal_fee_units": "250000",
  "net_units": "48750000",
  "usd_cents": "4998",
  "approval": "manual",
  "fee_label": "SANDBOX FIXTURE: 1 USDT network fee",
  "kind": "payout",
  "payment_id": null,
  "reason": null,
  "hold_reason": null,
  "amount": "50",
  "networkFee": "1",
  "withdrawalFee": "0.25",
  "net": "48.75",
  "usdValue": "49.98",
  "mode": "sandbox"
}

Refund a payment

POST/api/v1/payments/{id}/refunds

Sends funds received on an invoice back to the customer, in the same asset and network. Refunds carry no withdrawal commission; the network fee is deducted. Several partial refunds are allowed up to the amount received. GET /payments/{id}/refunds lists them.

API key · payouts permissionIdempotency-Key header required

Path parameters

idrequiredstringPayment pay_….

Body parameters

amountrequireddecimal stringAmount to refund.
addressrequiredstringCustomer’s address.
reasonstringShown in your records (max 200).

Errors

409Nothing left to refund, or the refund exceeds the refundable amount.
Request
curl -X POST "http://localhost:3000/api/v1/payments/pay_c18df35f8fe0c08ccd846672/refunds" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: withdrawal-8841" \
  -d '{"amount":"10","address":"TCustomer…refund","reason":"Item out of stock"}'
Response · 201
{
  "id": "out_61de27325974e48bed220dbb",
  "merchant_id": "mch_4e425c9f49d92545a65c80ce",
  "customer_id": null,
  "network": "tron",
  "asset": "USDT",
  "decimals": 6,
  "amount_units": "10000000",
  "address": "TCustomer…refund",
  "status": "pending_approval",
  "idempotency_key": "refund-1042-1",
  "created_at": "2026-09-24T11:03:41.867Z",
  "completed_at": null,
  "tx_hash": null,
  "network_fee_units": "1000000",
  "withdrawal_fee_units": "0",
  "net_units": "9000000",
  "usd_cents": "999",
  "approval": "manual",
  "fee_label": "SANDBOX FIXTURE: 1 USDT network fee",
  "kind": "refund",
  "payment_id": "pay_c18df35f8fe0c08ccd846672",
  "reason": "Item out of stock",
  "hold_reason": null,
  "amount": "10",
  "networkFee": "1",
  "withdrawalFee": "0",
  "net": "9",
  "usdValue": "9.99",
  "mode": "sandbox"
}

List refunds of a payment

GET/api/v1/payments/{id}/refunds

Refunds of one invoice plus how much is still refundable.

API key · read permission

Path parameters

idrequiredstringPayment pay_….

Errors

404The id does not exist or belongs to another merchant.
Request
curl "http://localhost:3000/api/v1/payments/pay_c18df35f8fe0c08ccd846672/refunds" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "refundable": "39.911414",
  "refunded": "10",
  "data": [
    {
      "id": "out_61de27325974e48bed220dbb",
      "kind": "refund",
      "status": "completed",
      "amount": "10",
      "net": "9",
      "reason": "Item out of stock",
      "…": "…"
    }
  ]
}

Automatic withdrawals

In Balances → Automatic withdrawals (dashboard only, 2FA required) you can sweep your own balance to your wallet: “when TRON USDT reaches 500, send everything above 50 to T…”. The worker checks every minute; platform limits still apply and you get an email whenever the destination changes.

Account statement

GET/api/v1/statement

Every balance movement with a running balance, from the double-entry ledger. GET /statement.csv with the same parameters downloads it as CSV for your accountant.

API key · read permission

Query parameters

fromdateYYYY-MM-DD (inclusive).
todateYYYY-MM-DD (inclusive).
Request
curl "http://localhost:3000/api/v1/statement?from=2026-09-01&to=2026-09-30" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "period": "2026-09-01 → 2026-09-30",
  "lines": [
    {
      "date": "2026-09-24T11:03:41.744Z",
      "reference": "deposit:dep_709ecb039bb5d29ece216883",
      "description": "Deposit received (net of processing fee)",
      "account": "available",
      "customerId": null,
      "network": "tron",
      "asset": "USDT",
      "amount": "49.661856",
      "balance": "1859.478712"
    }
  ]
}

Monthly fee invoice

GET/api/v1/invoices/{month}

Processing, withdrawal and conversion fees charged in a month, per network and asset. The dashboard renders it as a printable invoice.

API key · read permission

Path parameters

monthrequiredstringYYYY-MM.
Request
curl "http://localhost:3000/api/v1/invoices/2026-09" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "number": "QC-202609-4E425C9F",
  "month": "2026-09",
  "periodStart": "2026-09-01T00:00:00.000Z",
  "periodEnd": "2026-10-01T00:00:00.000Z",
  "issuedAt": "2026-09-24T11:03:41.962Z",
  "customer": {
    "id": "mch_4e425c9f49d92545a65c80ce",
    "name": "Orbit Commerce",
    "email": "[email protected]"
  },
  "lines": [
    {
      "description": "Processing fee (0.5%)",
      "network": "tron",
      "asset": "USDT",
      "transactions": 9,
      "amount": "9.094558"
    },
    "…"
  ]
}

Webhooks

Stablora POSTs a JSON event to your endpoint whenever something happens. Set the URL in Settings → Webhook destination and copy the signing secret from Developers.

Event envelope
{
  "id": "evt_56706555c39438cb1106801f",
  "type": "payment.completed",
  "createdAt": "2026-09-24T11:03:41.745Z",
  "mode": "sandbox",
  "data": { "id": "pay_c18df35f8fe0c08ccd846672", "reference": "order-1042", "status": "completed", "amount": "49.911414", "received": "49.911414", "asset": "USDT", "network": "tron", "…": "…" }
}
HeaderValue
Stablora-Signaturet=<unix seconds>,v1=<hex HMAC-SHA256>
Stablora-Event-IdSame as id in the body
Content-Typeapplication/json

Delivery

  • Respond with any 2xx within 5 seconds. Do slow work after responding.
  • Failed deliveries are retried with exponential backoff (10 s, 20 s, 40 s … capped at 1 hour), 8 attempts in total, then marked failed. Replay failed events from the dashboard or with POST /events/{id}/replay.
  • Events can arrive more than once and out of order: store id and ignore repeats; always act on the current state from GET /payments/{id}.
  • Redirects are not followed.

Event types

EventWhendata
payment.completedThe invoice received the full amount (or an amount within your underpayment tolerance).Payment
payment.discrepancyFunds arrived but the invoice is underpaid, overpaid or late. Review before fulfilling.Payment
payment.reversedA chain reorganization removed a credited transfer. The credit was undone; put the order back on hold.Payment + reversedDeposit
payment.heldThe sender is on a sanctions list (OFAC SDN) or the platform blocklist. The funds are frozen for compliance review; do not fulfil. If released you get payment.completed.Payment + reason
deposit.confirmedA transfer to an invoice or permanent customer wallet was credited.Deposit
deposit.heldA transfer to a customer wallet or top-up wallet came from a sanctioned sender and is frozen.Deposit
deposit.reversedA credited transfer to a customer wallet or top-up wallet was reorganized away.Deposit
topup.confirmedYour merchant top-up wallet received funds for the payout pool (no processing fee).Deposit
payout.requestedA payout or refund was created. Check status and hold_reason: it may wait for approval.Payout
payout.broadcastingThe payout was signed and sent to the network (testnets). Wait for payout.completed.Payout
payout.completedThe payout is final. tx_hash is set.Payout
payout.failedThe network rejected the payout. The reserved amount was returned to the balance.Payout
payout.cancelledYou cancelled a pending payout. The reserved amount was returned.Payout
payout.rejectedThe platform rejected a payout held for review. The reserved amount was returned.Payout
swap.queuedLive: an incoming coin is reserved for an on-chain DEX conversion (Uniswap V3 / PancakeSwap V3) on the same network.Swap
swap.completedAn incoming asset was converted to your settlement stablecoin. Live swaps credit the amount actually received, minus the 0.25% markup.Swap
swap.skippedConversion was skipped (no route, no liquidity or no fresh price); funds stay in the received asset.{ reference, reason } or Swap
swap.failedLive: the DEX swap could not be executed; the reserved funds are available again in the received asset.Swap
test.pingSent by "Send test event" in the dashboard to check your endpoint.{ message, sentAt }

Verify signatures

Compute HMAC-SHA256 over "{t}.{raw body}" with your signing secret and compare it with v1 in constant time. Reject timestamps older than 5 minutes. Use the raw request bytes — re-serialized JSON will not match.

Node.js
import { createHmac, timingSafeEqual } from 'node:crypto';

export function verifyStablora(rawBody, header, secret) {
  const m = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header || '');
  if (!m || Math.abs(Date.now() / 1000 - Number(m[1])) > 300) return false;
  const expected = createHmac('sha256', secret).update(`${m[1]}.${rawBody}`).digest();
  return timingSafeEqual(expected, Buffer.from(m[2], 'hex'));
}

// Express: app.post('/webhooks/stablora', express.raw({ type: 'application/json' }), (req, res) => {
//   if (!verifyStablora(req.body.toString('utf8'), req.get('Stablora-Signature'), process.env.STABLORA_WEBHOOK_SECRET)) return res.sendStatus(400);
//   const event = JSON.parse(req.body); /* dedupe event.id, then handle */ res.sendStatus(200);
// });
Python
import hmac, hashlib, re, time

def verify_stablora(raw_body: bytes, header: str, secret: str) -> bool:
    m = re.fullmatch(r"t=(\d+),v1=([a-f0-9]{64})", header or "")
    if not m or abs(time.time() - int(m.group(1))) > 300:
        return False
    expected = hmac.new(secret.encode(), m.group(1).encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, m.group(2))
PHP
function stablora_verify(string $rawBody, string $header, string $secret): bool {
    if (!preg_match('/\At=(\d+),v1=([a-f0-9]{64})\z/', $header, $m)) return false;
    if (abs(time() - (int) $m[1]) > 300) return false;
    $expected = hash_hmac('sha256', $m[1] . '.' . $rawBody, $secret);
    return hash_equals($expected, $m[2]);
}
// $raw = file_get_contents('php://input');
// stablora_verify($raw, $_SERVER['HTTP_STABLORA_SIGNATURE'] ?? '', getenv('STABLORA_WEBHOOK_SECRET'));

The Node SDK ships the same check: stablora.webhooks.verify(rawBody, signature, secret). In the dashboard, Developers → Send test event delivers a signed test.ping to your endpoint.

Reversals & discrepancies

Discrepancies

Underpaid, overpaid and late payments send payment.discrepancy. The funds are credited but the order should not be fulfilled automatically. Options: ask the customer to send the rest (until expiry), accept it, or refund. Set an underpayment tolerance (0–20 %) in Settings so tiny shortfalls caused by wallet fees still complete.

Reversals (chain reorganizations)

On chains without instant finality (EVM networks, Bitcoin) Stablora keeps re-checking a credited transfer until its block is final. If the transfer disappears from the canonical chain, the credit — including the fee and any automatic conversion — is undone and you receive payment.reversed with the updated payment and the reversedDeposit. If the same transfer is confirmed again, it is reinstated and payment.completed is sent again.

  • Put a fulfilled order back on hold; do not ship.
  • Never refund automatically — the customer’s funds did not arrive.
  • If you already paid the funds out, your balance can become negative; this is shown to you and the platform.

Solana (finalized commitment) and TRON (solidified blocks) only credit final transfers, so reversals do not happen there.

Address screening

Every payout destination and every deposit sender is screened against the OFAC SDN list of sanctioned crypto addresses (refreshed daily) and the platform blocklist.

  • A payout or refund to a listed address is refused with 403 and code address_blocked. Nothing is reserved or sent.
  • A deposit from a listed sender is frozen: it appears in your balance as frozen, no fee is charged, no conversion runs, and you receive payment.held (or deposit.held) instead of payment.completed. Do not fulfil the order.
  • The platform reviews it. If released, the deposit is credited normally and payment.completed is sent; if kept frozen, the funds stay out of your balance.

Sandbox: pass a listed address in fromAddresses to POST /test/deposits to see the whole flow.

Inspect an event

GET/api/v1/events/{id}

The webhook inspector: the exact body we sent, the headers, and every delivery attempt with the URL, the signature, your server’s HTTP status, the first 2 KB of its response, the duration and any error. The dashboard shows the same under Developers → Inspect.

API key · read permission

Path parameters

idrequiredstringEvent evt_….

Errors

404The id does not exist or belongs to another merchant.
Request
curl "http://localhost:3000/api/v1/events/evt_56706555c39438cb1106801f" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "id": "evt_56706555c39438cb1106801f",
  "type": "payment.completed",
  "status": "delivered",
  "attempts": 2,
  "created_at": "2026-09-24T11:03:41.745Z",
  "delivered_at": "2026-09-24T11:03:52.310Z",
  "next_attempt_at": null,
  "last_error": null,
  "request": {
    "method": "POST",
    "headers": {
      "Content-Type": "application/json",
      "Stablora-Signature": "t=…,v1=… (recomputed per attempt)",
      "Stablora-Event-Id": "evt_56706555c39438cb1106801f"
    },
    "body": {
      "id": "evt_56706555c39438cb1106801f",
      "type": "payment.completed",
      "…": "…"
    }
  },
  "deliveries": [
    {
      "attempt": 1,
      "url": "https://shop.example/webhooks/stablora",
      "signature": "t=1790249021,v1=4f1c…",
      "statusCode": 500,
      "responseBody": "{\"error\":\"db down\"}",
      "durationMs": 84,
      "error": "Receiver returned HTTP 500.",
      "at": "2026-09-24T11:03:42.100Z"
    },
    {
      "attempt": 2,
      "url": "https://shop.example/webhooks/stablora",
      "signature": "t=1790249032,v1=9ab2…",
      "statusCode": 200,
      "responseBody": "{\"ok\":true}",
      "durationMs": 41,
      "error": null,
      "at": "2026-09-24T11:03:52.310Z"
    }
  ]
}

Replay an event

POST/api/v1/events/{id}/replay

Re-queues a failed or retrying event for immediate delivery (for example after fixing your endpoint).

API key · payments permission

Path parameters

idrequiredstringEvent evt_….

Errors

409Only failed or retrying events can be replayed.
Request
curl -X POST "http://localhost:3000/api/v1/events/evt_56706555c39438cb1106801f/replay" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
Response · 200
{
  "ok": true
}

List events

GET/api/v1/events

Recent events with their delivery status and last error.

API key · read permission

Query parameters

statusstringpending, retry, delivered, failed.
limitinteger1–200, default 50.
cursorstringNext page.
Request
curl "http://localhost:3000/api/v1/events" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "evt_56706555c39438cb1106801f",
      "type": "payment.completed",
      "status": "delivered",
      "attempts": 1,
      "last_error": null,
      "created_at": "2026-09-24T11:03:41.745Z",
      "delivered_at": "2026-09-24T11:03:42.310Z"
    }
  ],
  "nextCursor": null
}

Node.js SDK

@stablora/node — zero dependencies, Node 20+, TypeScript types, automatic retries with idempotency, auto-pagination and webhook verification. Source in sdk/node.

bash
npm install /path/to/stablora/sdk/node
javascript
import Stablora from '@stablora/node';

const stablora = new Stablora({ apiKey: process.env.STABLORA_API_KEY, baseUrl: 'http://localhost:3000/api/v1' });

const payment = await stablora.payments.create({
  reference: 'order-1042', amount: '49.90', currency: 'USD', network: 'tron', asset: 'USDT',
});

// In your webhook handler (raw body!):
const event = stablora.webhooks.verify(rawBody, req.headers['stablora-signature'], process.env.STABLORA_WEBHOOK_SECRET);

MCP server (AI agents)

Let Claude and other MCP clients create invoices, share checkout links and answer “has order 1042 been paid?”. The server can never send, refund or approve payouts. Give it a key with only read + payments.

ToolDoes
create_paymentUSD invoice, returns checkoutUrl
create_checkout_sessionLink where the customer picks the coin
get_payment, list_paymentsStatus and history
get_balances, list_deposits, list_payoutsRead balances and movements
quote_payoutPreview fees (sends nothing)
list_networksSupported networks and assets
verify_webhookCheck a signature
Claude Code
claude mcp add stablora --env STABLORA_API_KEY=qk_test_... -- node /path/to/stablora/sdk/mcp/bin/stablora-mcp.js
Claude Desktop — claude_desktop_config.json
{
  "mcpServers": {
    "stablora": {
      "command": "node",
      "args": ["/path/to/stablora/sdk/mcp/bin/stablora-mcp.js"],
      "env": { "STABLORA_API_KEY": "qk_test_...", "STABLORA_BASE_URL": "http://localhost:3000/api/v1" }
    }
  }
}

E-commerce plugins

PlatformStatusWhere
WooCommerceClassic and block checkout; reversals handledplugins/woocommerce-stablora (installable ZIP)
Paid Memberships ProMembership checkout; level activated only after verified paymentplugins/wordpress-more/pmpro-stablora
Easy Digital DownloadsDownload purchasesplugins/wordpress-more/edd-stablora
PrestaShop 8Payment module with “awaiting” and “reversed” order statesplugins/php-carts (ZIP via tools/package.ps1)
OpenCart 4Payment extension, per-store settingsplugins/php-carts (stablora.ocmod.zip)
Shopify, Ecwid, BigCommerceSelf-hosted bridge service (Node.js)plugins/saas-bridge
All plugins pass their automated tests against mocked platforms. None has been installed in a live store yet — try them on a staging shop first.

Shopify: adding a native crypto option to Shopify checkout requires Shopify’s payments-partner approval. Until then the bridge uses a manual payment method named “Crypto (Stablora)”: the customer gets a pay link on the order status page and in the order email, and the order is marked paid automatically when the payment completes.

Every plugin follows the same rule: an order is marked paid only after a verified webhook and a server-side GET /payments/{id}.

Networks & assets

GET/api/v1/networks

Supported networks, their assets and finality rules. The table below is generated from the live catalogue.

API key · read permission
Network idNameAssetsFamilyType
ethereumEthereumUSDTUSDCETHSHIBPEPEEVMMainnet · live key
baseBaseUSDCETHEVMMainnet · live key
arbitrumArbitrumUSDTUSDCETHEVMMainnet · live key
polygonPolygonUSDTUSDCPOLEVMMainnet · live key
bnbBNB ChainUSDTUSDCBNBEVMMainnet · live key
optimismOptimismUSDTUSDCETHEVMMainnet · live key
avalancheAvalancheUSDTUSDCAVAXEVMMainnet · live key
tronTRONUSDTTRXTRONMainnet · live key
solanaSolanaUSDCUSDTSOLBONKWIFSolanaMainnet · live key
bitcoinBitcoinBTCUTXOMainnet · live key
dogecoinDogecoinDOGEUTXOMainnet · live key
moneroMoneroXMRMoneroMainnet · live key
litecoinLitecoinLTCUTXOMainnet · live key
sepoliaSepolia testnetETHUSDCEVMTestnet · test key (no value)
base-sepoliaBase Sepolia testnetETHUSDCEVMTestnet · test key (no value)
arbitrum-sepoliaArbitrum Sepolia testnetETHUSDCEVMTestnet · test key (no value)
op-sepoliaOP Sepolia testnetETHUSDCEVMTestnet · test key (no value)
polygon-amoyPolygon Amoy testnetPOLUSDCEVMTestnet · test key (no value)
avalanche-fujiAvalanche Fuji testnetAVAXUSDCEVMTestnet · test key (no value)
bnb-testnetBNB Chain testnetBNBEVMTestnet · test key (no value)
tron-nileTRON Nile testnetTRXUSDTTRONTestnet · test key (no value)
solana-devnetSolana devnetSOLUSDCSolanaTestnet · test key (no value)
bitcoin-testnetBitcoin testnetBTCUTXOTestnet · test key (no value)
Request
curl "http://localhost:3000/api/v1/networks" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "ethereum",
      "name": "Ethereum",
      "symbol": "ETH",
      "family": "evm",
      "assets": [
        "USDT",
        "USDC",
        "ETH",
        "SHIB",
        "PEPE"
      ],
      "mode": "sandbox",
      "liveReady": false,
      "finality": {
        "confirmations": 12,
        "note": "Wait for finalized/safe head; handle reorgs before crediting."
      }
    },
    "…"
  ]
}

Errors

Errors return a non-2xx status and a JSON body with a human-readable message; some include a machine-readable code (for example two_factor_required).

json
{ "error": { "message": "Amount must be a positive decimal string." } }
StatusMeaning
400Validation failed: missing field, wrong format, too many decimals, unsupported network/asset.
401Missing or invalid API key or session; or a two-factor code is required.
403Key lacks the permission (read / payments / payouts), IP not allowed, blocked address, or dashboard-only route.
404Not found — including resources that belong to another merchant.
409Conflict: idempotency key reused with different details, insufficient balance, limit reached, already settled.
410The checkout expired. Create a new one.
413Request body larger than 16 KB.
415Send Content-Type: application/json.
429Rate limit exceeded (300 requests per minute per merchant; lower on public endpoints). Retry after a minute.
500Unexpected error. Safe to retry GET requests and requests with an Idempotency-Key.
503No fresh market price to lock a USD amount. Retry shortly.