API reference

Stablora 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, TON, 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, ton, 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",
  "expiresAt": "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": "TJ8vYx1kR3qmHf2nWbE6uPzC9aLdS4tGoQ",
  "refunded": "0",
  "mode": "live",
  "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": "Coinbase"
  }
}

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",
  "expiresAt": "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": "TJ8vYx1kR3qmHf2nWbE6uPzC9aLdS4tGoQ",
  "refunded": "0",
  "mode": "live",
  "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": "Coinbase"
  }
}

List payments

GET/api/v1/payments

Invoices, newest first.

API key · read permission

Query parameters

statusstringFilter by status.
customerIdstringOnly this customer.
checkoutSessionIdstringEvery invoice picked on one checkout session (cs_…). Plugins use it to settle a missed webhook when the customer returns.
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",
      "expiresAt": "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": "TJ8vYx1kR3qmHf2nWbE6uPzC9aLdS4tGoQ",
      "refunded": "0",
      "mode": "live",
      "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": "Coinbase"
      }
    }
  ],
  "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": "Coinbase"
}

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: the same reference returns the same session, renewed if it expired unpaid.
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"
}

Checkout session payments

When the customer picks a coin on a checkout session, Stablora creates a normal locked invoice for that option. Its reference is your reference plus #<network>:<asset> (and #<suffix> if an expired option is picked again), and it carries the session id and the price in your currency:

GET /payments/{id}
{
  "id": "pay_…",
  "reference": "order-1042#solana:USDC",
  "checkoutSessionId": "cs_cc25b6efa956b4911ea9920a",
  "network": "solana", "asset": "USDC", "amount": "49.9", "status": "completed", "mode": "live",
  "pricing": { "currency": "USD", "displayAmount": "49.90", "usdAmount": "49.90", "locked": true }
}

Mark your order paid only when all of these hold for the payment you fetched with GET /payments/{id}:

  • the reference before the first # is your order reference;
  • checkoutSessionId is the session you created for that order;
  • pricing.currency and pricing.displayAmount equal your order currency and total;
  • status is completed and mode matches your key (live for qk_live_, sandbox/testnet for qk_test_);
  • you have not recorded this payment id before (use it as your transaction id).

A customer may pick one coin, change their mind and pay another. Bind the order to the first completed payment; if a second option is paid too, flag it for a refund — never count it twice. The PHP SDK does these checks in Stablora\Checkout::sessionPaymentMatches(); every Stablora plugin applies the same rules.

Simulate a deposit (local sandbox only)

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": "live"
}

Simulate a reorg (local sandbox only)

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": "live"
}

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": "TQm4sN7bWx2eCz9RkV1hF6pLgUa3YdJ8oX",
  "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": "TQm4sN7bWx2eCz9RkV1hF6pLgUa3YdJ8oX",
      "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"
    }
  ]
}

Cashier widget

A hosted deposit and withdrawal window for your players (casinos, games, marketplaces). Your server opens a short-lived session for a player; your page shows it with cashier.js. Deposits go to the player’s permanent wallets; withdrawals are requests that your server confirms or rejects — the widget can never move money by itself.

Your page
<script src="https://stablora.xyz/cashier.js"></script>
<script>
  // url from POST /v1/cashier/sessions on your server
  Stablora.openCashier(url, { onClose: refreshBalance });      // modal
  // Stablora.mountCashier('#cashier', url);                   // inside your page
  // Stablora.popupCashier(url);                               // new window
</script>
Your server (Node SDK)
import Stablora from '@stablora/node'; // 0.3.0 or later

const stablora = new Stablora({ apiKey: process.env.STABLORA_API_KEY });

// Each time the player opens the cashier:
const { url } = await stablora.cashier.sessions.create({ externalId: player.id });

