Stablora API reference
http://localhost:3000/api/v1Introduction
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 URL | http://localhost:3000/api/v1 |
| Format | JSON over HTTPS. Amounts are decimal strings, never floats. |
| Auth | Authorization: Bearer qk_live_… (live) or qk_test_… (testnets) |
| Webhooks | Signed with HMAC-SHA256 (Stablora-Signature) |
| Machine-readable | OpenAPI 3.1 · llms.txt |
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.completedwebhook. - 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)
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:
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.
Authorization: Bearer qk_live_3f9c…| Key | Reaches | Use for |
|---|---|---|
qk_live_… | Live mainnets only — real funds | Your production store or app |
qk_test_… | Testnets only — coins without value | Development, 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
| Permission | Allows |
|---|---|
read | All GET requests and POST /payouts/quote. |
payments | Create customers, wallets, invoices, checkout sessions, payment links, test deposits. |
payouts | Create, 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.
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.
| Live | Testnets | Sandbox (local install) | |
|---|---|---|---|
| Key | qk_live_… | qk_test_… | qk_test_… |
| Networks | tron, ton, bnb, ethereum, polygon, arbitrum, base, optimism, avalanche, bitcoin, litecoin, solana, dogecoin, monero | sepolia, base-sepolia, tron-nile, solana-devnet, bitcoin-testnet… | The mainnet ids, simulated |
| Addresses | A real deposit address per invoice | Real testnet addresses | Simulated qtest_… identifiers |
| Deposits | Real transfers, credited after confirmations | Faucet coins, credited after confirmations | POST /test/deposits, instant |
| Payouts | Real signed transactions | Real signed testnet transactions | Simulated, 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
amounttoPOST /test/deposits. - Late: pay after
expires_at. - Duplicate delivery: repeat the same
transactionHash— it is never credited twice. - Chain reorganization:
POST /test/deposits/{id}/reversesendspayment.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
| Prefix | Object |
|---|---|
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
/api/v1/paymentsCreates 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.
Body parameters
referencerequiredstring | Your unique order id (max 120). Repeating it returns the same invoice. |
amountrequireddecimal string | Fiat 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. |
networkrequiredstring | Network id, e.g. tron, ethereum, solana, sepolia. See Networks & assets. |
assetrequiredstring | Asset on that network, e.g. USDT. |
descriptionstring | Shown on the checkout page (max 200). |
customerIdstring | Attach the invoice to a customer (cus_…). |
expirationMinutesinteger | 5–1440, default 60. Funds after expiry are still credited as late. |
paymentUrlis relative: combine it with the Stablora origin you trust.- If you set a processing-fee payer of
customerin Settings,amountis grossed up andbaseAmountis what you net.
Errors
400 | Unknown network/asset or invalid amount. |
409 | Reference already used with different details. |
503 | No fresh market price for a USD invoice — retry shortly. |
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"}'{
"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
/api/v1/payments/{id}The authoritative state of an invoice. Re-read it after a webhook before fulfilling.
Path parameters
idrequiredstring | Payment id pay_…. |
Errors
404 | The id does not exist or belongs to another merchant. |
curl "http://localhost:3000/api/v1/payments/pay_c18df35f8fe0c08ccd846672" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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
/api/v1/paymentsInvoices, newest first.
Query parameters
statusstring | Filter by status. |
customerIdstring | Only this customer. |
checkoutSessionIdstring | Every invoice picked on one checkout session (cs_…). Plugins use it to settle a missed webhook when the customer returns. |
limitinteger | 1–200, default 50. |
cursorstring | nextCursor from the previous page. |
curl "http://localhost:3000/api/v1/payments?status=completed&limit=20" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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
| Status | Meaning | Fulfil? |
|---|---|---|
pending | Waiting for funds. | No |
completed | Full amount received (or within your underpayment tolerance). | Yes |
underpaid | Less than the amount arrived. More can still be sent until expiry. | Review |
overpaid | More than the amount arrived. The surplus is in your balance; refund it if needed. | Yes, then refund surplus |
late | Funds arrived after expires_at. They are credited but flagged. | Review |
expired | Nothing arrived before expires_at. | No |
reversed | A chain reorganization undid the credit. See Reversals. | No — hold the order |
held | Funds 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
/api/v1/quotesShows 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.
Body parameters
networkrequiredstring | Network id. |
assetrequiredstring | Asset. |
usdAmountrequireddecimal string | USD amount. |
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"}'{
"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
/api/v1/checkout-sessionsA 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.
Body parameters
referencerequiredstring | Your order id (max 100). Idempotent: the same reference returns the same session, renewed if it expired unpaid. |
amountrequireddecimal string | Amount in currency. |
currency"USD" | "EUR" | "GBP" | Default USD. |
descriptionstring | Shown to the customer. |
optionsarray | List 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). |
successUrlstring | Where the “Return to store” button leads after payment. |
customerIdstring | Attach resulting invoices to a customer. |
expirationMinutesinteger | 5–1440, default 60. |
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"}]}'{
"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:
{
"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; checkoutSessionIdis the session you created for that order;pricing.currencyandpricing.displayAmountequal your order currency and total;statusiscompletedandmodematches your key (liveforqk_live_,sandbox/testnetforqk_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.
Create a payment link
/api/v1/payment-linksA reusable public link (/l/{slug}) for donations, social media sales or a fixed-price product. Every visitor gets their own checkout session, invoice and deposit address. Payments carry the reference link:{slug}:….
Body parameters
titlerequiredstring | Shown on the link page (max 100). |
amountdecimal string | Fixed USD price. Omit to let the buyer choose. |
minAmountdecimal string | Lowest amount when the buyer chooses (default 1). |
maxAmountdecimal string | Highest amount when the buyer chooses (default 10000, max 1,000,000). |
descriptionstring | Optional text (max 200). |
optionsarray | Coins to offer, as for checkout sessions. |
successUrlstring | Return URL after payment. |
curl -X POST "http://localhost:3000/api/v1/payment-links" \
-H "Authorization: Bearer $STABLORA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title":"Support the stream","minAmount":"2","maxAmount":"500"}'{
"id": "pl_1e7aac13b02f2ca4635607cf",
"slug": "xnt1_yKaKiAH",
"url": "/l/xnt1_yKaKiAH",
"title": "Support the stream",
"description": null,
"active": true,
"amount": null,
"minAmount": "2",
"maxAmount": "500",
"options": [
{
"network": "tron",
"asset": "USDT"
},
"…"
],
"successUrl": null,
"sessions": 0,
"paidCount": 0,
"paidUsd": "0",
"created_at": "2026-09-24T11:03:41.820Z"
}List payment links
/api/v1/payment-linksAll links with usage statistics: sessions started, paid count and paid USD.
curl "http://localhost:3000/api/v1/payment-links" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"data": [
{
"id": "pl_1e7aac13b02f2ca4635607cf",
"slug": "xnt1_yKaKiAH",
"url": "/l/xnt1_yKaKiAH",
"title": "Support the stream",
"active": true,
"amount": null,
"minAmount": "2",
"maxAmount": "500",
"sessions": 14,
"paidCount": 9,
"paidUsd": "212.5"
}
]
}Deactivate a payment link
/api/v1/payment-links/{id}/deactivateStops the link from accepting new buyers. Invoices already started are not affected.
Path parameters
idrequiredstring | Link id pl_…. |
Errors
404 | The id does not exist or belongs to another merchant. |
curl -X POST "http://localhost:3000/api/v1/payment-links/pl_1e7aac13b02f2ca4635607cf/deactivate" \
-H "Authorization: Bearer $STABLORA_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'{
"id": "pl_1e7aac13b02f2ca4635607cf",
"active": false,
"…": "…"
}Activate a payment link
/api/v1/payment-links/{id}/activateTurns a deactivated link back on. The URL stays the same.
Path parameters
idrequiredstring | Link id pl_…. |
Errors
404 | The id does not exist or belongs to another merchant. |
curl -X POST "http://localhost:3000/api/v1/payment-links/pl_1e7aac13b02f2ca4635607cf/activate" \
-H "Authorization: Bearer $STABLORA_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'{
"id": "pl_1e7aac13b02f2ca4635607cf",
"active": true,
"…": "…"
}Simulate a deposit (local sandbox only)
/api/v1/test/depositsSimulates an incoming transfer on a sandbox network. Refused on testnets — send real testnet coins there instead.
Body parameters
paymentId | walletId | merchantWalletIdrequiredstring | What the transfer pays: an invoice, a permanent customer wallet or your top-up wallet. |
amountrequireddecimal string | Amount in the asset. |
assetstring | Required for wallets; taken from the invoice otherwise. |
transactionHashrequiredstring | Unique id such as sim_order_1042. Replays are ignored. |
fromAddressesarray | Sender addresses, screened against sanctions lists — use a listed address to test payment.held. |
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"}'{
"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)
/api/v1/test/deposits/{id}/reverseUndoes a sandbox deposit as if the block had been reorganized away, and sends payment.reversed (or deposit.reversed). Use it to test your handling.
Path parameters
idrequiredstring | Deposit id dep_…. |
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 '{}'{
"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
/api/v1/customersIdempotent by externalId: repeating it returns the existing customer. Different merchants can use the same externalId without sharing anything.
Body parameters
externalIdrequiredstring | Your user id (max 120). |
namestring | Display name (max 100). |
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"}'{
"id": "cus_116f187959c5a838e125b339",
"merchant_id": "mch_4e425c9f49d92545a65c80ce",
"external_id": "player-8841",
"name": "Alex Morgan",
"created_at": "2026-09-24T11:03:41.255Z"
}List customers
/api/v1/customersCustomers, newest first.
Query parameters
limitinteger | 1–200, default 50. |
cursorstring | Next page. |
curl "http://localhost:3000/api/v1/customers" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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
/api/v1/walletsReturns the customer’s permanent address on a network, creating it on first use.
Body parameters
customerIdrequiredstring | Customer cus_…. |
networkrequiredstring | Network id. One address serves every asset on that network. |
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"}'{
"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
/api/v1/walletsWallets, optionally for one customer.
Query parameters
customerIdstring | Only this customer. |
limitinteger | 1–200, default 50. |
cursorstring | Next page. |
curl "http://localhost:3000/api/v1/wallets?customerId=cus_116f187959c5a838e125b339" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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
/api/v1/customers/{id}/balancesA customer’s ledger balances per network and asset (per-customer balance model).
Path parameters
idrequiredstring | Customer cus_…. |
Errors
404 | The id does not exist or belongs to another merchant. |
curl "http://localhost:3000/api/v1/customers/cus_116f187959c5a838e125b339/balances" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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.
<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>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.confirmedwebhook (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
localeor let the player choose. Show the player’s balance withbalanceand keep it fresh with POST /cashier/balances.
Open a cashier session
/api/v1/cashier/sessionsOpens 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.
Body parameters
externalIdstring | Your player id. The customer is created on first use (or pass customerId). |
customerIdstring | An existing customer cus_… instead of externalId. |
namestring | Display name for a new customer. |
withdrawalsboolean | Show the Withdraw tab (default true; off when disabled in your cashier settings). |
expiresInMinutesinteger | 5–120, default 30. |
themestring | dark (default) or light. |
localestring | Widget 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. |
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"}}'{
"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
/api/v1/cashier/balancesSets 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.
Body parameters
externalIdstring | Your player id (or pass customerId). |
customerIdstring | The customer cus_…. |
amountrequiredstring | Decimal string, e.g. "175.40". |
currencyrequiredstring | 1–12 letters, digits or a currency sign: USD, EUR, Coins. |
balancenull | Send "balance": null instead of amount/currency to hide it. |
Errors
404 | No customer with this externalId yet (open a cashier session first). |
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"}'{
"customerId": "cus_116f187959c5a838e125b339",
"externalId": "player-8841",
"balance": {
"amount": "175.40",
"currency": "USD",
"updatedAt": "2026-10-12T16:07:00.000Z"
}
}List withdrawal requests
/api/v1/cashier/withdrawalsWithdrawal requests from the cashier, newest first.
Query parameters
statusstring | pending, confirmed, rejected, cancelled or expired. |
customerIdstring | Only this customer. |
limitinteger | 1–200, default 50. |
curl "http://localhost:3000/api/v1/cashier/withdrawals?status=pending" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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
/api/v1/cashier/withdrawals/{id}One withdrawal request. After confirmation status follows the payout: processing, completed, failed or rejected.
Path parameters
idrequiredstring | Request cwr_…. |
Errors
404 | The id does not exist or belongs to another merchant. |
curl "http://localhost:3000/api/v1/cashier/withdrawals/cwr_5c1f0e9a7b3d2c4e6f8a0b1c" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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
/api/v1/cashier/withdrawals/{id}/confirmCall 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.
Path parameters
idrequiredstring | Request cwr_…. |
Errors
404 | The id does not exist or belongs to another merchant. |
409 | The request was cancelled, rejected or expired, or your balance is too low. |
403 | The destination is on a sanctions list; the request was rejected. |
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 '{}'{
"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
/api/v1/cashier/withdrawals/{id}/rejectRefuses a waiting request; the player sees your reason in the cashier.
Path parameters
idrequiredstring | Request cwr_…. |
Body parameters
reasonstring | Shown to the player (max 200 characters). |
Errors
404 | The id does not exist or belongs to another merchant. |
409 | Only a waiting request can be rejected. |
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"}'{
"id": "cwr_5c1f0e9a7b3d2c4e6f8a0b1c",
"status": "rejected",
"reason": "Wagering requirement not met"
}Balances
/api/v1/balancesYour total balances per network and asset. available can be paid out; reserved is held by pending payouts.
curl "http://localhost:3000/api/v1/balances" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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
/api/v1/balances/unallocatedOnly 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.
curl "http://localhost:3000/api/v1/balances/unallocated" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"data": [
{
"network": "tron",
"asset": "USDT",
"decimals": 6,
"available": "1690.416856",
"reserved": "0"
}
]
}List deposits
/api/v1/depositsEvery credited transfer with fee and net amount. reversed_at is set if a reorganization undid it.
Query parameters
customerIdstring | Only this customer. |
limitinteger | 1–200, default 50. |
cursorstring | Next page. |
curl "http://localhost:3000/api/v1/deposits" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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
/api/v1/topup-walletsYour 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.
Body parameters
networkrequiredstring | Network id. |
curl -X POST "http://localhost:3000/api/v1/topup-wallets" \
-H "Authorization: Bearer $STABLORA_API_KEY" \
-H "Content-Type: application/json" \
-d '{"network":"tron"}'{
"id": "mwl_59a8b99deaaebb5dafcf64c7",
"merchant_id": "mch_4e425c9f49d92545a65c80ce",
"network": "tron",
"address": "TXr2cK9mZp5vQe1wHs7bN3yGfUj6LdA4oT",
"created_at": "2026-09-24T11:03:41.928Z"
}List top-up wallets
/api/v1/topup-walletsAll your top-up addresses.
curl "http://localhost:3000/api/v1/topup-wallets" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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.
| Status | Meaning |
|---|---|
pending_approval | Waiting for you to approve (manual mode, above your automatic limit, or held by a player-protection rule — see hold_reason). |
pending_review | Above the platform limit; the platform reviews it. |
broadcasting | Signed and sent to the network (testnets). Wait for confirmation. |
completed | Final. tx_hash is set. |
cancelled / rejected / failed | Nothing 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
/api/v1/payouts/quoteNetwork fee, withdrawal commission and the net amount the recipient receives. Sends nothing.
Body parameters
networkrequiredstring | Network id. |
assetrequiredstring | Asset. |
amountrequireddecimal string | Amount leaving your balance. |
Errors
400 | Amount does not cover the network fee and withdrawal commission. |
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"}'{
"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
/api/v1/payoutsReserves the amount and either sends it (automatic approval) or waits for approval. Always send an Idempotency-Key and reuse it when retrying.
Body parameters
networkrequiredstring | Network id. |
assetrequiredstring | Asset. |
amountrequireddecimal string | Amount leaving your balance (the recipient gets net). |
addressrequiredstring | Destination address, validated for the network. |
customerIdstring | The user being paid. Required for per-customer balances and player-protection rules. |
Errors
403 | Destination is on the platform block list. |
409 | Insufficient balance, daily platform limit reached, or the key was reused with different details. |
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"}'{
"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
/api/v1/payouts/batchUp 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.
Body parameters
payoutsrequiredarray | Items with network, asset, amount, address and optionally customerId or externalId (the player). |
Errors
400 | Payout N: … — item N is invalid (address, amount, unknown customer), or more than 100 items. |
403 | Payout N: … — destination on the block list (nothing created). |
409 | Payout N: … — insufficient balance or limit reached, or the key was reused with different items (nothing created). |
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"}]}'{
"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
/api/v1/payouts/batches/{id}A batch with its payouts, a count per status and totals per network and asset.
Path parameters
idrequiredstring | Batch pob_…. |
Errors
404 | The id does not exist or belongs to another merchant. |
curl "http://localhost:3000/api/v1/payouts/batches/pob_3e9a1c7d5b2f4e6a8c0d1b3f" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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
/api/v1/payouts/{id}Current status, fees, hold_reason and tx_hash.
Path parameters
idrequiredstring | Payout out_…. |
Errors
404 | The id does not exist or belongs to another merchant. |
curl "http://localhost:3000/api/v1/payouts/out_32310a196885bf86be5926a4" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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
/api/v1/payoutsPayouts and refunds, newest first. Poll status=pending_approval to build an approval queue.
Query parameters
statusstring | e.g. pending_approval, completed. |
customerIdstring | Only this player/customer. |
limitinteger | 1–200, default 50. |
cursorstring | Next page. |
curl "http://localhost:3000/api/v1/payouts?status=pending_approval" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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
/api/v1/payouts/{id}/approveApproves a pending_approval payout and sends it.
Path parameters
idrequiredstring | Payout out_…. |
Errors
404 | The id does not exist or belongs to another merchant. |
409 | The payout is no longer pending. |
curl -X POST "http://localhost:3000/api/v1/payouts/out_32310a196885bf86be5926a4/approve" \
-H "Authorization: Bearer $STABLORA_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'{
"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
/api/v1/payouts/{id}/cancelCancels a payout that has not been sent yet and returns the reserved amount to the balance. Sends payout.cancelled.
Path parameters
idrequiredstring | Payout out_…. |
Errors
404 | The id does not exist or belongs to another merchant. |
409 | The payout was already sent. |
curl -X POST "http://localhost:3000/api/v1/payouts/out_32310a196885bf86be5926a4/cancel" \
-H "Authorization: Bearer $STABLORA_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'{
"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
/api/v1/payments/{id}/refundsSends 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.
Path parameters
idrequiredstring | Payment pay_…. |
Body parameters
amountrequireddecimal string | Amount to refund. |
addressrequiredstring | Customer’s address. |
reasonstring | Shown in your records (max 200). |
Errors
409 | Nothing left to refund, or the refund exceeds the refundable amount. |
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"}'{
"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
/api/v1/payments/{id}/refundsRefunds of one invoice plus how much is still refundable.
Path parameters
idrequiredstring | Payment pay_…. |
Errors
404 | The id does not exist or belongs to another merchant. |
curl "http://localhost:3000/api/v1/payments/pay_c18df35f8fe0c08ccd846672/refunds" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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
/api/v1/statementEvery 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.
Query parameters
fromdate | YYYY-MM-DD (inclusive). |
todate | YYYY-MM-DD (inclusive). |
curl "http://localhost:3000/api/v1/statement?from=2026-09-01&to=2026-09-30" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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
/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.
Path parameters
monthrequiredstring | YYYY-MM. |
curl "http://localhost:3000/api/v1/invoices/2026-09" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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.
{
"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", "…": "…" }
}| Header | Value |
|---|---|
Stablora-Signature | t=<unix seconds>,v1=<hex HMAC-SHA256> |
Stablora-Event-Id | Same as id in the body |
Content-Type | application/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
idand ignore repeats; always act on the current state fromGET /payments/{id}. - Redirects are not followed.
Event types
| Event | When | data |
|---|---|---|
payment.completed | The invoice received the full amount (or an amount within your underpayment tolerance). | Payment |
payment.discrepancy | Funds arrived but the invoice is underpaid, overpaid or late. Review before fulfilling. | Payment |
payment.reversed | A chain reorganization removed a credited transfer. The credit was undone; put the order back on hold. | Payment + reversedDeposit |
payment.held | The 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.confirmed | A transfer to an invoice or permanent customer wallet was credited. | Deposit |
deposit.held | A transfer to a customer wallet or top-up wallet came from a sanctioned sender and is frozen. | Deposit |
deposit.reversed | A credited transfer to a customer wallet or top-up wallet was reorganized away. | Deposit |
topup.confirmed | Your merchant top-up wallet received funds for the payout pool (no processing fee). | Deposit |
payout.requested | A payout or refund was created. Check status and hold_reason: it may wait for approval. | Payout |
payout.broadcasting | The payout was signed and sent to the network (testnets). Wait for payout.completed. | Payout |
payout.completed | The payout is final. tx_hash is set. | Payout |
payout.failed | The network rejected the payout. The reserved amount was returned to the balance. | Payout |
payout.cancelled | You cancelled a pending payout. The reserved amount was returned. | Payout |
payout.rejected | The platform rejected a payout held for review. The reserved amount was returned. | Payout |
cashier.withdrawal_requested | A 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_cancelled | The player cancelled a waiting withdrawal request. Release anything you reserved for it. | Cashier withdrawal |
cashier.withdrawal_expired | A withdrawal request was not answered within 24 hours and expired. Release anything you reserved for it. | Cashier withdrawal |
swap.queued | Live: an incoming coin is reserved for an on-chain DEX conversion (Uniswap V3 / PancakeSwap V3) on the same network. | Swap |
swap.completed | An incoming asset was converted to your settlement stablecoin. Live swaps credit the amount actually received, minus the 0.25% markup. | Swap |
swap.skipped | Conversion was skipped (no route, no liquidity or no fresh price); funds stay in the received asset. | { reference, reason } or Swap |
swap.failed | Live: the DEX swap could not be executed; the reserved funds are available again in the received asset. | Swap |
test.ping | Sent 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.
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);
// });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))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
403and codeaddress_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 receivepayment.held(ordeposit.held) instead ofpayment.completed. Do not fulfil the order. - The platform reviews it. If released, the deposit is credited normally and
payment.completedis 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
/api/v1/webhook-endpointsRegisters a destination. The response contains its signing secret — shown only once; store it to verify deliveries.
Body parameters
urlrequiredstring | Public https:// URL (no credentials, no #fragment). |
events"*" | string[] | Event types to deliver, e.g. ["payment.completed"]. Default "*" (all). |
labelstring | Shown in the dashboard (max 80). |
Errors
400 | Invalid or private URL, or unknown event type format. |
409 | Already 25 active endpoints. |
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"}'{
"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
/api/v1/webhook-endpointsActive endpoints, including those created for connected stores (connectionId). Secrets are never returned here.
curl "http://localhost:3000/api/v1/webhook-endpoints" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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
/api/v1/webhook-endpoints/{id}Stops deliveries to the endpoint immediately; pending retries are cancelled.
Path parameters
idrequiredstring | Endpoint we_…. |
Errors
404 | The id does not exist or belongs to another merchant. |
curl -X DELETE "http://localhost:3000/api/v1/webhook-endpoints/we_3f1c0b8e2d4a5f6e7d8c9b0a" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"id": "we_3f1c0b8e2d4a5f6e7d8c9b0a",
"deleted": true
}Inspect an event
/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.
Path parameters
idrequiredstring | Event evt_…. |
Errors
404 | The id does not exist or belongs to another merchant. |
curl "http://localhost:3000/api/v1/events/evt_56706555c39438cb1106801f" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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
/api/v1/events/{id}/replayRe-queues a failed or retrying event for immediate delivery (for example after fixing your endpoint).
Path parameters
idrequiredstring | Event evt_…. |
Errors
409 | Only failed or retrying events can be replayed. |
curl -X POST "http://localhost:3000/api/v1/events/evt_56706555c39438cb1106801f/replay" \
-H "Authorization: Bearer $STABLORA_API_KEY" \
-H "Content-Type: application/json" \
-d '{}'{
"ok": true
}List events
/api/v1/eventsRecent events with their delivery status and last error.
Query parameters
statusstring | pending, retry, delivered, failed. |
limitinteger | 1–200, default 50. |
cursorstring | Next page. |
curl "http://localhost:3000/api/v1/events" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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).
npm install @stablora/nodeimport 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.
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);$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.
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'])# 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 idMCP 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.
| Tool | Does |
|---|---|
create_payment | USD invoice, returns checkoutUrl |
create_checkout_session | Link where the customer picks the coin |
get_payment, list_payments | Status and history |
get_balances, list_deposits, list_payouts | Read balances and movements |
quote_payout | Preview fees (sends nothing) |
list_networks | Supported networks and assets |
verify_webhook | Check a signature |
claude mcp add stablora --env STABLORA_API_KEY=qk_test_... -- node /path/to/stablora/sdk/mcp/bin/stablora-mcp.js{
"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).
| Platform | What it does | Setup |
|---|---|---|
| WooCommerce | Classic and block checkout; reversals handled | Connect with Stablora (one click) |
| Paid Memberships Pro | Membership checkout; level activated only after verified payment | Connect with Stablora |
| Gravity Forms 2.5+ | Payment add-on: any form total, entry Paid after a verified payment, Payment Completed notifications | API key + webhook endpoint |
| Easy Digital Downloads | Download purchases | Connect with Stablora |
| PrestaShop 8 | Payment module with “awaiting” and “reversed” order states | Connect with Stablora |
| OpenCart 4 | Payment extension, per-store settings | Connect with Stablora |
| Magento 2.4 | Payment method; invoice on verified payment, hold on reversal | API key + webhook endpoint |
| Shopware 6.7 | Payment handler; transaction paid on verified payment, chargeback on reversal | API key + webhook endpoint |
| CS-Cart 4.15+ | Payment processor; order Processed on verified payment | API key + webhook endpoint |
| nopCommerce 5.00 | Redirection payment method (source release, build in your solution) | API key + webhook endpoint |
| Drupal Commerce 2/3 | Off-site gateway; payment recorded from the verified webhook | API key + webhook endpoint |
| Medusa v2 | Payment Module provider; captured by the verified webhook | medusa-config options + webhook endpoint |
| Vendure 3 | Payment handler + createStabloraCheckout mutation; settled by the verified webhook | API key + webhook endpoint |
| WHMCS 8 | Invoices paid with any coin; Gateway Log for review cases | API key + webhook endpoint |
| FOSSBilling 0.8+ | Invoices paid with any coin; a missed webhook is settled when the client returns | API key + webhook endpoint |
| Blesta 5 | Non-merchant gateway; transactions from verified payments, void on reversal | API key + webhook endpoint |
| Discord | Self-hosted bot: /buy sells roles (permanent or for N days); role added after a verified payment | .env + products.json + webhook endpoint |
| Telegram | Self-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 attention | API key + webhook endpoint |
| Moodle 4.1+ | Payment gateway (paygw): course fees and other Moodle payments; enrolment after a verified payment | API 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 alerts | Dashboard → Donations; nothing to install |
| XenForo 2.2+ | User upgrades; reversed on a chain reorganization | API key + webhook endpoint |
| n8n | Action node (checkouts, payments, wallets) and a verified payment trigger | API key; the trigger registers its own webhook |
| Odoo 18 | Payment provider for eCommerce, invoices and payment links; order confirmed after a verified payment | API key + webhook endpoint |
| Bubble | Server-side actions: create checkout, check checkout on the return page, verify webhook payments | Private plugin from our source (marketplace listing pending) or API Connector |
| Zapier, Make, Activepieces | Create checkouts; start automations on verified payments | API key; triggers register their own webhook (Zapier and Make need our app listing first) |
| Shopify, Ecwid, BigCommerce | Self-hosted bridge service (Node.js) | .env; see the platform notes below |
| Node.js / PHP / Python SDK, MCP server | Build your own integration | API key |
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
/api/v1/networksSupported networks, their assets and finality rules. The table below is generated from the live catalogue.
Live networks — use with a live key (qk_live_…); real funds.
| Network id | Name | Assets | Family |
|---|---|---|---|
ethereum | Ethereum | EVM | |
base | Base | EVM | |
arbitrum | Arbitrum | EVM | |
polygon | Polygon | EVM | |
bnb | BNB Chain | EVM | |
optimism | Optimism | EVM | |
avalanche | Avalanche | EVM | |
tron | TRON | TRON | |
ton | TON | TON | |
solana | Solana | Solana | |
bitcoin | Bitcoin | UTXO | |
dogecoin | Dogecoin | UTXO | |
monero | Monero | Monero | |
litecoin | Litecoin | UTXO |
Testnets for integration tests — test key (qk_test_…), free coins with no value (10)
| Network id | Name | Assets | Family |
|---|---|---|---|
sepolia | Sepolia testnet | EVM | |
base-sepolia | Base Sepolia testnet | EVM | |
arbitrum-sepolia | Arbitrum Sepolia testnet | EVM | |
op-sepolia | OP Sepolia testnet | EVM | |
polygon-amoy | Polygon Amoy testnet | EVM | |
avalanche-fuji | Avalanche Fuji testnet | EVM | |
bnb-testnet | BNB Chain testnet | EVM | |
tron-nile | TRON Nile testnet | TRON | |
solana-devnet | Solana devnet | Solana | |
bitcoin-testnet | Bitcoin testnet | UTXO |
curl "http://localhost:3000/api/v1/networks" \
-H "Authorization: Bearer $STABLORA_API_KEY"{
"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).
{ "error": { "message": "Amount must be a positive decimal string." } }| Status | Meaning |
|---|---|
400 | Validation failed: missing field, wrong format, too many decimals, unsupported network/asset. |
401 | Missing or invalid API key or session; or a two-factor code is required. |
403 | Key lacks the permission (read / payments / payouts), IP not allowed, blocked address, or dashboard-only route. |
404 | Not found — including resources that belong to another merchant. |
409 | Conflict: idempotency key reused with different details, insufficient balance, limit reached, already settled. |
410 | The checkout expired. Create a new one. |
413 | Request body larger than 16 KB. |
415 | Send Content-Type: application/json. |
429 | Rate limit exceeded (300 requests per minute per merchant; lower on public endpoints). Retry after a minute. |
500 | Unexpected error. Safe to retry GET requests and requests with an Idempotency-Key. |
503 | No fresh market price to lock a USD amount. Retry shortly. |