Introduction
The SoftBet Wallet API is a crypto payment gateway built for iGaming. Create payment intents with a hosted checkout, assign each player a permanent deposit address, reconcile balances, pay out with operator approval, and swap between assets — all from one REST API over https://api.softbet.io.
Requests and responses are JSON. Authenticated calls use a Bearer secret key. A flat 0.4% fee applies where a fee is charged — it is always returned explicitly on the object (fee, fee_rate) so you never have to compute it.
Authentication
Authenticate every request with your secret API key in the Authorization header. Live keys start with sk_live_, test keys with sk_test_. Create keys in the dashboard (up to 10 active per account) — the secret is shown exactly once. Never expose a secret key in client-side code.
Authorization: Bearer sk_live_••••••••••••••••
curl https://api.softbet.io/v1/wallets \ -H "Authorization: Bearer sk_live_••••"
Error envelope
Success responses return the resource directly (some read endpoints wrap it as { ok: true, data } — each response example below shows the exact shape). Every error uses one envelope:
{
"ok": false,
"error": {
"code": "unknown_api_key",
"message": "API key not recognized."
}
}
Validation failures return 422 invalid_input with a details array of { path, message } entries pointing at the offending fields.
Idempotency
Money-moving POSTs take an Idempotency-Key header (any string, 8–120 chars). It is required on POST /v1/payouts and POST /v1/swaps, and optional (but honored) on POST /v1/payments. Retrying with the same key and the same body returns the original stored response — the operation never runs twice. Reusing a key with a different body is rejected with 409 idempotency_key_reuse.
curl https://api.softbet.io/v1/payouts \ -H "Authorization: Bearer sk_live_••••" \ -H "Idempotency-Key: payout-8842-attempt-1" \ -H "Content-Type: application/json" \ -d '{ "amount": "4200.00", "currency": "usdttrc20", "address": "T••••" }'
Rate limits & IP allowlist
Each key has a per-second request limit (default 50 req/s, configurable 1–2000 when the key is created). Exceeding it returns 429 rate_limited — back off and retry. Optionally, restrict a key to an IP allowlist of IPv4 addresses or CIDR blocks (e.g. 203.0.113.0/24); requests from any other address are rejected with 403 ip_blocked.
Environments & testing
Every key is minted for exactly one environment — sk_live_ or sk_test_ — chosen at creation and not switchable. Test keys operate against a fully isolated test ledger: test money can never touch live balances, and vice versa. The same endpoints, request shapes and webhooks work in both.
Sandbox faucet
In the portal, switch to Test mode and use the faucet to credit your test balance instantly. A faucet grant books a synthetic deposit and fires a real deposit.confirmed webhook, so you can exercise the whole pipeline end-to-end without touching a chain. Limits: 10 grants per 24 hours, with per-asset caps per grant.
Payments & hosted checkout
A payment is an intent to collect a fiat-denominated amount in crypto. Creating one assigns (or reuses) the player’s permanent deposit address for the chosen asset and quotes the exact crypto amount. The credit happens when the on-chain deposit confirms — you’ll get a payment.confirmed webhook.
/v1/payments"250.00".USD is supported in v1 (default USD).btc, eth, bnb, sol, trx, xrp, ltc, doge, usdt, usdc, usdttrc20, usdtspl, usdcspl, usdcbase. The live list with availability is GET /v1/pay-currencies.curl https://api.softbet.io/v1/payments \ -H "Authorization: Bearer sk_live_••••" \ -H "Content-Type: application/json" \ -d '{ "amount": "250.00", "currency": "USD", "pay_currency": "usdttrc20", "player_id": "p_8a2f91", "order_id": "BET-44192" }'
const res = await fetch("https://api.softbet.io/v1/payments", { method: "POST", headers: { "Authorization": `Bearer ${process.env.SOFTBET_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ amount: "250.00", currency: "USD", pay_currency: "usdttrc20", player_id: "p_8a2f91", }), }); const payment = await res.json();
{
"id": "pay_1f0c9a4e-5b2d-4c8e-9f3a-7d6e5c4b3a21",
"payment_id": "1f0c9a4e-5b2d-4c8e-9f3a-7d6e5c4b3a21",
"status": "waiting",
"amount": "250.00",
"currency": "USD",
"pay_currency": "usdttrc20",
"pay_amount": "249.000000",
"pay_address": "T•••••••••••••••••••••••••••••••••",
"chain": "tron",
"fee": "1.00",
"fee_rate": "0.004",
"player_id": "p_8a2f91",
"order_id": "BET-44192",
"environment": "live",
"expires_at": "2026-07-02T12:40:00.000Z"
}
The 0.4% fee is deducted from the receivable: pay_amount = amount × (1 − 0.004) / rate — a $250 payment quotes 249 USDT. The quote (and its expires_at) is fixed at creation. destination_tag is included when the asset requires one (XRP).
A payment is created waiting and becomes confirmed when a deposit that fully covers pay_amount settles on-chain. Underpayments never confirm an intent — they still credit your balance as a plain deposit (deposit.confirmed).
/v1/payments/:idFetch a payment by its pay_… id. Returns the same object plus created_at and confirmed_at.
curl https://api.softbet.io/v1/payments/pay_1f0c9a4e-5b2d-4c8e-9f3a-7d6e5c4b3a21 \ -H "Authorization: Bearer sk_live_••••"
Hosted checkout
Instead of building your own payment screen, send the player to the hosted checkout — it renders the address + QR, live status and countdown, and polls until the payment confirms:
https://wallet.softbet.io/checkout.html?id=pay_1f0c9a4e-5b2d-4c8e-9f3a-7d6e5c4b3a21
/v1/checkout/:idPublic · no authThe status endpoint behind that page. It requires no API key — the unguessable payment id is the capability — and returns only player-facing fields (id, status, amount, currency, pay_currency, pay_amount, pay_address, destination_tag?, chain, merchant_name, order_id, fee, tx_hash, expires_at, confirmed_at), so you can safely poll it from a browser.
Permanent deposit addresses
Each player gets one dedicated deposit address per asset that never changes. Anything sent to it is detected, confirmation-gated and credited to your balance, firing a deposit.confirmed webhook with the player reference — no rotation, no reconciliation spreadsheets.
/v1/addressesAssign (or fetch the existing) permanent address for a player on a chain. Idempotent: repeat calls return the same address.
curl https://api.softbet.io/v1/addresses \ -H "Authorization: Bearer sk_live_••••" \ -H "Content-Type: application/json" \ -d '{ "player_id": "p_8a2f91", "chain": "btc" }'
{
"address": "bc1q••••••••••••••••••••••••••••",
"chain": "btc",
"player_id": "p_8a2f91",
"permanent": true
}
chain takes the same lowercase tokens as pay_currency (btc, eth, usdttrc20, …). Unsupported values return 422 unsupported_chain.
Custody wallet API
The custody wallet API is the newer per-merchant wallet, keyed by an explicit (asset, network) pair — USDT on ethereum-mainnet and USDT on tron-mainnet are distinct balances with distinct addresses. Discover the enabled pairs via GET /v1/currencies. Your wallet is auto-provisioned on the first address request.
Supported assets & networks
USDT and USDC are multi-chain — each (asset, network) pair is a distinct balance with its own deposit address and its own withdrawals. Always pass the explicit network. The live, authoritative matrix is GET /v1/currencies; the current mainnet set:
USDT is available on ethereum-mainnet, polygon-mainnet, arbitrum-mainnet, bsc-mainnet, base-mainnet, tron-mainnet and solana-mainnet. USDC on ethereum-mainnet, polygon-mainnet, arbitrum-mainnet, bsc-mainnet, base-mainnet and solana-mainnet. Same ticker, different chain → different balance, address and fee.
/v1/wallet/addressessk_live_ / sk_test_ (env-matched)ETH, USDT, USDC, BNB, TRX, SOL, XRP, LTC, DOGE…ethereum-mainnet, tron-mainnet, solana-mainnet (lowercased).curl https://api.softbet.io/v1/wallet/addresses \ -H "Authorization: Bearer sk_test_••••" \ -H "Content-Type: application/json" \ -d '{ "player_id": "p_8a2f91", "asset": "USDT", "network": "tron-mainnet" }'
{
"address": "T•••••••••••••••••••••••••••••••••",
"asset": "USDT",
"network": "tron-mainnet",
"player_id": "p_8a2f91",
"permanent": true
}
Errors: 422 unsupported_asset (no enabled asset on that network), 429 address_quota_exceeded (per-account address limit), 503 signer_busy (address service busy — retry shortly).
/v1/wallet/balancessk_live_ / sk_test_ (env-matched){
"balances": [
{ "asset": "USDT", "network": "tron-mainnet", "balance": "100.000000" },
{ "asset": "ETH", "network": "ethereum-mainnet", "balance": "0.250000000000000000" }
],
"environment": "test"
}
/v1/wallet/deposits?status=&limit=sk_live_ / sk_test_ (env-matched)Inbound on-chain deposits, newest first. status filters on detected or credited; limit up to 200.
{
"deposits": [
{
"id": "8842",
"asset": "USDT",
"network": "tron-mainnet",
"address": "T••••",
"destination_tag": null,
"tx_hash": "c2a9••••",
"output_index": 0,
"amount": "100.000000",
"confirmations": 1,
"required_confs": 1,
"status": "credited",
"block_number": 61234567,
"created_at": "2026-07-02T10:14:02.000Z",
"confirmed_at": "2026-07-02T10:15:00.000Z",
"credited_at": "2026-07-02T10:15:00.000Z"
}
]
}
Payouts
Send crypto from your balance to any address. Requesting a payout reserves the funds atomically (your available balance is debited immediately) and creates a payout in requested state. Every payout is then released by operator approval — a two-person rule where the approver must differ from the requester — before it is signed and broadcast. require_approval: false is accepted for forward compatibility but never auto-broadcasts to an arbitrary address.
/v1/payoutsIdempotency-Key required"4200.00". Must be > 0.usdttrc20, btc, eth.curl https://api.softbet.io/v1/payouts \ -H "Authorization: Bearer sk_live_••••" \ -H "Idempotency-Key: payout-8842-attempt-1" \ -H "Content-Type: application/json" \ -d '{ "amount": "4200.00", "currency": "usdttrc20", "address": "T•••••••••••••••••••••••••••••••••" }'
{
"id": "7c3d1b2a-9e4f-4a6b-8c5d-2f1e0d9c8b7a",
"status": "requested",
"currency": "usdttrc20",
"amount": "4200.00",
"address": "T•••••••••••••••••••••••••••••••••",
"created_at": "2026-07-02T10:20:00.000Z"
}
Insufficient balance returns 422 insufficient_funds and reserves nothing. A missing Idempotency-Key returns 400 idempotency_key_required.
Rejected payouts release the reserved funds back to your balance. Once a payout is broadcast and confirms, its object carries the tx_hash and paid_at — poll GET /v1/payouts/:id after approval to track it (a payout.paid event is additionally delivered to webhook endpoints configured without an event filter).
/v1/payouts/:id{
"id": "7c3d1b2a-9e4f-4a6b-8c5d-2f1e0d9c8b7a",
"currency": "usdttrc20",
"chain": "tron",
"amount": "4200.00",
"destination_address": "T••••",
"destination_tag": null,
"status": "paid",
"tx_hash": "a91b••••",
"created_at": "2026-07-02T10:20:00.000Z",
"approved_at": "2026-07-02T10:26:41.000Z",
"submitted_at": "2026-07-02T10:27:04.000Z",
"paid_at": "2026-07-02T10:29:12.000Z"
}
/v1/payouts?status=List your latest 100 payouts, newest first, optionally filtered by status (requested, approved, submitted, paid, rejected, failed). Returns { data: […] }.
Automated cashouts (iGaming)
Pay a player their winnings automatically, on the right chain, straight from your custodial hot wallet — with no operator step on our side. The flow: your casino/CMS approves a withdrawal on your side (staff approval, risk checks, whatever your process is) → your backend calls POST /v1/casino-payouts → we reserve, sign (isolated signer — source is your treasury, capped, chain-pinned), and broadcast. You track it via the withdrawal.broadcast / withdrawal.confirmed webhooks. If the player’s chosen currency balance is short, enable auto-swap and we convert from another crypto you hold (cross-chain if needed) to cover it.
/v1/casino-payoutscasino:payout · HMAC · Idempotency-KeyHeaders: Authorization: Bearer sk_live_…, a unique Idempotency-Key, and x-casino-signature: t=<unix>,v1=<hmac> where v1 = HMAC-SHA256(bs_secret, "<t>.<raw request body>") (5-minute clock tolerance). Replaying the same Idempotency-Key returns the original payout — it never double-pays.
const body = JSON.stringify({ player_ref:"p_8a2f", address:"T…", asset:"USDT", network:"tron-mainnet", amount:"25" }); const t = Math.floor(Date.now()/1000); const v1 = crypto.createHmac("sha256", BS_SECRET).update(`${t}.${body}`).digest("hex"); await fetch("https://api.softbet.io/v1/casino-payouts", { method:"POST", headers:{ "authorization":`Bearer ${SK_LIVE}`, "content-type":"application/json", "idempotency-key": withdrawalId, "x-casino-signature":`t=${t},v1=${v1}` }, body });
Card-to-crypto
Let players top up with a bank card (Visa / Mastercard / Maestro), Apple Pay, Google Pay, or local bank transfer (SEPA / ACH and regional rails) — settled instantly as USDC on Polygon to the player’s own permanent deposit address, with chargeback protection and no card KYC on your side. You create a card order, open the hosted checkout (popup or redirect), and the USDC payout lands on-chain where it is credited by the exact same pipeline as any deposit (deposit.confirmed webhook, unified transactions feed, custody ledger). The checkout auto-selects from 20+ integrated payment providers by the customer’s country, order amount and best rate.
Receive any crypto, not just USDC-Polygon. The card provider only outputs USDC on Polygon, but you are not limited to it: set target_asset/target_network on the order (e.g. USDT on tron-mainnet, ETH, SOL, BTC) and once the USDC is credited we convert it on-chain into your chosen asset via the swap engine — the merchant ends up holding exactly the crypto the flow was set to deliver, every leg verifiable by tx hash.
How it works
One order = one hosted checkout link for one player top-up. The payout wallet registered for the order is the player’s own permanent USDC (Polygon) custody address, so funds never route through a shared account:
POST /v1/card-orders. We mint (or reuse) the player’s USDC/Polygon custody address, register it against a per-order callback, and return the payment_url.Endpoints
Public API endpoints take a Bearer sk_live_ key; the portal endpoints (portal session) are the same operations for the logged-in dashboard.
/v1/card-orders30/minCreate a card order. Portal equivalent: POST /v1/gateway/dash/card-orders (session, 20/min).
"25.00". Must meet the selected method’s minimum. Add your own markup here if you want to pass processing costs on.USD, EUR, GBP, CAD. Some methods are single-currency — the checkout only offers those that fit your amount + currency.USDT, ETH, SOL, BTC), we auto-convert the received USDC on-chain into it as soon as it is credited. Default USDC (no swap).target_asset, e.g. tron-mainnet, ethereum-mainnet, solana-mainnet. Default polygon-mainnet. The order object echoes target_asset/target_network and, once the conversion is booked, swap_ccs_id (the on-chain swap you can track in the Swaps history / cross-chain-swap API).curl https://api.softbet.io/v1/card-orders \ -H "Authorization: Bearer sk_live_••••" \ -H "Content-Type: application/json" \ -d '{ "player_ref": "player_123", "amount": "25.00", "currency": "EUR", "email": "[email protected]" }'
{
"order": {
"id": "card_7c3d1b2a-9e4f-4a6b-8c5d-2f1e0d9c8b7a",
"end_user_ref": "player_123",
"email": "[email protected]",
"fiat_currency": "EUR",
"fiat_amount": "25.00",
"provider": null,
"deposit_address": "0x•••• (player USDC/Polygon)",
"status": "pending",
"value_coin": null,
"coin": null,
"txid_in": null,
"txid_out": null,
"created_at": "2026-07-02T10:20:00.000Z",
"paid_at": null
},
"payment_url": "https://pay.••••/checkout?order=…&amount=25.00¤cy=EUR"
}
Open payment_url in a popup or redirect the player to it. Treat the URL as opaque — hand it straight to the browser.
/v1/card-ordersList your card orders, newest first. Query limit (1–200, default 50). Returns { "items": […] } of order objects (see the field reference). Portal equivalent: GET /v1/gateway/dash/card-orders.
/v1/card-orders/:idFetch a single card order by its card_… id. Poll this to detect settlement (status: "paid"). 404 order_not_found if it isn’t yours; 400 invalid_order_id for a malformed id.
/v1/gateway/dash/card-orders/:id/refreshportal session6/minConvenience status poll for a pending order (portal only). The callback is the source of truth — this just re-checks a stuck row and, if it now reports paid, flips the order and backfills value_coin / coin / txid_out. Returns the refreshed order.
/v1/gateway/dash/card-orders/providersportal sessionThe live registry of available payment providers and their minimums — use it if you want to pin a provider. Returns:
{
"providers": [
{ "id": "…", "provider_name": "…", "status": "active",
"minimum_currency": "USD", "minimum_amount": 2 },
… // 20+ providers: cards, mobile wallets, pay-later, and 15+ local bank rails
]
}
The public API has no providers endpoint — either omit provider (auto-select, recommended) or pass an id obtained from this portal endpoint. Minimums range from about 2 to 30 in the provider’s minimum_currency; providers your amount/currency can’t meet are simply not offered.
Open the checkout in a popup
The simplest embed: keep the player on your page and open the hosted checkout in a centered popup window — no extra setup, works with the plain payment_url. Create the order on your server (never expose an sk_live_ key to the browser), hand the page the payment_url + order.id, then:
function openTopUp(paymentUrl, orderId) { // Must be called from a click/tap or the browser blocks the popup. const w = 460, h = 760; const x = screen.left + (screen.width - w) / 2; const y = screen.top + (screen.height - h) / 2; const pop = window.open(paymentUrl, "topup", `popup=yes,width=${w},height=${h},left=${x},top=${y}`); if (!pop) { location.href = paymentUrl; return; } // blocked → redirect fallback // Detect settlement: poll your backend (which proxies GET /v1/card-orders/:id). const tick = async () => { const o = await (await fetch(`/your-backend/card-orders/${orderId}`)).json(); if (o.status === "paid") { pop.close(); showSuccess(o); return; } if (pop.closed) { showClosed(); return; } // player closed it — offer retry setTimeout(tick, 4000); }; setTimeout(tick, 4000); }
The order object
Webhook: card_payment.paid
Fired to your configured webhook endpoints the moment an order settles (signed with the usual Softbet-Signature HMAC). It confirms the order is paid; the balance credit is a separate on-chain deposit.confirmed once the USDC transfer confirms on Polygon — key your accounting off the deposit, use this for order UX.
{
"event": "card_payment.paid",
"order_id": "card_7c3d1b2a-…",
"player_ref": "player_123",
"fiat_currency": "EUR",
"fiat_amount": "25.00",
"value_coin": "26.40",
"coin": "polygon_usdc",
"txid_out": "0x…",
"deposit_address": "0x…",
"environment": "live"
}
Errors
Balances & deposits
/v1/walletsBalances per asset with a USD valuation. hot is your spendable on-book balance; cold reports "0.00" in v1 (funds swept to your own cold wallet leave our books).
{
"wallets": {
"btc": { "currency": "BTC", "hot": "0.42000000", "cold": "0.00", "usd": "45890.12" },
"usdttrc20": { "currency": "USDT_TRON", "hot": "12480.000000", "cold": "0.00", "usd": "12481.75" }
},
"total_usd": "58371.87",
"environment": "live"
}
/v1/gateway/balancesRaw per-currency available balances (registry codes like USDT_TRON, not lowercase tokens). Served with Cache-Control: no-store — never cache balances.
{
"ok": true,
"data": [
{ "currency": "BTC", "available": "0.42000000", "updated_at": "2026-07-02T10:15:00.000Z" },
{ "currency": "USDT_TRON", "available": "12480.000000", "updated_at": "2026-07-02T09:58:31.000Z" }
]
}
/v1/gateway/deposits?status=&limit=&cursor=Deposit history with keyset pagination. status filters on detected, credited or reversed; limit 1–200 (default 50). Pass the returned next_cursor (an ISO timestamp) as cursor to fetch the next page; null means you’ve reached the end. GET /v1/gateway/deposits/:id fetches a single deposit including end_user_ref.
curl "https://api.softbet.io/v1/gateway/deposits?status=credited&limit=50" \ -H "Authorization: Bearer sk_live_••••"
{
"ok": true,
"data": [
{
"id": "6f0e2d4c-8b7a-4e5f-9c1d-3a2b1c0d9e8f",
"end_user_id": "9d8c7b6a-5e4f-4d3c-2b1a-0f9e8d7c6b5a",
"currency": "USDT_TRON",
"chain": "tron",
"amount": "100.000000",
"tx_hash": "c2a9••••",
"block_number": 61234567,
"status": "credited",
"created_at": "2026-07-02T10:14:02.000Z",
"credited_at": "2026-07-02T10:15:00.000Z"
}
],
"next_cursor": null
}
Swaps
Swap between assets you hold, instantly. This API swap is an internal book transfer at the oracle mid-rate minus the flat 0.4% — no external exchange, no on-chain transaction, both balances updated atomically. Quote first, then execute.
Real on-chain swaps in the dashboard. Alongside this instant book swap, your dashboard offers two on-chain swap modes that each produce a real transaction hash: same-chain via the 0x aggregator (e.g. USDC → ETH on Ethereum), and cross-chain via SideShift (e.g. ETH → BTC, USDC-Ethereum → USDC-Arbitrum, SOL → ETH). You pick the pay/receive asset + chain, get a live quote (a labeled estimate that’s reconciled to the exact on-chain-settled amount), and the bought asset lands in your treasury at finality — fully block-explorer trackable. The same cross-chain engine also powers auto-swap on payouts.
/v1/swaps/quote?from=&to=&amount=curl "https://api.softbet.io/v1/swaps/quote?from=eth&to=usdt&amount=0.5" \ -H "Authorization: Bearer sk_live_••••"
{
"from_currency": "eth",
"to_currency": "usdt",
"from_amount": "0.5",
"to_amount": "1246.563720",
"rate": "2503.14",
"fee_rate": "0.004",
"fee_usd": "5.0063"
}
/v1/swapsIdempotency-Key requiredcurl https://api.softbet.io/v1/swaps \ -H "Authorization: Bearer sk_live_••••" \ -H "Idempotency-Key: swap-2026-07-02-001" \ -H "Content-Type: application/json" \ -d '{ "from": "eth", "to": "usdt", "amount": "0.5" }'
{
"id": "swp_4b9e8d7c-6a5f-4e3d-2c1b-0a9f8e7d6c5b",
"from_currency": "eth",
"to_currency": "usdt",
"from_amount": "0.5",
"to_amount": "1246.563720",
"rate": "2503.14",
"fee_rate": "0.004",
"fee_usd": "5.0063",
"status": "done",
"created_at": "2026-07-02T10:31:00.000Z"
}
The rate is fetched at execution time, so the executed to_amount can differ slightly from an earlier quote. Errors: 422 unsupported_currency, 422 same_currency (from = to), 422 bad_amount, 422 insufficient_funds, 422 zero_output (result rounds to zero).
Auto-swap on payouts
Turn on auto-swap in your dashboard settings and you never have to pre-fund every currency your players cash out in. When a payout (either POST /v1/casino-payouts or a dashboard payout) is short in the target currency, we automatically convert from another crypto you already hold to cover it — same-chain via the on-chain 0x aggregator when possible, or cross-chain via a real SideShift shift (e.g. you hold USDT on Tron, the player wants ETH). No pre-funding, no manual swap.
The payout is accepted immediately with status pending_topup (HTTP 202) and an auto_swap block describing the conversion. The moment the swap settles on-chain, the payout is released, signed, and broadcast — you track it through the usual withdrawal.pending_topup → withdrawal.broadcast → withdrawal.confirmed webhooks. If the conversion can’t be sourced (no other balance large enough), the call returns 422 no_topup_source and nothing is sent. Every leg is real and on-chain-trackable — no estimated or off-book value.
How the amount is sized. We convert enough of your source asset (plus a small slippage buffer) to cover the payout amount + fee in the target currency, sized off the live rate and reconciled to the exact on-chain-settled amount. Enable it per-account, and optionally pin a preferred settlement_currency we draw from first.
Utility endpoints
Four public helpers — no API key needed, rate-limited to 60 requests/minute per IP. All wrap their result as { ok: true, data: … }.
/v1/currenciesPublicThe enabled asset/network registry — the source of truth for valid (asset, network) pairs.
curl https://api.softbet.io/v1/currencies { "ok": true, "data": { "currencies": [ { "asset": "ETH", "network": "ethereum-mainnet", "chain_class": "evm", "is_token": false, "decimals": 18, "min_confirmations": 12, "shared_address": false, "is_testnet": false }, { "asset": "USDT", "network": "tron-mainnet", "chain_class": "tron", "is_token": true, "decimals": 6, … } … ] } }
/v1/pay-currenciesPublicThe pay-in currencies for POST /v1/payments and POST /v1/addresses (and the hosted checkout), each with an available flag reflecting whether the rail is live right now. What this lists is always mintable — use it to build your payment currency picker.
curl https://api.softbet.io/v1/pay-currencies { "ok": true, "data": { "currencies": [ { "pay_currency": "btc", "code": "BTC", "chain": "BTC", "decimals": 8, "available": true }, { "pay_currency": "bnb", "code": "BNB", "chain": "BSC", "decimals": 18, "available": true } … ] } }
/v1/min-amount?currency=&network=PublicMinimum processable amount for a currency ($1 equivalent, converted at the current rate). network is optional.
curl "https://api.softbet.io/v1/min-amount?currency=btc" { "ok": true, "data": { "currency": "BTC", "network": null, "min_amount": "0.00000917", "min_amount_usd": "1" } }
/v1/estimate?amount=&from=&to=PublicConvert between fiat and crypto (or crypto and crypto) at the current rate. from/to accept currency codes or USD. Unknown currencies return 400 unknown_currency.
curl "https://api.softbet.io/v1/estimate?amount=250&from=usd&to=sol" { "ok": true, "data": { "from": "USD", "to": "SOL", "amount": "250", "amount_usd": "250.0000", "estimated_amount": "1.666667" } }
/v1/validate-address?currency=&address=PublicFormat-only address validation (no network calls, no checksum) — use it as a first-line check before requesting a payout. Provide currency and/or network. Returns { valid }, with a reason when invalid.
curl "https://api.softbet.io/v1/validate-address?currency=trx&address=TABCD..." { "ok": true, "data": { "valid": false, "reason": "expected base58 T… (34 chars) (format check only)" } }
Currency logos
Self-hosted cryptocurrency logos, served from our API so you can drop crisp coin icons into your cashier, price tables and wallet UI — no third-party keys, no attribution, hotlinkable and CDN-cached. Every logo comes in three shapes (original, round, rounded-square), light & dark themes, PNG & WebP, at any size up to 512 px. Any cryptocurrency ticker resolves on demand; our settlement coins are pre-generated for instant first paint.
/v1/crypto-logos/:tickerpublic · no authReturns the image bytes for a ticker (e.g. BTC, ETH, USDT, SOL). Append .png / .webp or use ?format=. Cached 24h and Access-Control-Allow-Origin: * — hotlink it straight from an <img> tag.
<img src="https://api.softbet.io/v1/crypto-logos/BTC.png" width="48" alt="BTC"> <img src="https://api.softbet.io/v1/crypto-logos/ETH.png?shape=round&size=64" width="32"> <img src="https://api.softbet.io/v1/crypto-logos/USDT.webp?shape=square&theme=dark">
Live examples (BTC · ETH · USDT · SOL · XRP):





/v1/crypto-logospublic · no authThe catalogue: the URL template, the allowed parameter values, the pre-synced tickers, and a code_ticker map from our settlement codes (incl. chain-suffixed stablecoins like USDT_TRON, USDC_BASE) to their logo ticker.
{
"ok": true,
"data": {
"url_template": "https://api.softbet.io/v1/crypto-logos/{ticker}.{format}?theme={theme}&shape={shape}&size={size}",
"params": { "theme": {…}, "shape": {…}, "format": {…}, "size": {…} },
"assets": [ { "ticker": "BTC", "url": "https://api.softbet.io/v1/crypto-logos/BTC.png" }, … ],
"code_ticker": { "USDT_TRON": "USDT", "USDC_BASE": "USDC", … }
}
}
Webhooks
We POST a signed JSON event to your endpoint(s) when money moves. Configure endpoints in the portal (Dashboard → API → Webhooks): up to 10 endpoints per account, each with an optional event filter (default: all events) and its own whsec_ signing secret, revealed once at creation. The portal can send a test ping to any endpoint and redeliver failed deliveries.
deposit.confirmedAn on-chain deposit reached its required confirmations and was credited to your balance.deposit.reversedA previously credited deposit was reversed after a chain reorg — deduct the funds from the player.payment.confirmedA deposit settled a payment intent created via POST /v1/payments.card_payment.paidA card order was paid by the customer — carries order_id, player_ref, value_coin and txid_out. The balance credit follows as deposit.confirmed once the USDC payout confirms on Polygon.Signature
Every delivery carries a softbet-signature header in the form t=<unix>,v1=<hex>, where v1 is HMAC-SHA256(secret, t + "." + rawBody) using your endpoint’s whsec_ secret. Verify against the raw request body (before any JSON parsing), reject timestamps older than 5 minutes, and compare in constant time.
content-type: application/json
softbet-signature: t=1751450100,v1=5f8a••••64-hex-chars••••
import crypto from "node:crypto"; function verifySoftbetSignature(secret, rawBody, header, toleranceSec = 300) { const parts = Object.fromEntries( String(header || "").split(",").map((kv) => kv.split("=").map((s) => s.trim()))); const t = Number(parts.t); if (!Number.isFinite(t)) return false; if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSec) return false; const expected = crypto.createHmac("sha256", secret) .update(`${t}.${rawBody}`, "utf8").digest("hex"); const a = Buffer.from(expected), b = Buffer.from(String(parts.v1 || "")); return a.length === b.length && crypto.timingSafeEqual(a, b); } // Express: mount with a raw body parser, verify, then parse. app.post("/hooks/softbet", express.raw({ type: "*/*" }), (req, res) => { const raw = req.body.toString("utf8"); if (!verifySoftbetSignature(process.env.WHSEC, raw, req.headers["softbet-signature"])) return res.status(400).end(); const event = JSON.parse(raw); // handle event ... idempotently (deliveries can repeat) res.status(200).end(); });
Legacy alias headers x-bc-signature, x-bc-timestamp and x-bc-event are also sent with every delivery for older integrations — they carry the same signature.
Delivery & retries
A delivery succeeds on any 2xx response within 5 seconds. Anything else is retried on a fixed backoff schedule, then abandoned:
Retries mean your handler can see the same event more than once — process idempotently, keying on deposit_id / payment_id.
Payloads
{
"event": "deposit.confirmed",
"deposit_id": "6f0e2d4c-8b7a-4e5f-9c1d-3a2b1c0d9e8f",
"end_user_ref": "p_8a2f91",
"currency": "USDT_TRON",
"chain": "tron",
"amount": "100.000000",
"address": "T••••",
"environment": "live",
"tx_hash": "c2a9••••",
"at": "2026-07-02T10:15:00.000Z"
}
{
"event": "payment.confirmed",
"id": "pay_1f0c9a4e-5b2d-4c8e-9f3a-7d6e5c4b3a21",
"payment_id": "1f0c9a4e-5b2d-4c8e-9f3a-7d6e5c4b3a21",
"status": "confirmed",
"order_id": "BET-44192",
"player_id": "p_8a2f91",
"pay_currency": "USDT_TRON",
"pay_amount": "249.000000",
"received_amount": "249.000000",
"received_usd": 249.11,
"environment": "live",
"amount": "250.00",
"fee": "1.00",
"deposit_id": "6f0e2d4c-8b7a-4e5f-9c1d-3a2b1c0d9e8f",
"tx_hash": "c2a9••••",
"address": "T••••",
"at": "2026-07-02T10:15:00.000Z"
}
{
"event": "deposit.reversed",
"deposit_id": "6f0e2d4c-8b7a-4e5f-9c1d-3a2b1c0d9e8f",
"currency": "USDT_TRON",
"chain": "tron",
"amount": "100.000000",
"tx_hash": "c2a9••••",
"at": "2026-07-02T10:47:00.000Z"
}
Deposits detected by the custody wallet API (/v1/wallet/*) fire deposit.confirmed with asset + network fields instead of currency + chain, and a deposit_id prefixed oct_. Faucet grants in test mode include "faucet": true.
Errors & rate limits
Conventional HTTP status codes: 2xx success, 4xx a problem with the request, 5xx a problem on our side. Every error body carries the envelope from Authentication with a stable machine-readable error.code:
{
"ok": false,
"error": {
"code": "invalid_input",
"message": "Request validation failed.",
"details": [ { "path": "amount", "message": "amount must be a positive decimal string" } ]
}
}