// On the signed cashier.withdrawal_requested webhook, after debiting the player:
await stablora.cashier.withdrawals.confirm(event.data.id);
// or: await stablora.cashier.withdrawals.reject(event.data.id, { reason: 'Wagering not met' });
  • Credit players on the signed deposit.confirmed webhook (customer_id), never on browser events.
  • On cashier.withdrawal_requested: debit the player, then confirm (it becomes a payout with your limits, holds and approval rules) or reject with a reason the player sees.
  • Requests nobody answers expire after 24 hours (cashier.withdrawal_expired); players can cancel waiting ones (cashier.withdrawal_cancelled). At most 3 can wait per player.
  • Allowed sites (Dashboard → Cashier) stop the widget from running inside any other site. A test key opens a test cashier, a live key a live one.
  • Optional minimum and maximum withdrawal in USD (Dashboard → Cashier), checked at the live price of the coin; the cashier shows them to the player.
  • Seven languages (English, Türkçe, Español, Português, Русский, Deutsch, Français): pass locale or let the player choose. Show the player’s balance with balance and keep it fresh with POST /cashier/balances.

Open a cashier session

POST/api/v1/cashier/sessions

Opens the cashier for one player and returns its url. Call it from your server each time the player opens the cashier; give the url to the browser only.

API key · payments permission

Body parameters

externalIdstringYour player id. The customer is created on first use (or pass customerId).
customerIdstringAn existing customer cus_… instead of externalId.
namestringDisplay name for a new customer.
withdrawalsbooleanShow the Withdraw tab (default true; off when disabled in your cashier settings).
expiresInMinutesinteger5–120, default 30.
themestringdark (default) or light.
localestringWidget language: en, tr, es, pt, ru, de or fr. Default: the player’s browser language. Players can switch it in the cashier.
balanceobject{ amount, currency } — the player’s balance in your system, shown in the cashier header (display only, e.g. { "amount": "125.40", "currency": "USD" }). Update it with POST /cashier/balances.
Request
curl -X POST "http://localhost:3000/api/v1/cashier/sessions" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"externalId":"player-8841","name":"Alex Morgan","locale":"en","balance":{"amount":"125.40","currency":"USD"}}'
Response · 201
{
  "id": "csh_9d2e4b7a1c0f3e5d8b6a2c4e",
  "url": "https://stablora.xyz/cashier#cst_…",
  "customerId": "cus_116f187959c5a838e125b339",
  "externalId": "player-8841",
  "withdrawals": true,
  "expiresAt": "2026-10-12T16:30:00.000Z",
  "createdAt": "2026-10-12T16:00:00.000Z"
}

Show a player balance

POST/api/v1/cashier/balances

Sets the balance the cashier shows for a player — for example right after you credited a deposit. Display only: Stablora never uses it to decide anything. Open cashiers pick it up within seconds.

API key · payments permission

Body parameters

externalIdstringYour player id (or pass customerId).
customerIdstringThe customer cus_….
amountrequiredstringDecimal string, e.g. "175.40".
currencyrequiredstring1–12 letters, digits or a currency sign: USD, EUR, Coins.
balancenullSend "balance": null instead of amount/currency to hide it.

Errors

404No customer with this externalId yet (open a cashier session first).
Request
curl -X POST "http://localhost:3000/api/v1/cashier/balances" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"externalId":"player-8841","amount":"175.40","currency":"USD"}'
Response · 200
{
  "customerId": "cus_116f187959c5a838e125b339",
  "externalId": "player-8841",
  "balance": {
    "amount": "175.40",
    "currency": "USD",
    "updatedAt": "2026-10-12T16:07:00.000Z"
  }
}

List withdrawal requests

GET/api/v1/cashier/withdrawals

Withdrawal requests from the cashier, newest first.

API key · read permission

Query parameters

statusstringpending, confirmed, rejected, cancelled or expired.
customerIdstringOnly this customer.
limitinteger1–200, default 50.
Request
curl "http://localhost:3000/api/v1/cashier/withdrawals?status=pending" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "cwr_5c1f0e9a7b3d2c4e6f8a0b1c",
      "status": "pending",
      "network": "tron",
      "asset": "USDT",
      "amount": "60",
      "address": "TXYZ…player-wallet",
      "received": null,
      "txHash": null,
      "reason": null,
      "expiresAt": "2026-10-13T16:05:00.000Z",
      "createdAt": "2026-10-12T16:05:00.000Z",
      "resolvedAt": null,
      "customerId": "cus_116f187959c5a838e125b339",
      "externalId": "player-8841",
      "payoutId": null,
      "sessionId": "csh_9d2e4b7a1c0f3e5d8b6a2c4e"
    }
  ]
}

Get a withdrawal request

GET/api/v1/cashier/withdrawals/{id}

One withdrawal request. After confirmation status follows the payout: processing, completed, failed or rejected.

API key · read permission

Path parameters

idrequiredstringRequest cwr_….

Errors

404The id does not exist or belongs to another merchant.
Request
curl "http://localhost:3000/api/v1/cashier/withdrawals/cwr_5c1f0e9a7b3d2c4e6f8a0b1c" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "id": "cwr_5c1f0e9a7b3d2c4e6f8a0b1c",
  "status": "processing",
  "network": "tron",
  "asset": "USDT",
  "amount": "60",
  "address": "TXYZ…player-wallet",
  "received": "58.7",
  "customerId": "cus_116f187959c5a838e125b339",
  "externalId": "player-8841",
  "payoutId": "out_32310a196885bf86be5926a4"
}

Confirm a withdrawal request

POST/api/v1/cashier/withdrawals/{id}/confirm

Call this after you debited the player. The request becomes a payout (idempotent: confirming twice returns the same payout) and follows your payout approval rules and player limits.

API key · payouts permission

Path parameters

idrequiredstringRequest cwr_….

Errors

404The id does not exist or belongs to another merchant.
409The request was cancelled, rejected or expired, or your balance is too low.
403The destination is on a sanctions list; the request was rejected.
Request
curl -X POST "http://localhost:3000/api/v1/cashier/withdrawals/cwr_5c1f0e9a7b3d2c4e6f8a0b1c/confirm" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{}'
Response · 200
{
  "withdrawal": {
    "id": "cwr_5c1f0e9a7b3d2c4e6f8a0b1c",
    "status": "processing",
    "payoutId": "out_32310a196885bf86be5926a4"
  },
  "payout": {
    "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": "1 USDT network fee (tron)",
    "kind": "payout",
    "payment_id": null,
    "reason": null,
    "hold_reason": null,
    "amount": "50",
    "networkFee": "1",
    "withdrawalFee": "0.25",
    "net": "48.75",
    "usdValue": "49.98",
    "mode": "live"
  }
}

Reject a withdrawal request

POST/api/v1/cashier/withdrawals/{id}/reject

Refuses a waiting request; the player sees your reason in the cashier.

API key · payouts permission

Path parameters

idrequiredstringRequest cwr_….

Body parameters

reasonstringShown to the player (max 200 characters).

Errors

404The id does not exist or belongs to another merchant.
409Only a waiting request can be rejected.
Request
curl -X POST "http://localhost:3000/api/v1/cashier/withdrawals/cwr_5c1f0e9a7b3d2c4e6f8a0b1c/reject" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reason":"Wagering requirement not met"}'
Response · 200
{
  "id": "cwr_5c1f0e9a7b3d2c4e6f8a0b1c",
  "status": "rejected",
  "reason": "Wagering requirement not met"
}

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": "live"
    }
  ],
  "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": "TXr2cK9mZp5vQe1wHs7bN3yGfUj6LdA4oT",
  "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": "TXr2cK9mZp5vQe1wHs7bN3yGfUj6LdA4oT",
      "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": "1 USDT network fee (tron)",
  "mode": "live"
}

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": "1 USDT network fee (tron)",
  "kind": "payout",
  "payment_id": null,
  "reason": null,
  "hold_reason": null,
  "amount": "50",
  "networkFee": "1",
  "withdrawalFee": "0.25",
  "net": "48.75",
  "usdValue": "49.98",
  "mode": "live"
}

Create a batch of payouts

POST/api/v1/payouts/batch

Up to 100 payouts in one request (affiliates, streamers, many player withdrawals). All or nothing: if any payout fails validation, screening, balance or limits, none is created. Payout i uses the idempotency key <your key>:<i>, so a retry with the same key returns the same payouts. Each payout then follows your approval rules.

API key · payouts permissionIdempotency-Key header required

Body parameters

payoutsrequiredarrayItems with network, asset, amount, address and optionally customerId or externalId (the player).

Errors

400Payout N: … — item N is invalid (address, amount, unknown customer), or more than 100 items.
403Payout N: … — destination on the block list (nothing created).
409Payout N: … — insufficient balance or limit reached, or the key was reused with different items (nothing created).
Request
curl -X POST "http://localhost:3000/api/v1/payouts/batch" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: withdrawal-8841" \
  -d '{"payouts":[{"network":"tron","asset":"USDT","amount":"25","address":"TXYZ…affiliate-1"},{"network":"tron","asset":"USDT","amount":"40","address":"TXYZ…affiliate-2","externalId":"player-8841"}]}'
Response · 201
{
  "id": "pob_3e9a1c7d5b2f4e6a8c0d1b3f",
  "count": 2,
  "createdAt": "2026-10-12T16:00:00.000Z",
  "statuses": {
    "pending_approval": 2
  },
  "totals": [
    {
      "network": "tron",
      "asset": "USDT",
      "amount": "65"
    }
  ],
  "payouts": [
    {
      "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": "1 USDT network fee (tron)",
      "kind": "payout",
      "payment_id": null,
      "reason": null,
      "hold_reason": null,
      "amount": "50",
      "networkFee": "1",
      "withdrawalFee": "0.25",
      "net": "48.75",
      "usdValue": "49.98",
      "mode": "live"
    }
  ]
}

Get a payout batch

GET/api/v1/payouts/batches/{id}

A batch with its payouts, a count per status and totals per network and asset.

API key · read permission

Path parameters

idrequiredstringBatch pob_….

Errors

404The id does not exist or belongs to another merchant.
Request
curl "http://localhost:3000/api/v1/payouts/batches/pob_3e9a1c7d5b2f4e6a8c0d1b3f" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "id": "pob_3e9a1c7d5b2f4e6a8c0d1b3f",
  "count": 2,
  "statuses": {
    "completed": 2
  },
  "totals": [
    {
      "network": "tron",
      "asset": "USDT",
      "amount": "65"
    }
  ],
  "payouts": [
    {
      "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": "1 USDT network fee (tron)",
      "kind": "payout",
      "payment_id": null,
      "reason": null,
      "hold_reason": null,
      "amount": "50",
      "networkFee": "1",
      "withdrawalFee": "0.25",
      "net": "48.75",
      "usdValue": "49.98",
      "mode": "live"
    }
  ]
}

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": "1 USDT network fee (tron)",
  "kind": "payout",
  "payment_id": null,
  "reason": null,
  "hold_reason": null,
  "amount": "50",
  "networkFee": "1",
  "withdrawalFee": "0.25",
  "net": "48.75",
  "usdValue": "49.98",
  "mode": "live"
}

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": "1 USDT network fee (tron)",
      "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": "live"
    }
  ],
  "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": "1 USDT network fee (tron)",
  "kind": "payout",
  "payment_id": null,
  "reason": null,
  "hold_reason": null,
  "amount": "50",
  "networkFee": "1",
  "withdrawalFee": "0.25",
  "net": "48.75",
  "usdValue": "49.98",
  "mode": "live"
}

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": "1 USDT network fee (tron)",
  "kind": "payout",
  "payment_id": null,
  "reason": null,
  "hold_reason": null,
  "amount": "50",
  "networkFee": "1",
  "withdrawalFee": "0.25",
  "net": "48.75",
  "usdValue": "49.98",
  "mode": "live"
}

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": "1 USDT network fee (tron)",
  "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": "live"
}

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. Need more than one destination (a store plus WHMCS plus n8n)? Add webhook endpoints: each gets every event, its own signing secret and its own retries.

Event envelope
{
  "id": "evt_56706555c39438cb1106801f",
  "type": "payment.completed",
  "createdAt": "2026-09-24T11:03:41.745Z",
  "mode": "live",
  "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
cashier.withdrawal_requestedA player asked for a withdrawal in the cashier widget. Nothing was paid: debit the player in your system, then confirm it (POST /v1/cashier/withdrawals/{id}/confirm) or reject it.Cashier withdrawal + estimate
cashier.withdrawal_cancelledThe player cancelled a waiting withdrawal request. Release anything you reserved for it.Cashier withdrawal
cashier.withdrawal_expiredA withdrawal request was not answered within 24 hours and expired. Release anything you reserved for it.Cashier withdrawal
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.

Webhook endpoints

Extra destinations next to your account webhook. Every event is delivered to each active endpoint, signed with that endpoint's own secret and retried independently (8 attempts). Add them in Developers → Webhook endpoints or with the API below; automation tools (n8n, Zapier, Make) register theirs when a workflow is switched on and remove it when switched off.

  • Stores connected with Connect with Stablora get their own endpoint automatically, signed with the account secret they received. Connecting another store never silences the first.
  • An endpoint with the same URL as your account webhook is skipped, so nothing is delivered twice.
  • The event inspector lists every attempt for every destination.
  • Up to 25 endpoints per account.

Add a webhook endpoint

POST/api/v1/webhook-endpoints

Registers a destination. The response contains its signing secret — shown only once; store it to verify deliveries.

API key · payments permission

Body parameters

urlrequiredstringPublic https:// URL (no credentials, no #fragment).
events"*" | string[]Event types to deliver, e.g. ["payment.completed"]. Default "*" (all).
labelstringShown in the dashboard (max 80).

Errors

400Invalid or private URL, or unknown event type format.
409Already 25 active endpoints.
Request
curl -X POST "http://localhost:3000/api/v1/webhook-endpoints" \
  -H "Authorization: Bearer $STABLORA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url":"https://billing.example.com/modules/gateways/callback/stablora.php","events":["payment.completed","payment.discrepancy","payment.reversed"],"label":"WHMCS"}'
Response · 201
{
  "id": "we_3f1c0b8e2d4a5f6e7d8c9b0a",
  "url": "https://billing.example.com/modules/gateways/callback/stablora.php",
  "events": [
    "payment.completed",
    "payment.discrepancy",
    "payment.reversed"
  ],
  "label": "WHMCS",
  "connectionId": null,
  "createdAt": "2026-09-29T10:24:09.806Z",
  "active": true,
  "secret": "whsec_…"
}

List webhook endpoints

GET/api/v1/webhook-endpoints

Active endpoints, including those created for connected stores (connectionId). Secrets are never returned here.

API key · read permission
Request
curl "http://localhost:3000/api/v1/webhook-endpoints" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "data": [
    {
      "id": "we_3f1c0b8e2d4a5f6e7d8c9b0a",
      "url": "https://billing.example.com/modules/gateways/callback/stablora.php",
      "events": [
        "payment.completed"
      ],
      "label": "WHMCS",
      "connectionId": null,
      "createdAt": "2026-09-29T10:24:09.806Z",
      "active": true
    }
  ]
}

Delete a webhook endpoint

DELETE/api/v1/webhook-endpoints/{id}

Stops deliveries to the endpoint immediately; pending retries are cancelled.

API key · payments permission

Path parameters

idrequiredstringEndpoint we_….

Errors

404The id does not exist or belongs to another merchant.
Request
curl -X DELETE "http://localhost:3000/api/v1/webhook-endpoints/we_3f1c0b8e2d4a5f6e7d8c9b0a" \
  -H "Authorization: Bearer $STABLORA_API_KEY"
Response · 200
{
  "id": "we_3f1c0b8e2d4a5f6e7d8c9b0a",
  "deleted": true
}

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

npm install @stablora/node — zero dependencies, Node 20+, TypeScript types, automatic retries with idempotency, auto-pagination and webhook verification. Defaults to http://localhost:3000/api/v1; API keys are only sent over https (plain http only for localhost).

bash
npm install @stablora/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);

PHP SDK

One dependency-free file (sdk/php/src/Stablora.php, PHP 7.4+ with curl): checkouts, payments, payment links, customers and wallets, payouts, webhook endpoints, and webhook verification. Download it from Integrations.

Checkout
require 'stablora-php/src/Stablora.php';

$stablora = new Stablora\Client(getenv('STABLORA_API_KEY'), 'http://localhost:3000/api/v1');
$session = $stablora->createCheckoutSession([
    'reference' => 'order-1042', 'amount' => '49.90', 'currency' => 'USD',
    'successUrl' => 'https://shop.example/orders/1042',
]);
header('Location: ' . $stablora->checkoutUrl($session['id']), true, 303);
Webhook
$event = Stablora\Webhook::verify(file_get_contents('php://input'), $_SERVER['HTTP_STABLORA_SIGNATURE'] ?? null,
    getenv('STABLORA_WEBHOOK_SECRET'), $_SERVER['HTTP_STABLORA_EVENT_ID'] ?? null);
$payment = $stablora->getPayment(Stablora\Webhook::paymentId($event));
$order = Stablora\Checkout::referenceBase($payment['reference']);
if (Stablora\Checkout::isPaid($payment) && Stablora\Checkout::sessionPaymentMatches($payment, $order, $savedSessionId, 'USD', $orderTotal, getenv('STABLORA_API_KEY'))) {
    // mark $order paid once, using $payment['id'] as the transaction id
}

Python SDK

One dependency-free module (Python 3.8+, standard library only): checkouts, payments, payment links, customers and wallets, payouts, webhook endpoints, webhook verification, and StoreFlow — the order flow the Stablora shop modules use. Download it from Integrations.

Checkout
import os, stablora

client = stablora.Client(os.environ['STABLORA_API_KEY'], 'http://localhost:3000/api/v1')
session = client.create_checkout_session(
    reference='order-1042', amount='49.90', currency='USD',
    success_url='https://shop.example/orders/1042')
redirect_to = client.checkout_url(session['id'])
Webhook
# Flask: verify over the raw body, then read the payment from the API
event = stablora.Webhook.verify(request.get_data(), request.headers.get('Stablora-Signature'),
                                os.environ['STABLORA_WEBHOOK_SECRET'], request.headers.get('Stablora-Event-Id'))
payment = client.get_payment(stablora.Webhook.payment_id(event))
if stablora.Checkout.is_paid(payment):
    order = stablora.Checkout.reference_base(payment['reference'])
    # mark order paid once, using payment['id'] as the transaction id

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" }
    }
  }
}

Plugins & integrations

Download and install guides: Integrations. Store plugins let the customer choose any coin and network by default (setting Coins offered).

PlatformWhat it doesSetup
WooCommerceClassic and block checkout; reversals handledConnect with Stablora (one click)
Paid Memberships ProMembership checkout; level activated only after verified paymentConnect with Stablora
Gravity Forms 2.5+Payment add-on: any form total, entry Paid after a verified payment, Payment Completed notificationsAPI key + webhook endpoint
Easy Digital DownloadsDownload purchasesConnect with Stablora
PrestaShop 8Payment module with “awaiting” and “reversed” order statesConnect with Stablora
OpenCart 4Payment extension, per-store settingsConnect with Stablora
Magento 2.4Payment method; invoice on verified payment, hold on reversalAPI key + webhook endpoint
Shopware 6.7Payment handler; transaction paid on verified payment, chargeback on reversalAPI key + webhook endpoint
CS-Cart 4.15+Payment processor; order Processed on verified paymentAPI key + webhook endpoint
nopCommerce 5.00Redirection payment method (source release, build in your solution)API key + webhook endpoint
Drupal Commerce 2/3Off-site gateway; payment recorded from the verified webhookAPI key + webhook endpoint
Medusa v2Payment Module provider; captured by the verified webhookmedusa-config options + webhook endpoint
Vendure 3Payment handler + createStabloraCheckout mutation; settled by the verified webhookAPI key + webhook endpoint
WHMCS 8Invoices paid with any coin; Gateway Log for review casesAPI key + webhook endpoint
FOSSBilling 0.8+Invoices paid with any coin; a missed webhook is settled when the client returnsAPI key + webhook endpoint
Blesta 5Non-merchant gateway; transactions from verified payments, void on reversalAPI key + webhook endpoint
DiscordSelf-hosted bot: /buy sells roles (permanent or for N days); role added after a verified payment.env + products.json + webhook endpoint
TelegramSelf-hosted shop bot for physical goods and services (digital goods must use Telegram Stars); paid orders to your admin chat.env + products.json + webhook endpoint
pretix 2024.1+Payment plugin (self-hosted): ticket orders paid after a verified payment; reversals flag check-in attentionAPI key + webhook endpoint
Moodle 4.1+Payment gateway (paygw): course fees and other Moodle payments; enrolment after a verified paymentAPI key + webhook endpoint
OBS Studio (Twitch, YouTube, Kick)Built in: public donate page, alert and goal overlays as browser sources, custom sound/GIF/text, test alertsDashboard → Donations; nothing to install
XenForo 2.2+User upgrades; reversed on a chain reorganizationAPI key + webhook endpoint
n8nAction node (checkouts, payments, wallets) and a verified payment triggerAPI key; the trigger registers its own webhook
Odoo 18Payment provider for eCommerce, invoices and payment links; order confirmed after a verified paymentAPI key + webhook endpoint
BubbleServer-side actions: create checkout, check checkout on the return page, verify webhook paymentsPrivate plugin from our source (marketplace listing pending) or API Connector
Zapier, Make, ActivepiecesCreate checkouts; start automations on verified paymentsAPI key; triggers register their own webhook (Zapier and Make need our app listing first)
Shopify, Ecwid, BigCommerceSelf-hosted bridge service (Node.js).env; see the platform notes below
Node.js / PHP / Python SDK, MCP serverBuild your own integrationAPI key
WooCommerce is in use on live stores. The other integrations pass automated tests against a real Stablora and their platform APIs (n8n was also run in a real n8n 1.x); try them on a staging site before taking real payments.

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

Live networks — use with a live key (qk_live_…); real funds.

Network idNameAssetsFamily
ethereumEthereumUSDTUSDCETHSHIBPEPEEVM
baseBaseUSDCETHEVM
arbitrumArbitrumUSDTUSDCETHEVM
polygonPolygonUSDTUSDCPOLEVM
bnbBNB ChainUSDTUSDCBNBEVM
optimismOptimismUSDTUSDCETHEVM
avalancheAvalancheUSDTUSDCAVAXEVM
tronTRONUSDTTRXTRON
tonTONUSDTTONTON
solanaSolanaUSDCUSDTSOLBONKWIFSolana
bitcoinBitcoinBTCUTXO
dogecoinDogecoinDOGEUTXO
moneroMoneroXMRMonero
litecoinLitecoinLTCUTXO
Testnets for integration tests — test key (qk_test_…), free coins with no value (10)
Network idNameAssetsFamily
sepoliaSepolia testnetETHUSDCEVM
base-sepoliaBase Sepolia testnetETHUSDCEVM
arbitrum-sepoliaArbitrum Sepolia testnetETHUSDCEVM
op-sepoliaOP Sepolia testnetETHUSDCEVM
polygon-amoyPolygon Amoy testnetPOLUSDCEVM
avalanche-fujiAvalanche Fuji testnetAVAXUSDCEVM
bnb-testnetBNB Chain testnetBNBEVM
tron-nileTRON Nile testnetTRXUSDTTRON
solana-devnetSolana devnetSOLUSDCSolana
bitcoin-testnetBitcoin testnetBTCUTXO
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": "live",
      "liveReady": true,
      "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.