API Reference v2.1
Complete reference for integrating OrbChain's crypto payment gateway. Accept and send BTC, LTC, ETH, USDT (ERC-20 & TRC-20), and SOL. Non-pooled per-merchant settlement, no KYC.
New in v2.1: fiat-priced invoices, shareable payment links, scheduled automatic payouts, donation-button embed, and per-customer static addresses.
Overview
Base URL
https://orbchain.io
All paths below are relative to this base URL.
Response Envelope
Every V2 endpoint returns a consistent JSON envelope:
{
"data": { // ... endpoint-specific payload },
"message": "OK",
"error": null,
"status": 200,
"version": "2.0.0"
}
{
"data": null,
"message": "invalid destination address",
"error": {
"type": "validation",
"message": "invalid destination address"
},
"status": 400,
"version": "2.0.0"
}
Content-Type
All request bodies must be application/json. The API returns application/json for every response.
Authentication
OrbChain issues three separate API keys per merchant, each scoped to a different capability surface. Use the correct key for each group of endpoints.
| Key Name | Header | Scope |
|---|---|---|
| merchant_api_key | merchant_api_key | Create/read payment addresses |
| payout_api_key | payout_api_key | Initiate and check payouts |
| general_api_key | general_api_key | Query balances |
Pass the key in the corresponding HTTP header (not Authorization):
curl https://orbchain.io/v1/payment/static-address \ -X POST \ -H "Content-Type: application/json" \ -H "merchant_api_key: 1802bbed16da84f0667ebd4f21c1a3b9" \ -d '{"currency":"USDT_TRC20","user_id":"player-42"}'
The Dashboard API (Section 8) uses a standard Authorization: Bearer <JWT> header instead of API keys. JWTs are obtained via /dashboard/auth/login or /dashboard/auth/signup and expire after 24 hours.
Important: API keys are shown only once at signup. If you lose them, rotate them from the dashboard. Rotation invalidates the previous key immediately.
Public Endpoints
These endpoints require no authentication and are available to everyone.
Health Check
Shallow liveness probe. Always returns 200 if the process is running.
{ "ok": true }
List All Currencies
Returns all supported coins with network details and confirmation requirements.
{
"data": [
{
"symbol": "BTC",
"name": "Bitcoin",
"network": "bitcoin",
"decimals": 8,
"confirmations": 2,
"min_payout": "0.0001"
},
{
"symbol": "USDT_TRC20",
"name": "Tether (TRC-20)",
"network": "tron",
"decimals": 6,
"confirmations": 19,
"min_payout": "1"
},
{
"symbol": "SOL",
"name": "Solana",
"network": "solana",
"decimals": 9,
"confirmations": 32,
"min_payout": "0.01"
}
],
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Get Live Prices
Returns live USD prices for all supported coins. Cached for 60 seconds.
{
"data": {
"BTC": 94250,
"LTC": 82.40,
"ETH": 3186.55,
"USDT_ERC20": 1.00,
"USDT_TRC20": 1.00,
"SOL": 148.72
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
If a price cannot be fetched the coin's value is 0. This field is informational -- the gateway never uses fiat prices to compute payout amounts.
Get Receipt Public Key
Ed25519 public key used to sign settlement receipts. Fetch once and pin in your own config -- verify every receipt against the pinned value.
{
"data": {
"public_key": "4vsd+tl9YF8nK8PyWX0XTBLRCx9DqwYr097v5Qy20rA=",
"public_key_pem": "-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEA...\n-----END PUBLIC KEY-----\n",
"algorithm": "Ed25519",
"ephemeral": false
}
}
ephemeral: true means the server did not load a persistent key and generated one at boot -- treat such signatures as dev/staging only.
Payment Endpoints
All endpoints in this section require the merchant_api_key header. Payments belong exclusively to the merchant whose key was used.
Create Static Address
Generate a permanent per-user deposit address. Ideal for casino and exchange models where each player needs a persistent wallet. If the same (user_id, currency) pair is requested again, the same address is returned.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| currency | string | required | Coin to receive: BTC, LTC, ETH, USDT_ERC20, USDT_TRC20, SOL |
| user_id | string | optional | Your internal user/player ID (max 128 chars). Same (user_id, currency) always returns the same address. |
| order_id | string | optional | Your reference string (max 128 chars). Echoed back on webhooks. |
| description | string | optional | Human-readable note (max 500 chars). |
| callback_url | string | optional | Override the merchant's default webhook URL for this payment only. |
curl https://orbchain.io/v1/payment/static-address \ -X POST \ -H "Content-Type: application/json" \ -H "merchant_api_key: 1802bbed16da84f0667ebd4f21c1a3b9" \ -d '{ "currency": "USDT_TRC20", "user_id": "player-42", "order_id": "deposit-session-001", "description": "Player deposit address", "callback_url": "https://yoursite.com/webhooks/orbchain" }'
{
"data": {
"track_id": "cmo39n6n10008145nt2fyroas",
"type": "static_address",
"status": "Waiting",
"amount": null,
"currency": "USDT_TRC20",
"pay_currency": "USDT_TRC20",
"pay_amount": null,
"address": "TRrqEHua7YXsv7HYaDp1chAP6ErPMhR18e",
"order_id": "deposit-session-001",
"callback_url": "https://yoursite.com/webhooks/orbchain",
"expires_at": null,
"created_at": 1745500800,
"transactions": []
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Error Cases
- 400
validation error: ...-- missing or invalid fields - 400
currency not accepted by merchant - 503
maintenance-- gateway is in maintenance mode
Create White Label Payment
Create a one-time payment with a specific expected amount and optional expiry. Returns a fresh deposit address each time. You render the address, QR code, and amount in your own UI.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| currency | string | required | Coin to receive. |
| amount | string | conditional | Expected deposit amount in coin units (e.g. "0.005" BTC, "100" USDT). Must be > 0. Exactly one of amount or amount_fiat is required. |
| amount_fiat v2.1 | string | conditional | Price the invoice in fiat. Gateway locks a coin amount at creation using the live rate. Requires fiat_currency. |
| fiat_currency v2.1 | string | conditional | USD, EUR, GBP, JPY, CAD, AUD, CHF, CNY, INR, RUB. |
| pay_currency | string | optional | Coin the user should actually send. Defaults to currency. |
| order_id | string | optional | Your reference string (max 128 chars). |
| lifetime | integer | optional | Minutes until the address expires (1-43200). Default: 30. |
| callback_url | string | optional | Override default webhook URL. |
| description | string | optional | Max 500 chars. |
| user_id | string | optional | Max 128 chars. |
| splits v2.4 | array | optional | Revenue-split declaration — up to 10 recipients by basis points, sum must equal exactly 10000. See Revenue Splits. Mutually exclusive with settlement_coin. |
| settlement_coin v2.4 | string | optional | Coin YOU receive regardless of what the buyer pays — the conversion quote locks at creation. See Auto Settle. Defaults to your account's default_settlement_coin when set. Mutually exclusive with splits. |
curl https://orbchain.io/v1/payment/white-label \ -X POST \ -H "Content-Type: application/json" \ -H "merchant_api_key: 1802bbed16da84f0667ebd4f21c1a3b9" \ -d '{ "currency": "USDT_TRC20", "amount": "99.50", "order_id": "order-7291", "lifetime": 60, "description": "Premium plan - 1 month", "callback_url": "https://yoursite.com/webhooks/orbchain" }'
{
"data": {
"track_id": "cmo39n6nu000g145ngbl1zaw5",
"type": "white_label",
"status": "Waiting",
"amount": "99.50",
"currency": "USDT_TRC20",
"pay_amount": "99.50",
"address": "TKqBY6s8KYGdnQ3oHrQiVBkCtXdF8p2Nmr",
"order_id": "order-7291",
"expires_at": 1745504400,
"created_at": 1745500800,
"transactions": []
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Fiat-Priced Example v2.1
curl https://orbchain.io/v1/payment/white-label \ -X POST \ -H "Content-Type: application/json" \ -H "merchant_api_key: $KEY" \ -d '{ "currency": "BTC", "amount_fiat": "100", "fiat_currency": "EUR", "order_id": "order-7291" }'
{
"data": {
"track_id": "cmo444oyg000bt3pbojaw9ink",
"amount": "0.00153591",
"pay_amount": "0.00153591",
"currency": "BTC",
"amount_fiat": "100",
"fiat_currency": "EUR",
"rate_locked": "65108",
"rate_locked_at": 1776503188,
"address": "bc1qjahp0r6ln63m0zznu26kw735ye7wnsxtwzre8r"
}
}
The rate is frozen on the Payment row -- market drift during the payment window never changes what the customer owes.
Error Cases
- 400
validation error: ...-- missing currency, neither amount nor amount_fiat set, both set, amount_fiat without fiat_currency, invalid format, amount <= 0 - 400
currency not accepted by merchant - 503
maintenance
Revenue Splits v2.4
Attach a splits array to a white-label payment to fan the settlement out across up to 10 recipients, declared in basis points (1 bps = 0.01%). The sum must equal exactly 10000 or the payment is rejected at creation (splits_bps_sum_not_10000). Each entry names exactly one recipient type — recipient_merchant_id XOR recipient_address:
recipient_merchant_id— another OrbChain merchant. Their share lands as an instant, zero-fee ledger credit when the payment confirms.recipient_address— an external address in the payment coin. Their share becomes an automatic on-chain payout. The address is format-validated and burn-address-checked at creation.
{
"currency": "USDT_TRC20",
"amount": "500.00",
"splits": [
{ "bps": 7000, "recipient_merchant_id": "cmb4k1x9e0001ab8f" },
{ "bps": 2000, "recipient_merchant_id": "cmb4k1x9e0002cd3a" },
{ "bps": 1000, "recipient_address": "TKqBY6s8KYGdnQ3oHrQiVBkCtXdF8p2Nmr" }
]
}Distribution runs in integer base units; the final split absorbs the sub-unit remainder so totals always reconcile to the deposit. The platform fee applies once to the deposit, not per share. Splits are immutable after creation and cannot be combined with settlement_coin on the same payment (splits_settlement_coin_exclusive).
Error Cases
- 400
splits_too_many-- more than 10 entries - 400
splits_bps_sum_not_10000 - 400
split_recipient_xor-- entry names both or neither recipient type - 400
invalid_split_address/burn_split_address - 400
split_merchant_unknown_or_disabled
Auto Settle (settlement coin) v2.4
Accept any coin at checkout, receive the coin you actually want to hold. Two modes, freely combinable:
- Per payment — pass
settlement_coinon a white-label payment. The conversion quote locks at invoice creation, so the amount shown to the buyer already prices the swap in. - Account-wide — set
default_settlement_coin(dashboard → Settings, orPUT /dashboard/settings/default-settlement-coin). Every credited deposit in a different coin auto-converts, whichever product it arrived through.
Execution is slippage-guarded: if the venue delivers materially less than quoted, the conversion is refused and you are credited in the original coin — funds never hang in between. The outcome is visible in the payment.paid webhook:
{
"status": "PAID",
"coin": "BTC",
"amount": "0.00131", // what the buyer paid
"settled_coin": "USDT_TRC20", // what you received
"settled_amount": "148.61",
"swap_fallback": false // true = conversion refused, credited in original coin
}OrbChain adds no markup on the conversion — the standard deposit fee applies once and the venue rate passes through. Not combinable with splits on the same payment.
Create Invoice Payment
Create a hosted payment page. Returns a payment_url that you redirect the user to. The gateway renders a branded page with the deposit address, QR code, expected amount, and auto-refreshing status.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| amount | string | required | Amount in the specified currency. Must be > 0. |
| currency | string | optional | USD, EUR, or a crypto symbol. Defaults to the merchant's first accepted coin. |
| order_id | string | optional | Your reference string (max 128 chars). |
| lifetime | integer | optional | Minutes until expiry (1-43200). Default: 60. |
| callback_url | string | optional | Override default webhook URL. |
| return_url | string | optional | Redirect user to this URL after a successful payment. |
| description | string | optional | Shown on the payment page (max 500 chars). |
| string | optional | Pre-fill the payer's email. |
curl https://orbchain.io/v1/payment/invoice \ -X POST \ -H "Content-Type: application/json" \ -H "merchant_api_key: 1802bbed16da84f0667ebd4f21c1a3b9" \ -d '{ "amount": "49.99", "currency": "USD", "order_id": "sub-pro-8812", "lifetime": 30, "return_url": "https://yoursite.com/thank-you?order=sub-pro-8812", "description": "Pro subscription - 1 month", "email": "[email protected]" }'
{
"data": {
"track_id": "cmo39n6oe000k145n4chw827o",
"type": "invoice",
"status": "Waiting",
"amount": "49.99",
"currency": "USD",
"payment_url": "https://orbchain.io/pay/cmo39n6oe000k145n4chw827o",
"return_url": "https://yoursite.com/thank-you?order=sub-pro-8812",
"email": "[email protected]",
"expires_at": 1745502600,
"created_at": 1745500800,
"transactions": []
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Error Cases
- 400
validation error: ...-- amount missing or invalid - 503
maintenance
Get Accepted Currencies
Returns the list of coins the merchant has enabled. Use this to populate a coin-selection UI before creating a payment.
{
"data": {
"accepted_coins": ["BTC", "LTC", "ETH", "USDT_ERC20", "USDT_TRC20", "SOL"]
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Get Payment Status
Fetch the current status of a payment. The transactions array contains every on-chain deposit attributed to this payment.
curl https://orbchain.io/v1/payment/cmo39n6n10008145nt2fyroas \ -H "merchant_api_key: 1802bbed16da84f0667ebd4f21c1a3b9"
{
"data": {
"track_id": "cmo39n6n10008145nt2fyroas",
"type": "static_address",
"status": "Paid",
"currency": "USDT_TRC20",
"address": "TRrqEHua7YXsv7HYaDp1chAP6ErPMhR18e",
"paid_at": 1745501210,
"transactions": [
{
"tx_hash": "a0491325689e3e07f81b4e4b...",
"coin": "USDT_TRC20",
"amount": "50.00",
"amount_usd": "50.00",
"status": "CREDITED",
"confirmations": 21
}
]
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Payment Status Values
| Status | Description |
|---|---|
| Waiting | No deposit detected yet. |
| Paying | Deposit detected on-chain but below the confirmation threshold. |
| Paid | Confirmed and merchant balance credited. |
| Expired | Lifetime elapsed with no credited payment. |
| Cancelled | Manually cancelled. |
Error Cases
- 404
not found-- track_id does not exist or belongs to a different merchant
Get Signed Settlement Receipt
Ed25519-signed receipt for a completed payment. Verify offline against the platform public key -- no ongoing API trust required. Useful for high-trust B2B / invoice finance, accounting archives, and third-party audits.
curl -H "merchant_api_key: YOUR_KEY" \ https://your-domain.com/v1/payment/cmo3yhg6m000bfao625pjluvh/receipt
{
"data": {
"receipt": {
"v": 1,
"type": "payment_receipt",
"track_id": "cmo3yhg6m000bfao625pjluvh",
"merchant_id": "cmo3agjqy0000eph0qavegvzp",
"status": "PAID",
"pay_currency": "USDT_TRC20",
"pay_amount": "100.000000",
"amount_fiat": "100.00",
"fiat_currency": "USD",
"paid_at": 1776517903,
"deposits": [
{
"tx_hash": "a3f4...",
"coin": "USDT_TRC20",
"amount": "100.000000",
"confirmations": 19,
"at": 1776517783
}
],
"issued_at": 1776517903
},
"canonical": "{\"amount_fiat\":\"100.00\",\"deposits\":[...],...}",
"signature": "9qogmUY9GBqOP3Jzy0b7Gd1r9dx...",
"public_key": "4vsd+tl9YF8nK8PyWX0XTBLRCx9DqwYr097v5Qy20rA=",
"public_key_pem": "-----BEGIN PUBLIC KEY-----\n...\n",
"algorithm": "Ed25519",
"ephemeral": false
}
}
Key Fields
| Field | Description |
|---|---|
receipt | Structured payment record. All amounts preserve the string precision used during settlement. |
canonical | Deterministic JSON of receipt (sorted keys, no whitespace). Sign the bytes of this string, not a re-serialization of receipt. |
signature | Ed25519 signature over the UTF-8 bytes of canonical (base64). |
public_key_pem | SPKI PEM of the signing key. Prefer this for most libraries. |
ephemeral | false in production. true = dev/staging; reject in prod. |
Verification -- Node.js (stdlib)
const crypto = require('crypto'); function verifyReceipt(envelope) { const pubKey = crypto.createPublicKey(envelope.public_key_pem); return crypto.verify( null, Buffer.from(envelope.canonical), pubKey, Buffer.from(envelope.signature, 'base64') ); }
Verification -- Python (cryptography)
import base64 from cryptography.hazmat.primitives.serialization import load_pem_public_key from cryptography.exceptions import InvalidSignature def verify_receipt(envelope): pub = load_pem_public_key(envelope["public_key_pem"].encode()) try: pub.verify(base64.b64decode(envelope["signature"]), envelope["canonical"].encode()) return True except InvalidSignature: return False
Trust Model
- Fetch and pin
public_keyfrom/v1/common/receipt-public-keyat integration time. Store in your own config; do not re-fetch per verification. - Reject any receipt whose
public_keydoes not match the pinned value. - Reject receipts with
ephemeral: truein production. - The signature proves the gateway saw the payment as PAID at
issued_at-- it does not prove the blockchain state at read time. For that, verifydeposits[].tx_hashagainst a block explorer.
Error Cases
- 404 payment not found or belongs to another merchant
- 409
receipt only available for PAID payments
List Payments
List payments with pagination and optional filters.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| status | string | -- | Filter: WAITING, PAYING, PAID, EXPIRED, CANCELLED |
| type | string | -- | Filter: STATIC_ADDRESS, WHITE_LABEL, INVOICE |
| order_id | string | -- | Exact match on your order reference. |
| currency | string | -- | Filter by coin symbol. |
| limit | integer | 20 | Max records per page (1-200). |
| offset | integer | 0 | Records to skip for pagination. |
| sort_order | string | desc | Sort direction by created_at: asc or desc. |
curl "https://orbchain.io/v1/payment?status=PAID¤cy=USDT_TRC20&limit=50" \ -H "merchant_api_key: 1802bbed16da84f0667ebd4f21c1a3b9"
{
"data": {
"payments": [
{
"track_id": "cmo39n6n10008145nt2fyroas",
"type": "static_address",
"status": "Paid",
"currency": "USDT_TRC20",
"address": "TRrqEHua7YXsv7HYaDp1chAP6ErPMhR18e",
"paid_at": 1745501210,
"created_at": 1745500800
}
],
"total": 142,
"limit": 50,
"offset": 0
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
List Deposits
Paginated history of individual on-chain deposits — the endpoint to poll for reconciliation, especially with static addresses.
Reconciling repeat deposits to a static address. A static address is permanent and can receive many deposits over time, but each deposit is its own event. GET /v1/payment/:track_id only ever returns that one payment's deposit(s) — it does not accumulate later deposits. And a repeat deposit that lands with no open payment has no track_id at all, so it won't appear in GET /v1/payment either. GET /v1/deposits lists every deposit whether or not it's tied to a payment, so it is the canonical polling / reconciliation endpoint. Filter by address (or user_id for per-user static addresses) to fetch a single address's full deposit history, or by since to page through everything new. Webhooks (payment.paid, deposit.credited) remain the real-time path; this is the pull-based backstop for anything a webhook missed.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| coin | string | -- | Filter by coin symbol, e.g. USDT_TRC20. |
| status | string | -- | Filter: PENDING, CONFIRMED, CREDITED, FAILED. |
| address | string | -- | Exact deposit address (e.g. a static address). |
| user_id | string | -- | Your external_user_id for per-user static addresses. |
| since | integer | -- | Unix seconds; only deposits created at or after this time. |
| limit | integer | 50 | Max records per page (1-200). |
| offset | integer | 0 | Records to skip for pagination. |
| sort_order | string | desc | Sort by created_at: asc or desc. |
curl "https://orbchain.io/v1/deposits?address=TRrqEHua7YXsv7HYaDp1chAP6ErPMhR18e&limit=50" \ -H "merchant_api_key: 1802bbed16da84f0667ebd4f21c1a3b9"
{
"data": {
"deposits": [
{
"tx_hash": "3a1f...c9d2",
"coin": "USDT_TRC20",
"network": "tron",
"amount": "25.5",
"amount_usd": "25.50",
"confirmations": 30,
"status": "Paid",
"credited": true,
"address": "TRrqEHua7YXsv7HYaDp1chAP6ErPMhR18e",
"user_id": "user_8842",
"track_id": null,
"order_id": null,
"created_at": 1745500800
}
],
"total": 7,
"limit": 50,
"offset": 0
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Payout Endpoints
All endpoints in this section require the payout_api_key header.
Create Payout
Initiate a withdrawal from the merchant's gateway balance to an external on-chain address. The gateway validates the destination address, checks for burn/null addresses, verifies the balance, signs the transaction, and broadcasts it asynchronously.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| currency | string | required | Coin to send: BTC, LTC, ETH, USDT_ERC20, USDT_TRC20, SOL |
| address | string | required | Destination on-chain address (max 128 chars). Validated per-coin format. Burn/null addresses are rejected. |
| amount | string | required | Amount to send in coin units (e.g. "0.001" BTC). Must be > 0. |
| callback_url | string | optional | Override default webhook URL for this payout. |
| description | string | optional | Internal note (max 500 chars). Not sent on-chain. |
| idempotency_key | string | optional | 8-128 chars. Prevents duplicate payouts on network retry. Also accepted as X-Idempotency-Key header. |
curl https://orbchain.io/v1/payout \ -X POST \ -H "Content-Type: application/json" \ -H "payout_api_key: 6bcebc3adb04752d889f3a01c2d4e5f6" \ -H "X-Idempotency-Key: wd-user-99-20260415-001" \ -d '{ "currency": "USDT_TRC20", "address": "TJYeasTPGpTKFR5HFqqPUVaHhtnD51RMRZ", "amount": "25.00", "description": "Player withdrawal #8821" }'
{
"data": {
"track_id": "cmo3b1xyz9000a145n7qrst8u",
"status": "Confirming",
"coin": "USDT_TRC20",
"amount": "25.00",
"destination": "TJYeasTPGpTKFR5HFqqPUVaHhtnD51RMRZ",
"created_at": 1745500900
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Idempotent replay: If the same idempotency_key is sent again, the original payout is returned with "idempotent": true in the response.
Error Cases
- 400
validation error: ...-- missing required fields, amount <= 0 - 400
invalid destination address-- address fails per-coin format validation - 400
destination is a burn/null address - 400
insufficient balance - 503
payout backlog too large-- circuit breaker (500+ pending payouts) - 503
maintenance
Get Payout Status
Fetch the current status of a payout.
curl https://orbchain.io/v1/payout/cmo3b1xyz9000a145n7qrst8u \ -H "payout_api_key: 6bcebc3adb04752d889f3a01c2d4e5f6"
{
"data": {
"track_id": "cmo3b1xyz9000a145n7qrst8u",
"status": "Confirmed",
"coin": "USDT_TRC20",
"amount": "25.00",
"destination": "TJYeasTPGpTKFR5HFqqPUVaHhtnD51RMRZ",
"tx_hash": "b3c4d5e6f7a8b9c0d1e2f3a4...",
"failure_reason": null,
"created_at": 1745500900,
"updated_at": 1745501350
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Payout Status Values
| Status | Description |
|---|---|
| Confirming | Transaction signed and broadcast. Waiting for on-chain confirmation. |
| Confirmed | Confirmed on-chain. tx_hash is populated. |
| Failed | Broadcast or confirmation failed. failure_reason describes the cause. |
Error Cases
- 404
not found-- track_id does not exist or belongs to a different merchant
Mass Payouts v2.4
Send up to 100 payouts in one call. Each item is validated, debited, and enqueued independently so partial failures (bad address, insufficient balance) surface per-item without rolling back earlier items. Idempotency via external_id: repeating the same ID returns the prior payout with status: "idempotent" rather than creating a duplicate.
curl -X POST https://orbchain.io/v1/payout/batch \ -H "payout_api_key: YOUR_PAYOUT_KEY" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "currency": "BTC", "address": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", "amount": "0.001", "external_id": "payroll-alice" }, { "currency": "USDT_TRC20", "address": "TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8", "amount": "50.00", "external_id": "payroll-bob" } ] }'
{
"data": {
"summary": { "total": 2, "created": 1, "idempotent": 0, "failed": 1 },
"results": [
{
"index": 0,
"external_id": "payroll-alice",
"track_id": "cmo79lqkm0005889z3q02j6w8",
"status": "created",
"error": null
},
{
"index": 1,
"external_id": "payroll-bob",
"track_id": null,
"status": "failed",
"error": "insufficient_balance"
}
]
}
}
Per-item status: created, idempotent, or failed (error one of invalid_address, burn_address, insufficient_balance, or payout_backlog_too_large). Batch-level errors: batch is empty, batch exceeds max of 100, duplicate external_id within batch.
Refunds v2.4
Refund a PAID payment in its original pay currency. Creates a new Payout with refund_of_payment_id pointing back at this payment. Partial refunds are allowed — the cumulative (non-failed) refund total cannot exceed the payment's original amount. Pass either amount (native units) or amount_usd (converted at current rate); omit both to refund the full remaining balance.
curl -X POST https://orbchain.io/v1/payment/cmo79lqkm0005889z3q02j6w8/refund \ -H "payout_api_key: YOUR_PAYOUT_KEY" \ -H "Content-Type: application/json" \ -d '{ "destination": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", "amount": "0.003", "description": "Duplicate charge — refunded per customer request", "external_id": "refund-order-1234" }'
Error Cases
- 404
payment not found - 400
payment_not_paid-- can only refund PAID payments - 400
refund_exceeds_remaining-- amount + prior refunds > original - 400
already_fully_refunded - 400
refund_btc_ln_unsupported-- BTC_LN refunds not yet supported - 400
insufficient_balance
Internal Transfers v2.2
Instant, zero-fee, zero-confirmation balance move between two OrbChain accounts by public account_id (ORB-…, shown on every dashboard). Nothing touches a blockchain: debit and credit run in one atomic database transaction — there is no state where the sender is debited and the recipient isn't credited. The recipient's balance is spendable immediately. Requires the transfer:create scope on scoped API keys.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| to_account_id | string | required | Recipient's public ORB-… account ID. Self-transfers are rejected. |
| coin | string | required | Any coin you hold a balance in. |
| amount | string | required | Amount in coin units, e.g. "25.00". |
| idempotency_key | string | optional | Body field or X-Idempotency-Key header. Retries with the same key return the original transfer instead of sending twice. Strongly recommended. |
| totp_code | string | conditional | Required on every transfer when your account has 2FA enabled (TOTP step-up — a leaked API key alone can't move balance). |
curl https://orbchain.io/v1/transfer \ -X POST \ -H "Content-Type: application/json" \ -H "merchant_api_key: YOUR_MERCHANT_KEY" \ -d '{ "to_account_id": "ORB-3qvxq5yVwnS6c3c3Eycj", "coin": "USDT_TRC20", "amount": "25.00", "idempotency_key": "payrun-2026-08-alice" }'
{
"data": {
"transfer_id": "cmo4s7tvh0003jak6gl2bj5wf",
"to_account_id": "ORB-3qvxq5yVwnS6c3c3Eycj",
"coin": "USDT_TRC20",
"amount": "25.00",
"created_at": 1776543645
}
}Error Cases
- 400
self_transfer_forbidden - 400
recipient_not_found/recipient_disabled - 400
insufficient_balance - 403
totp_required/totp_invalid-- 2FA-enabled account without a validtotp_code - 403
insufficient_scope-- scoped key withouttransfer:create
Both parties receive signed webhooks (transfer.sent / transfer.received). The same operation is available in the dashboard as POST /dashboard/transfer (JWT auth, same TOTP step-up).
Subscriptions API
Create a subscription against one of your own plans, server-to-server — no dashboard login or captcha. customer_ref is your tenant reference; it comes back on the subscription.paid webhook so a renewal payment maps straight to a tenant with no local lookup table.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| plan_id | string | required | One of your plan ids (from GET /dashboard/plans). |
| customer_ref | string | optional | Your tenant reference (max 128). Echoed on every subscription.paid. |
| customer_email | string | optional | Where OrbChain emails the pay link each cycle. |
| start_at | string | optional | RFC3339 timestamp. Delay the first invoice (e.g. now + 7 days for a trial). Defaults to now. |
curl https://orbchain.io/v1/subscription \ -X POST \ -H "merchant_api_key: YOUR_MERCHANT_KEY" \ -H "Content-Type: application/json" \ -d '{ "plan_id": "cmsyd425y000611fylgpdhawh", "customer_ref": "tenant_8841", "customer_email": "[email protected]" }'
{
"data": {
"subscription_id": "sub_xxx",
"status": "ACTIVE",
"plan_id": "cmsyd425y000611fylgpdhawh",
"plan_label": "Pro",
"amount_usd": "149",
"interval_sec": 2592000,
"customer_ref": "tenant_8841",
"next_billing_at": 1786993200,
"manage_url": "https://orbchain.io/subscribe/manage/<magicToken>"
}
}The first invoice is minted by the billing tick (within a few minutes) and the customer is emailed the pay link; subscription.invoiced fires with a pay_url. Errors: 400 plan_not_found_or_disabled, 400 validation error.
List Subscriptions
List your subscriptions, newest first. Query params: status (ACTIVE/PAUSED/CANCELLED/FAILED), customer_ref (find a subscription by your own tenant reference), limit (≤200), offset. Returns { subscriptions, total, limit, offset }.
Get Subscription Status
Status of one of your subscriptions. Merchant-scoped — an id that doesn't belong to you returns 404, so it can't be used to probe other merchants.
Cancel Subscription
Cancel one of your subscriptions (idempotent; rotates the magic token so any stale management link dies). Use this when a tenant downgrades on your side.
Bitcoin Lightning v2.4
OrbChain accepts and pays out Lightning Network payments through the same /v1/payment/white-label and /v1/payout endpoints used for on-chain coins. The coin value is BTC_LN. Settlement is atomic — confirmations never has to reach a threshold the way on-chain deposits do; the merchant is credited the moment the BOLT11 invoice is paid.
Enable Lightning on your account by adding BTC_LN to accepted_coins under Dashboard → Accepted Coins (or via PUT /dashboard/settings/coins). OrbChain runs a managed Lightning backend for you — you don't need to run Lightning infrastructure yourself.
Create a Lightning invoice
Same endpoint as any on-chain white-label invoice; pass currency: "BTC_LN". The response includes a lightning block with a BOLT11 invoice, payment_hash, amount_sat, and expires_at.
curl https://orbchain.io/v1/payment/white-label \
-X POST \
-H "Content-Type: application/json" \
-H "merchant_api_key: $KEY" \
-d '{
"currency": "BTC_LN",
"amount": "0.00010000",
"description": "Order #8821"
}'
{
"data": {
"track_id": "cmo_xxx",
"type": "white_label",
"status": "Waiting",
"currency": "BTC_LN",
"pay_currency": "BTC_LN",
"amount": "0.00010000",
"pay_amount": "0.00010000",
"address": null,
"lightning": {
"bolt11": "lnbc100u1p57thh5pp5...",
"payment_hash": "38c5722c...",
"amount_sat": "10000",
"expires_at": 1776676099
},
"expires_at": 1776676099
}
}
Rules:
lifetimeis clamped to[1, 1440]minutes. Default: 60 min (LN standard).- Max invoice amount: 10 BTC (
ln_amount_too_large). splitsand alternatesettlement_coinare reserved for v2.5 — currently rejected asbtc_ln_splits_not_supported/btc_ln_settlement_not_supported_yet.- The hosted
/pay/:trackIdpage renders the BOLT11 as a QR plus an⚡ Open in walletlightning:…deeplink.
Pay out over Lightning (BOLT11 or Lightning Address)
Set currency: "BTC_LN" and pass either a raw BOLT11 invoice or a Lightning Address in address:
curl https://orbchain.io/v1/payout \
-X POST \
-H "Content-Type: application/json" \
-H "payout_api_key: $KEY" \
-d '{
"currency": "BTC_LN",
"address": "[email protected]",
"amount": "0.00010000"
}'
Lightning Addresses (LUD-16) are resolved server-side via https://<domain>/.well-known/lnurlp/<user>. Private hostnames and RFC1918 IPs are rejected (SSRF guard). Raw BOLT11 works too — address: "lnbc...".
Fee policy: max_fee_sat = max(10, floor(amount_sat * 0.005)) — 0.5% ceiling with a 10 sat floor. If no route under this cap exists the payout is marked Failed, the merchant balance is re-credited, and a payout.failed webhook fires.
Success response includes a lightning block with the resolved BOLT11, payment_hash, and preimage (proof-of-payment):
{
"data": {
"track_id": "...",
"status": "Confirmed",
"coin": "BTC_LN",
"amount": "0.00010000",
"destination": "[email protected]",
"tx_hash": "38c5722c...",
"lightning": {
"bolt11": "lnbc100u1p...",
"payment_hash": "38c5722c...",
"preimage": "7f9c2ba4..."
}
}
}
Settlement webhook
When an LN invoice is paid, a payment.paid webhook fires on the same URL + secret your on-chain payments use. Payload is the standard payment shape with the lightning block populated (settled_at, settle_amount_sat). Idempotent by payment_hash — even if the LND SubscribeInvoices stream redelivers, the merchant is never double-credited.
Subscriptions v2.4
Recurring billing for SaaS, memberships, newsletters, and any other periodic-charge business. A Plan is the product (label, amount_usd, interval_sec, accepted_coins). A Subscription is one customer's enrollment. A 5-minute worker tick opens an invoice per due cycle, emails the customer a pay link, and fires webhooks. Two consecutive expired invoices flip the subscription to FAILED.
Create a Plan
curl -X POST https://orbchain.io/dashboard/plans \ -H "Authorization: Bearer <JWT>" \ -H "Content-Type: application/json" \ -d '{ "label": "Pro · monthly", "amount_usd": "9.99", "interval_sec": 2592000, "accepted_coins": ["BTC", "USDT_TRC20", "USDC_ERC20"] }'
interval_sec: 3600 (1h floor) to 31 536 000 (1y ceiling). Once the plan is created, share https://orbchain.io/subscribe/:planId with customers to enroll them.
Other dashboard endpoints: GET /dashboard/plans, PATCH /dashboard/plans/:id (patch label, amount_usd, interval_sec, accepted_coins, enabled), POST /dashboard/subscriptions (merchant-side enrollment), GET /dashboard/subscriptions, GET /dashboard/subscriptions/summary (active/paused/failed counts + approximate MRR).
Customer-facing pages
| URL | Method | Purpose |
|---|---|---|
/subscribe/:planId | GET | Enrollment form (plan details, email input, subscribe button). |
/subscribe/:planId | POST | email + customer_ref (both optional) -- creates subscription, redirects to manage page. |
/subscribe/manage/:magicToken | GET | Status + pause/resume/cancel buttons + link to current invoice. |
/subscribe/manage/:magicToken/pause | POST | Flip to PAUSED. |
/subscribe/manage/:magicToken/resume | POST | Flip to ACTIVE, nextBillingAt reset to now. |
/subscribe/manage/:magicToken/cancel | POST | Final. magicToken is rotated so the old URL 404s. |
All public subscription pages are rate-limited at 60 requests/min per IP.
Subscription webhooks
Fire on the merchant's standard webhook URL, HMAC-SHA512 signed with webhookSecret, same headers as every other v2 webhook.
{
"event_type": "subscription.invoiced",
"type": "subscription.invoiced",
"subscription_id": "sub_xxx",
"plan_id": "plan_xxx",
"customer_email": "[email protected]",
"payment_track_id": "cmo79lqkm0005889z3q02j6w8",
"amount_usd": "9.99",
"grace_count": 0,
"pay_url": "https://orbchain.io/pay/cmo79lqkm0005889z3q02j6w8"
}
{
"event_type": "subscription.paid",
"type": "subscription.paid",
"subscription_id": "sub_xxx",
"plan_id": "plan_xxx",
"plan_label": "Pro",
"amount_usd": "149",
"interval_sec": 2592000,
"customer_ref": "tenant_8841", // YOUR reference, set at subscription creation
"customer_email": "[email protected]",
"invoice_track_id": "cmo79lqkm0005889z3q02j6w8",
"paid_amount": "149.00",
"paid_coin": "USDT_TRC20",
"current_period_start": 1786993200,
"current_period_end": 1789585200 // = start + interval_sec; use as plan_expires_at
}
This is the grant/renew cue for subscriptions. Unlike payment.paid, it carries customer_ref (the reference you set when creating the subscription) so you map straight to a tenant with no local lookup table. The linked payment.paid still fires too — dedupe on event_id and act on whichever your integration prefers.
{
"event_type": "subscription.failed",
"subscription_id": "sub_xxx",
"plan_id": "plan_xxx",
"reason": "max_grace_exceeded",
"last_invoice_id": "cmo79lqkm0005889z3q02j6w8"
}
Typical merchant reaction: revoke access to the paid service. Customer can re-enroll from the same /subscribe/:planId link any time.
Balance Endpoints
Get Account Balance
Returns the merchant's per-coin balance. Balances reflect credited deposits minus confirmed payouts and gateway fees. Only coins with a non-zero balance are included.
curl https://orbchain.io/v1/general/account/balance \ -H "general_api_key: 62e1270e345f38fd9a1b2c3d4e5f6a7b"
{
"data": {
"balances": [
{ "coin": "USDT_TRC20", "amount": "4287.50" },
{ "coin": "BTC", "amount": "0.21830000" },
{ "coin": "ETH", "amount": "1.44200000" }
]
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Webhooks
The gateway sends an HTTP POST to your configured webhook_url (set in the dashboard or overridden per-request via callback_url) on every significant payment and payout event.
Request Correlation
Every API request is assigned an X-Request-Id -- either accepted from your inbound header (8--64 chars, [A-Za-z0-9_-] only) or generated as a 16-char hex value when absent. The id is echoed on the response and included in our server logs. Any webhook dispatched as a consequence of the request carries X-Request-Id (header) and request_id (body field), so you can trace "API call -> deposit/payout -> outbound webhook" with one identifier.
curl -H "X-Request-Id: my-ingest-job-abc123" \ -H "merchant_api_key: $KEY" \ https://your-domain.com/v1/payment/static-address -d '{...}' # Response: X-Request-Id: my-ingest-job-abc123
The same id is carried through any downstream processing triggered by the request, so log entries correlate back to the original API call.
Event Headers & Idempotency
Every outbound webhook carries event metadata in both the request headers and the JSON body. The body copy is HMAC-signed; headers are for routing/deduplication without parsing.
| Header | Body field | Description |
|---|---|---|
X-Event-Id | event_id | Stable 26-char Crockford-base32 id, deterministic per logical event. |
X-Event-Type | event_type | payment.paid, payout.confirmed, payout.failed, deposit.credited. |
X-Event-Timestamp | event_timestamp | Unix seconds when the envelope was sealed. |
Idempotency contract: the same event_id is reused across every retry of the same logical event -- both the 5-attempt backoff loop and any tick-level replay after a transient failure. Treat event_id as your idempotency key.
const eventId = req.header("X-Event-Id"); if (await seenBefore(eventId)) return res.status(200).send("dup"); await processEvent(body); await markSeen(eventId); res.status(200).send("ok");
Signature Verification
Always verify the signature before processing a webhook payload. Do not trust unsigned or incorrectly signed requests.
| API Version | Header | Algorithm | Secret |
|---|---|---|---|
| V2 (default) | HMAC | HMAC-SHA512 | Your webhook_secret |
The signature is computed over the raw request body bytes. Use a timing-safe comparison.
Important: the signing secret is webhook_secret (separate field on your merchant profile), never merchant_api_key. If webhook_secret is unset on the merchant record, the gateway refuses to send the webhook rather than fall back to another key.
const crypto = require("crypto"); app.post("/webhooks/orbchain", express.raw({ type: "application/json" }), (req, res) => { const receivedSig = req.header("HMAC") ?? ""; const expectedSig = crypto .createHmac("sha512", process.env.WEBHOOK_SECRET) // NOT your API key .update(req.body) // raw Buffer, not parsed JSON .digest("hex"); const ok = receivedSig.length === expectedSig.length && crypto.timingSafeEqual(Buffer.from(receivedSig), Buffer.from(expectedSig)); if (!ok) { return res.status(401).send("Invalid signature"); } const event = JSON.parse(req.body.toString()); // handle event... res.status(200).send("ok"); });
import hmac import hashlib from flask import Flask, request app = Flask(__name__) WEBHOOK_SECRET = "1802bbed16da84f0667ebd4f21c1a3b9" # NOT your API key @app.route("/webhooks/orbchain", methods=["POST"]) def webhook(): received_sig = request.headers.get("HMAC", "") expected_sig = hmac.new( WEBHOOK_SECRET.encode(), request.get_data(), # raw bytes hashlib.sha512 ).hexdigest() if not hmac.compare_digest(received_sig, expected_sig): return "Invalid signature", 401 event = request.get_json(force=True) # handle event... return "ok", 200
<?php $webhookSecret = getenv('WEBHOOK_SECRET'); // NOT your API key $rawBody = file_get_contents('php://input'); $receivedSig = $_SERVER['HTTP_HMAC'] ?? ''; $expectedSig = hash_hmac('sha512', $rawBody, $webhookSecret); if (!hash_equals($expectedSig, $receivedSig)) { http_response_code(401); exit('Invalid signature'); } $event = json_decode($rawBody, true); // handle $event... http_response_code(200); echo 'ok';
Payment Webhook (V2)
Delivered when a deposit reaches the required confirmation count and the merchant's balance is credited. Also fired with status: "Paying" when a deposit is first seen on-chain but not yet confirmed.
{
"type": "payment",
"track_id": "cmo39n6n10008145nt2fyroas",
"status": "Paid",
"amount": null,
"currency": "USDT_TRC20",
"pay_currency": "USDT_TRC20",
"address": "TRrqEHua7YXsv7HYaDp1chAP6ErPMhR18e",
"order_id": "deposit-session-001",
"paid_at": 1745501210,
"transactions": [
{
"tx_hash": "a0491325689e3e07f81b4e4b...",
"coin": "USDT_TRC20",
"amount": "50.00",
"amount_usd": "50.00",
"status": "CREDITED",
"confirmations": 21
}
]
}
Payout Webhook (V2)
Delivered when a payout reaches Confirmed or Failed.
{
"type": "payout",
"track_id": "cmo3b1xyz9000a145n7qrst8u",
"status": "Confirmed",
"coin": "USDT_TRC20",
"amount": "25.00",
"destination": "TJYeasTPGpTKFR5HFqqPUVaHhtnD51RMRZ",
"tx_hash": "b3c4d5e6f7a8b9c0d1e2f3a4...",
"failure_reason": null
}
{
"type": "payout",
"track_id": "cmo3b1xyz9000a145n7qrst8u",
"status": "Failed",
"coin": "USDT_TRC20",
"amount": "25.00",
"destination": "TJYeasTPGpTKFR5HFqqPUVaHhtnD51RMRZ",
"tx_hash": null,
"failure_reason": "broadcast_timeout"
}
Transfer Webhook (V2) v2.2
Fired on every internal transfer — once to the sender (transfer.sent) and once to the recipient (transfer.received). Same HMAC-SHA512 signing as the Payment webhook, distinct event_id per direction so each party can dedupe independently.
{
"event_id": "01HK4X7V2P9T8R5Q3N1B0M6A4W",
"event_type": "transfer.sent",
"event_timestamp": 1776543645,
"type": "transfer.sent",
"transfer_id": "cmo4s7tvh0003jak6gl2bj5wf",
"from_account_id": "ORB-4UuyB85qfXhYGvTP9CZS",
"to_account_id": "ORB-3qvxq5yVwnS6c3c3Eycj",
"coin": "USDT_TRC20",
"amount": "25.00",
"created_at": 1776543645
}Webhook delivery is fire-and-forget relative to the ledger — if the webhook fails to deliver the transfer still succeeds (source-of-truth is the InternalTransfer audit row). Reconcile against GET /dashboard/transfers if you miss a webhook.
Retries & Idempotency
Retry Behavior
- The gateway attempts delivery 5 times with exponential backoff: 2s, 4s, 8s, 16s, 32s.
- Your endpoint must return an HTTP
2xxstatus within 10 seconds. - If all 5 attempts fail the webhook is re-attempted on the next monitor scan (~30 seconds later).
- Delivery history is available in the dashboard under Webhook Logs.
Idempotency
Your endpoint must be idempotent -- the same webhook may be delivered more than once due to retries and monitor re-scans. Deduplicate on: track_id + status for payment webhooks, and track_id for payout webhooks.
Dashboard API
The Dashboard API provides account management, reporting, and settings management using a JWT obtained at login. Pass the token in every request:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWTs expire after 24 hours. Re-authenticate via /dashboard/auth/login.
Sign Up
Create a new merchant account. API keys are generated automatically and returned once.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| string | required | Valid email address. Must be unique. | |
| password | string | required | 8-128 characters. |
| turnstile_token | string | optional | Bot-protection CAPTCHA token. Required when the signup form renders a CAPTCHA widget. |
curl https://orbchain.io/dashboard/auth/signup \ -X POST \ -H "Content-Type: application/json" \ -d '{ "email": "[email protected]", "password": "securePassword123" }'
{
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"merchant_id": "cmo39mvd8000014n5abc123xy",
"email": "[email protected]",
"api_keys": {
"merchant_api_key": "1802bbed16da84f0667ebd4f21c1a3b9",
"payout_api_key": "6bcebc3adb04752d889f3a01c2d4e5f6",
"general_api_key": "62e1270e345f38fd9a1b2c3d4e5f6a7b"
}
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Important: The three API keys are shown only once at signup. Store them in your secrets manager immediately. They cannot be retrieved later -- only rotated.
Error Cases
- 400
validation error: ...-- invalid email or password too short - 403
captcha verification failed-- invalid CAPTCHA token - 409
email already registered
Login
Exchange email + password for a JWT.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| string | required | Registered email address. | |
| password | string | required | Account password. |
| turnstile_token | string | optional | CAPTCHA token (when the form renders a bot-protection widget). |
{
"data": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"merchant_id": "cmo39mvd8000014n5abc123xy",
"email": "[email protected]"
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Error Cases
- 401
invalid credentials - 403
captcha verification failed
Google OAuth
Redirects the browser to the Google OAuth consent screen. Not a JSON API -- this is a browser redirect.
OAuth Flow
- Send the user's browser to
GET /dashboard/auth/google. - Google redirects back to
/dashboard/auth/google/callbackwith acodeparameter. - The server exchanges the code for tokens, creates or logs in the merchant account, then redirects to
/app#google-auth=<encoded-payload>. - The frontend reads the fragment, stores the JWT, and redirects to the dashboard.
For a new account the fragment includes api_keys. For an existing account it contains only token, merchant_id, and email.
Get Account
Returns the merchant's profile including public account_id (used for internal transfers v2.2), webhook URL, fee rate, accepted coins, per-coin confirmation thresholds, and MFA status.
{
"data": {
"id": "cmo39mvd8000014n5abc123xy",
"account_id": "ORB-4UuyB85qfXhYGvTP9CZS",
"email": "[email protected]",
"webhook_url": "https://yoursite.com/webhooks/orbchain",
"fee_percent": "0.5",
"accepted_coins": ["BTC", "USDT_TRC20", "SOL"],
"confirmations": {
"BTC": 2, "LTC": 6, "ETH": 12,
"USDT_ERC20": 12, "USDT_TRC20": 19, "SOL": 32
},
"totp_enabled": false,
"enabled": true,
"created_at": 1745500000
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Get Balances
Returns per-coin merchant balances. Equivalent to /v1/general/account/balance but authenticated with a JWT.
{
"data": {
"balances": [
{ "coin": "USDT_TRC20", "amount": "4287.50" },
{ "coin": "BTC", "amount": "0.21830000" }
]
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Get Transactions
Returns recent deposits and payouts for the merchant.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| limit | integer | 100 | Max records (1-500). Applies independently to deposits and payouts. |
{
"data": {
"deposits": [
{
"id": "dep_abc123",
"coin": "USDT_TRC20",
"amount": "50.00",
"amountUsd": "50.00",
"status": "CREDITED",
"confirmations": 21
}
],
"payouts": [
{
"id": "pay_xyz789",
"coin": "USDT_TRC20",
"destination": "TJYeas...",
"amount": "25.00",
"status": "CONFIRMED"
}
]
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
List Payments (Dashboard)
List the merchant's payments. Accepts the same filters as GET /v1/payment.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| status | string | -- | WAITING, PAYING, PAID, EXPIRED, CANCELLED |
| type | string | -- | STATIC_ADDRESS, WHITE_LABEL, INVOICE |
| currency | string | -- | Filter by coin symbol. |
| limit | integer | 20 | Max per page. |
| offset | integer | 0 | Pagination offset. |
Response shape is identical to GET /v1/payment.
Get Payment Detail (Dashboard)
Fetch a single payment by track_id. Response shape is identical to GET /v1/payment/:track_id.
List Payouts (Dashboard)
List the merchant's payouts.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| limit | integer | 20 | Max per page (1-200). |
| offset | integer | 0 | Pagination offset. |
{
"data": {
"payouts": [
{
"id": "pay_xyz789",
"trackId": "cmo3b1xyz9000a145n7qrst8u",
"coin": "USDT_TRC20",
"destination": "TJYeas...",
"amount": "25.00",
"status": "CONFIRMED",
"txHash": "b3c4d5..."
}
],
"total": 37,
"limit": 20,
"offset": 0
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Get API Keys (Masked)
Returns masked versions of the three API keys (last 8 characters visible). Use this to display key hints in the UI without exposing full values.
{
"data": {
"merchant_api_key": "••••••••21c1a3b9",
"payout_api_key": "••••••••c2d4e5f6",
"general_api_key": "••••••••4e5f6a7b"
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
API Keys (Full List) v2.3
Same endpoint, extended response. Returns the three legacy masked keys and a full keys[] array containing every ApiKey row for the merchant (custom keys + the three legacy seeds). This is what drives the Merchants and Payouts tab "API Keys List" tables.
Per-row fields: id, label, prefix (first 8 chars of the plaintext — the only part retained server-side beyond the SHA-256 hash), scopes, coin_allowlist, ip_allowlist, webhook_url, rate_limit_rpm, last_used_at, revoked_at, expires_at, created_at, and is_legacy. Legacy rows are flagged so the UI can badge them and disallow revocation.
Create API Key v2.3
Creates a scoped API key. The plaintext token is returned exactly once — only the SHA-256 hash and the 8-char prefix are persisted. Copy it at the moment of creation.
Body
| Field | Type | Required | Description |
|---|---|---|---|
label | string (1–80) | yes | Human-readable identifier. |
scopes | string[] | yes | At least one of: payment:create, payment:read, payout:create, payout:read, balance:read, transfer:create. |
coin_allowlist | string[] | no | If non-empty, the key can only name a coin inside this list (else 403 coin_not_allowed). Empty/absent = all accepted coins. |
ip_allowlist | string[] | no | CIDRs (e.g. 1.2.3.4/32, 10.0.0.0/8). Calls from any other IP get 401 ip_not_allowed. |
webhook_url | string (URL) | no | Optional per-key webhook override (PK-T4). |
rate_limit_rpm | integer (1–10000) | no | Per-key write rate-limit override (PK-T5). |
expires_at | integer (unix sec) | no | Absolute expiry; past-expiry keys return 401. |
{
"data": {
"api_key": "orb_live_7d2f4a8c9b1e6d30f5a7b9c2e4d6f8a0",
"id": "cmo5hy21u0002c6cszq9m0epq",
"label": "storefront-prod",
"prefix": "orb_live",
"scopes": ["payment:create", "payment:read"],
"created_at": 1713525000
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Revoke API Key v2.3
Immediately revokes the key. Subsequent API calls using it return 401. Legacy seed rows (id starts with legacy_) cannot be revoked — creating a replacement first is required. Returns {ok:true, already_revoked:true} if the row was already revoked.
Scopes Matrix v2.3
| Scope | Routes |
|---|---|
payment:create | POST /v1/payment/static-address, /white-label, /invoice |
payment:read | GET /v1/payment/accepted-currencies, /:track_id, /:track_id/receipt, /v1/payment |
payout:create | POST /v1/payout |
payout:read | GET /v1/payout/:track_id |
balance:read | GET /v1/general/account/balance |
transfer:create | POST /v1/transfer |
refund:create | Reserved (future phase). |
The three legacy seed keys carry defaults so existing integrations keep working unchanged:
legacy_merchant_*:[payment:create, payment:read, transfer:create]legacy_payout_*:[payout:create, payout:read]legacy_general_*:[balance:read]
New error classes on authed routes
403 insufficient_scope: <scope> required— key lacks the scope the route requires.403 coin_not_allowed: <COIN> not in key allow-list— body coin is outsidecoin_allowlist.401 ip_not_allowed— caller IP is not inside any CIDR inip_allowlist.
Auto-withdraw Rules v2.3
Per-coin threshold rules. Merchant balances are checked on a 5-minute cadence; a Payout is issued for each rule that meets its threshold. A 30-minute cooldown per (merchant, coin) pair prevents retry loops after a failed broadcast. Max 20 rules.
Rule fields
| Field | Type | Notes |
|---|---|---|
coin | string | Coin enum value. |
threshold_native | string (decimal) | Native units. Rule fires when balance ≥ threshold. |
destination | string (8–200) | External address. Validated against the same burn-list + address-format guards the payout API uses. |
{
"rules": [
{ "coin": "BTC", "threshold_native": "0.001", "destination": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh" },
{ "coin": "USDT_TRC20", "threshold_native": "100", "destination": "TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8" }
]
}
Idempotency keys use the normalized amount string so two balance reads differing only in trailing zeros dedupe. Auto-withdraw respects the payout backlog circuit breaker.
Referrals v2.3
Affiliate / referral program stats. Shareable /app?ref=ORB-… link, referee list, and running total of earnings. Referrer earns 20% of each referee's platform fees for 12 months after signup.
Linking happens at signup: POST /dashboard/auth/signup accepts an optional ref field (a referrer's ORB-XXXXXXXX). Bad or unknown values are silently ignored — signup never fails on a malformed referral code.
{
"data": {
"referral_link": "https://orbchain.io/app?ref=ORB-2dhS7GvRmyaytuMoZ2PY",
"referral_rate_bps": 2000,
"eligibility_months": 12,
"referees_count": 1,
"total_earnings_usd": "100.00",
"referees": [
{
"account_id": "ORB-41CzvzFjitZEVpuBK1Jz",
"signed_up_at": 1713600000,
"eligibility_ends_at": 1745136000,
"enabled": true,
"fees_usd": "500.00",
"your_earnings_usd": "100.00"
}
]
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Only deposits with status = CREDITED count toward fees_usd. Disabled referees stay in the list — they may re-enable. Payout of the accrued balance is a manual admin operation today.
Webhook Logs
Returns webhook delivery history with status codes, attempt counts, and response previews.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| limit | integer | 50 | Max records (1-200). |
| offset | integer | 0 | Pagination offset. |
{
"data": {
"logs": [
{
"id": "wlog_abc123",
"url": "https://yoursite.com/webhooks/orbchain",
"status_code": 200,
"success": true,
"attempt": 1,
"error": null,
"payload_preview": "{\"type\":\"payment\",...}",
"response_preview": "ok"
}
],
"total": 214,
"limit": 50,
"offset": 0
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Analytics
Returns a 30-day rolling daily breakdown of deposit and payout volume in USD, plus 30-day totals.
{
"data": {
"daily": [
{
"date": "2026-03-16",
"deposits_usd": 1200.00,
"payouts_usd": 400.00,
"deposit_count": 8,
"payout_count": 2
}
],
"totals": {
"total_deposits_usd": "48250.75",
"total_payouts_usd": "12100.00",
"total_deposit_count": 312,
"total_payout_count": 87
}
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Create Payment (Dashboard)
Create a payment from the dashboard. Supports all three payment types in a single endpoint.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| type | string | required | static_address, white_label, or invoice |
| currency | string | conditional | Required for static_address and white_label. |
| amount | string | conditional | Required for white_label and invoice. |
| pay_currency | string | optional | For white_label only. Defaults to currency. |
| user_id | string | optional | Max 128 chars. |
| order_id | string | optional | Max 128 chars. |
| description | string | optional | Max 500 chars. |
| lifetime | integer | optional | Minutes until expiry (1-43200). |
| callback_url | string | optional | Must be a valid URL. |
| return_url | string | optional | For invoice type only. |
| string | optional | For invoice type only. |
Response shape matches the corresponding /v1/payment/* endpoint.
Create Payout (Dashboard)
Initiate a payout from the dashboard (JWT-authenticated).
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| currency | string | required | Coin to send. |
| address | string | required | Destination on-chain address. |
| amount | string | required | Amount in coin units. Must be > 0. |
| description | string | optional | Max 500 chars. |
Error Cases
- 400
insufficient balance - 400
invalid destination address - 400
destination is a burn address
Update Webhook URL
Update the merchant's default webhook URL. Private/internal IP addresses are rejected.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| webhook_url | string | required | Valid https:// or http:// URL. Max 500 chars. Private/internal addresses rejected. |
curl https://orbchain.io/dashboard/settings/webhook \ -X PUT \ -H "Content-Type: application/json" \ -H "Authorization: Bearer eyJhbGci..." \ -d '{"webhook_url":"https://yoursite.com/webhooks/orbchain"}'
Update Accepted Coins
Update the list of coins the merchant accepts. Must include at least one coin.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| accepted_coins | string[] | required | One or more of: BTC, LTC, ETH, USDT_ERC20, USDT_TRC20, SOL |
curl https://orbchain.io/dashboard/settings/coins \ -X PUT \ -H "Content-Type: application/json" \ -H "Authorization: Bearer eyJhbGci..." \ -d '{"accepted_coins":["BTC","USDT_TRC20","SOL"]}'
Update Confirmation Thresholds
Override per-coin confirmation thresholds. Platform minimums are enforced -- values below the floor are rejected.
Platform Minimums
| Coin | Minimum |
|---|---|
| BTC | 2 |
| LTC | 6 |
| ETH | 12 |
| USDT_ERC20 | 12 |
| USDT_TRC20 | 3 |
| SOL | 32 |
curl https://orbchain.io/dashboard/settings/confirmations \ -X PUT \ -H "Content-Type: application/json" \ -H "Authorization: Bearer eyJhbGci..." \ -d '{"BTC": 3, "USDT_TRC20": 19}'
Change Password
Change the account password. Requires the current password for verification.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| current_password | string | required | Current account password. |
| new_password | string | required | New password (8-128 characters). |
Error Cases
- 400
validation error: ...-- new password too short - 401
current password is incorrect
Rotate API Key
Rotate one of the three API keys. The new key is returned once and cannot be retrieved again. The previous key is invalidated immediately.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| key_type | string | required | One of: merchantApiKey, payoutApiKey, generalApiKey |
{
"data": {
"key_type": "merchantApiKey",
"new_key": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6"
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Important: Update any systems using the old key before rotating. There is no grace period.
MFA Setup
Generate a TOTP secret and QR code to configure an authenticator app. MFA is not enabled until you call /dashboard/mfa/verify.
{
"data": {
"secret": "JBSWY3DPEHPK3PXP",
"otpauth_url": "otpauth://totp/OrbChain:merchant%40example.com?secret=...",
"qr_code": "data:image/png;base64,iVBORw0KGgoAAAA..."
},
"message": "OK",
"status": 200,
"version": "2.0.0"
}
Error Cases
- 400
MFA is already enabled
MFA Verify
Confirm the TOTP code from the authenticator app and activate MFA on the account.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| code | string | required | 6-digit TOTP code from the authenticator app. |
Error Cases
- 400
run /dashboard/mfa/setup first-- no pending secret - 400
MFA is already enabled - 401
invalid TOTP code
MFA Disable
Disable MFA on the account. Requires the current TOTP code as proof of possession.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
| code | string | required | 6-digit TOTP code from the authenticator app. |
Error Cases
- 400
MFA is not enabled - 401
invalid TOTP code
Payment Links v2.1
Shareable no-code invoices. Each link resolves to a URL that mints a fresh invoice on every visit. Fixed-amount, fiat-priced, or buyer-entered.
| Field | Type | Required | Description |
|---|---|---|---|
| coin | string | required | Coin to receive. |
| amount | string | optional | Fixed native-coin amount. Mutually exclusive with amount_fiat. Omit both for a buyer-entered link. |
| amount_fiat | string | optional | Fixed fiat-priced amount. Requires fiat_currency. |
| fiat_currency | string | optional | USD, EUR, GBP, JPY, CAD, AUD, CHF, CNY, INR, RUB. |
| lifetime | integer | optional | Minutes each minted invoice stays open (1-43200). |
| description | string | optional | Max 500 chars. |
{
"data": {
"id": "cmo4455ud000lt3pbd8xpnetn",
"slug": "AuCU2vJS3nCd",
"url": "https://orbchain.io/l/AuCU2vJS3nCd",
"coin": "USDT_TRC20",
"amount_fiat": "50",
"fiat_currency": "USD",
"enabled": true,
"uses": 0
}
}
List all links owned by the merchant (newest first), each with its full url.
Toggle the enabled flag: {"enabled": false}. Disabled links return 410 Gone on resolution.
Link Embed Snippet v2.1
Return a paste-ready donation-button snippet built against the caller's request host. Optional ?label=Support+us customizes the button text.
{
"data": {
"slug": "AuCU2vJS3nCd",
"url": "https://orbchain.io/l/AuCU2vJS3nCd",
"snippet": "<script src=\"https://orbchain.io/assets/donate.js\" async></script>\n<div class=\"orbchain-donate\" data-slug=\"AuCU2vJS3nCd\" data-label=\"Donate crypto\"></div>"
}
}
Payout Schedules v2.1
Automatic recurring payouts. Enabled schedules are checked every minute; when nextRunAt has passed and the merchant's balance is at least min_amount, a full-balance payout is issued through the standard payout path — webhook retries and failure re-credit behave the same as manual payouts.
| Field | Type | Required | Description |
|---|---|---|---|
| coin | string | required | Coin to withdraw. |
| destination | string | required | Validated per-coin (rejects burn addresses + malformed). |
| min_amount | string | required | Skip the cycle if balance is below this. |
| interval_sec | integer | required | Cadence in seconds. Floor 3600 (1h), ceiling 2592000 (30d). |
{
"data": {
"id": "cmo445l710018t3pbgwbna4o0",
"coin": "BTC",
"destination": "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
"min_amount": "0.001",
"interval_sec": 3600,
"enabled": true,
"last_run_at": null,
"last_error": null,
"next_run_at": 1776506830
}
}
nextRunAt is advanced before each payout attempt, so a stuck payout never causes a tight retry loop. Failures capture to last_error and resume on the next cycle. Idempotency key is sched:<id>:<minute>, so duplicate ticks within the same minute deduplicate.
Patch any subset of {enabled, min_amount, interval_sec}.
In-flight payouts from a prior tick complete; only future ticks are stopped.
Error Cases
- 400
invalid_address - 400
burn_address - 400
invalid_min_amount - 400
invalid_interval
Customer-Static Mode v2.1
Toggle per-customer static addresses. When enabled, every call to the address-derivation path for the same (user_id, coin) returns the same address regardless of the caller's TTL. When disabled (default), each invoice derives a fresh address once the prior TTL expires.
Static-mode addresses persist with expiresAt = NULL in the database -- they survive invoice expiry.
curl https://orbchain.io/dashboard/settings/customer-static-mode \ -X PUT \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{"enabled": true}'
Brands (Multi-Brand) v2.5
Run multiple customer-facing identities on one account. Each brand carries its own checkout identity (name, logo, theme, color) and optionally its own webhook endpoint + signing secret. Funds stay unified in the account-level balance. Every account has an auto-created default brand; DELETE archives (history and tags stay queryable, the identity stops being usable for new payments). GET /dashboard/brands?archived=1 includes archived brands.
Request Body (POST / PUT — snake_case and camelCase both accepted)
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | required | Operator-facing label, 1-80 chars. |
| slug | string | optional | URL-safe, max 40 chars, unique within your account. Auto-derived from name when omitted. default is reserved. |
| logo_url | string | optional | Absolute URL, rendered on the hosted checkout. |
| theme_mode | string | optional | LIGHT | DARK. Falls back to account default when null. |
| brand_color | string | optional | Hex color, #RRGGBB. |
| webhook_url | string | optional | Per-brand webhook endpoint. Unset → inherits the account webhook. Same SSRF validation as the account-level URL. |
| webhook_secret | string | optional | 16-128 chars. When set, this secret signs that brand's webhooks (HMAC-SHA512) instead of the account secret. |
Brand-scoped API keys: pass brand_id when creating a scoped API key. Every payment created with that key is tagged to the brand automatically — it renders the brand's checkout identity, fires webhooks at the brand's endpoint (signed with the brand's secret when set), and shows up brand-tagged in listings and exports.
Portfolio (USD-Aggregated) v2.2
Total USD across every accepted coin, plus per-coin breakdown with current spot, 24h change, and USD value. Powers the Portfolio card in the dashboard home.
{
"data": {
"account_id": "ORB-4UuyB85qfXhYGvTP9CZS",
"total_usd": "4217.33",
"per_coin": [
{ "coin": "USDT_TRC20", "amount": "4000.00", "spot_usd": "1.00", "change_24h_pct": "0.00", "value_usd": "4000.00" },
{ "coin": "BTC", "amount": "0.00287", "spot_usd": "75752.00", "change_24h_pct": "1.82", "value_usd": "217.33" }
]
}
}Zero-balance coins still appear so the UI can render them greyed out. Spot prices are cached 60s, stale-tolerated 10 min.
Receive Address (single coin, fast path) v2.3
Single-coin fast path for the two-step coin → network → QR Receive picker shared by the dashboard and the Telegram Mini App. Derives-or-reuses a stable per-merchant address (no TTL) on the requested coin. Render the QR client-side via <img src="/tgapp/qr?data=<address>"> — the QR endpoint is same-origin and public.
Query Parameters
| Param | Required | Description |
|---|---|---|
coin | yes | One of the merchant's acceptedCoins. XMR returns 400 xmr_receive_unsupported — XMR addresses are minted per-invoice via the swap provider, see Create White Label Payment. |
Response (200)
{
"data": { "coin": "USDT_TRC20", "address": "T…NBb7u" },
"message": "OK", "status": 200, "version": "2.0.0"
}
Errors: 400 missing_coin, 400 coin_not_accepted, 400 xmr_receive_unsupported, 401 unauthorized.
Deposit Addresses + QRs v2.2
Long-lived deposit address per accepted coin, with a pre-rendered PNG QR code. Idempotent — the same addresses are returned across calls. Use to render a merchant-facing "Receive" dialog.
{
"data": {
"account_id": "ORB-4UuyB85qfXhYGvTP9CZS",
"addresses": [
{ "coin": "BTC", "address": "bc1qjzlwzx7f68qj4em6tvetx500764jssygcswfhh", "qr": "data:image/png;base64,iVBORw..." },
{ "coin": "USDT_TRC20", "address": "TMwkahRUF6TtDW1Z8WmdZbrrEyQmkpCMBW", "qr": "data:image/png;base64,iVBORw..." }
]
}
}The qr field is a data:image/png;base64,… URI that can be used directly as an <img> src.
Internal Transfer v2.2
Instant, zero-fee, zero-confirmation transfer between two OrbChain merchants by account_id. Atomic debit + credit + audit row in a single transaction. The same operation is available as POST /v1/transfer under merchant_api_key.
{
"to_account_id": "ORB-3qvxq5yVwnS6c3c3Eycj",
"coin": "USDT_TRC20",
"amount": "25.00"
}{
"data": {
"transfer_id": "cmo4s7tvh0003jak6gl2bj5wf",
"to_account_id": "ORB-3qvxq5yVwnS6c3c3Eycj",
"coin": "USDT_TRC20",
"amount": "25.00",
"created_at": 1776543645
}
}Errors: 400 self_transfer_forbidden, 400 recipient_not_found, 400 recipient_disabled, 400 insufficient_balance. Both parties receive webhooks (transfer.sent / transfer.received) — see the Webhooks section.
Transfer History v2.2
Merchant's internal transfers (both sent and received). Query param limit (default 100, max 500).
Balance Swap Quote v2.2
Preview a balance swap without debiting. Returns a live rate from the configured swap provider.
{ "from_coin": "BTC", "to_coin": "USDT_TRC20", "amount": "0.01" }{
"data": {
"venue": "<swap_provider>",
"from_amount": "0.01", "to_amount": "742.10",
"rate": "74210.23",
"min_from_amount": "0.0001679",
"valid_until": "2026-04-18T21:05:00.000Z",
"floating_rate": true
}
}Floating-rate swaps drift. When floating_rate: true, the actual to_amount at settlement may drift ~2% from the quote. Check actual_to_amount on GET /dashboard/swap/:id after confirmation for the real credited amount — never trust to_amount_quoted for reconciliation.
Create Balance Swap v2.2
Atomically debits the source coin and schedules the swap. On provider failure or timeout the source coin is fully credited back — merchants never lose balance to a failed swap.
Progression: QUOTED → PENDING → CONFIRMED (credit landed) or FAILED (refund issued). Poll via GET /dashboard/swap/:id.
Get Balance Swap v2.2
Scoped to the caller — returns 404 if the swap belongs to another merchant (prevents enumeration). Returns status, actual_to_amount, venue tx hashes (payin_hash, payout_hash, refund_hash), and failure_reason.
Swap History v2.2
Merchant's balance-swap history. Query param limit (default 50, max 200).
Telegram Integration v2.3
The @orbchain_bot Telegram bot plus its Mini App webview give merchants a mobile-native wallet experience. Two merchant-facing flows cross into OrbChain: the browser-link flow that binds a Telegram chat to an existing merchant account, and the Mini App (WebApp) that authenticates automatically via Telegram's signed initData.
Auth model
| Surface | Auth |
|---|---|
GET /tg/link/start?t=<token> | Self-contained HMAC token (<chatId>.<exp>.<hmac>), 10-min TTL. |
POST /dashboard/tg/link/complete | Dashboard JWT + link token. |
GET /tgapp | None (HTML only, strict CSP). |
POST /tgapp/session | Telegram WebApp initData — HMAC-SHA256 verified server-side per Telegram's documented scheme. |
GET /tgapp/* | Authorization: Bearer <tgapp JWT> (src="tgapp", 1h TTL). |
GET /tgapp/qr | None — content is a public address. |
Browser Link — Start v2.3
Entry point from the bot's 🔗 Link in browser inline button. Verifies the token, then redirects to the dashboard with the token in a URL fragment so it survives a login round-trip. On success, 302 Location: /app#tg-link=<token>. On failure, a friendly HTML error page ("Open the bot again and tap the button — links are valid for 10 minutes.").
Browser Link — Complete v2.3
Confirms the binding. Verifies the link token server-side, sets merchant.telegramChatId, and fires a ✅ confirmation message back to the Telegram chat.
Request Body
{ "token": "<link token from /tg/link/start>" }
Response (200)
{ "data": { "ok": true, "account_id": "ORB-…" } }
Errors
400 link_expired/link_invalid/not_configured409 chat_already_linked_elsewhere— different merchant owns this chat; run/unlinkthere first.
Browser Link — Status v2.3
Idempotent probe — used by the dashboard to decide whether to show "Link Telegram" vs. "Already linked".
{
"data": {
"linked": true,
"linked_at": 1776612739,
"chat_id": "342073830"
}
}
Mini App — Session v2.3
Exchanges Telegram initData for a short-lived tgapp JWT (claims: { m, tg, src: "tgapp" }, 1h TTL).
Request Body
{ "init_data": "<raw string from Telegram.WebApp.initData>" }
Response (200)
{
"data": {
"token": "<JWT>",
"expires_in": 3600,
"merchant": {
"id": "cmo…",
"account_id": "ORB-…",
"email": "[email protected]",
"default_settlement_coin": "USDT_TRC20",
"accepted_coins": ["BTC", "USDT_TRC20", "XMR", "…"]
}
}
}
Errors
401 init_data_missing_hash/bad_hmac/expired/missing_user/malformed_user403 not_linked— Telegram user hasn't bound a merchant yet (run the browser-link flow).404 not_configured— Telegram integration not enabled on this deployment.
Mini App — Balances v2.3
{ "data": { "balances": [{ "coin": "USDT_TRC20", "amount": "4.33" }] } }
Mini App — Payments v2.3
Returns the 10 most recent payments for the authed merchant. Shape mirrors formatPaymentResponse but trimmed.
Mini App — Receive v2.3
Derives-or-reuses a stable deposit address for the selected coin + returns a human-readable network label ("Tron (TRC-20)", "Ethereum (ERC-20)", etc.). Used by the Mini App's two-step coin → network → QR picker.
{
"data": {
"coin": "USDT_TRC20",
"address": "T…NBb7u",
"network_label": "Tron (TRC-20)"
}
}
Errors: 400 missing_coin / coin_not_accepted / xmr_receive_unsupported. XMR has no standing receive address — its customer pay-flow mints a payin address per-invoice via the swap provider, see Create White Label Payment.
Mini App — Prices v2.3
USD price + 24h change for every coin the merchant accepts. Fuels the 24h % pills on the Mini App asset list.
{
"data": {
"BTC": { "usd": 75759, "change_24h": -0.47 },
"USDT_TRC20": { "usd": 1, "change_24h": 0 },
"SOL": { "usd": 86.18, "change_24h": 1.73 }
}
}
Mini App — QR v2.3
Server-rendered QR code returned as SVG. Same-origin so it fits the strict CSP on /tgapp and on the dashboard Receive modal without loosening img-src. 5-minute Cache-Control. Rejects data payloads over 512 chars. Response Content-Type: image/svg+xml.
Mini App — In-app 2FA enrollment v2.3
Merchants can enroll 2FA without leaving Telegram. POST /tgapp/mfa/setup returns a fresh TOTP secret plus an otpauth:// URI; /tgapp/qr renders the URI so the QR is scannable directly inside the Mini App. Merchant then submits a 6-digit code to POST /tgapp/mfa/verify which flips totpEnabled=true and unlocks Send + Swap. Disable is deep-linked to the dashboard's Security settings.
// POST /tgapp/mfa/setup response
{
"data": {
"secret": "MKPR2K3GDFAD6VJLJBX6C4DGB57HR45I",
"otpauth": "otpauth://totp/OrbChain:[email protected]?secret=…&issuer=OrbChain&…"
}
}
// POST /tgapp/mfa/verify
{ "code": "123456" }
// → { "data": { "ok": true } }
Mini App — Send (payout / transfer) v2.3
Unified send endpoint. Auto-detects destination shape: ORB-<20 base58> → internal transfer (instant, zero-fee); anything else → on-chain payout validated by chain-specific rules. Every payout requires a 6-digit authenticator code — mobile wallets without a second factor are a footgun.
{
"currency": "USDT_TRC20",
"destination": "T…NBb7u",
"amount": "12.5",
"totp_code": "123456"
}
// on-chain response
{
"data": {
"kind": "onchain",
"track_id": "…",
"status": "CONFIRMING",
"coin": "USDT_TRC20",
"amount": "12.5",
"destination": "T…NBb7u"
}
}
Errors: 403 totp_setup_required | merchant_disabled, 401 totp_invalid, 400 insufficient_balance | invalid_address | burn_address | recipient_not_found | self_transfer | coin_not_supported.
Mini App — Swap v2.3
Thin auth proxies over the balance-swap backend. Quote is a pure read; commit debits the merchant's balance atomically and schedules execution. When the provider returns a floating rate, the Mini App surfaces a warning via floating_rate: true.
// POST /tgapp/swap/quote
{ "from_coin": "USDT_TRC20", "to_coin": "BTC", "amount": "100" }
// → {
// "venue": "",
// "from_amount": "100", "to_amount": "0.0012547",
// "rate": "0.000012547",
// "min_from_amount": "16.209336",
// "valid_until": "2026-04-19T…Z",
// "floating_rate": true
// }
// POST /tgapp/swap (body + totp_code)
{ "from_coin": "USDT_TRC20", "to_coin": "BTC",
"amount": "100", "totp_code": "123456" }
// → {
// "swap_id": "bs_…",
// "from_coin": "USDT_TRC20", "from_amount": "100",
// "to_coin": "BTC", "to_amount_quoted": "0.0012547",
// "status": "QUOTED"
// }
Mini App — Unlink v2.3
Mirrors the bot's /unlink command. Clears merchant.telegramChatId + telegramLinkedAt; the Mini App closes itself on success via Telegram.WebApp.close().
Hosted Payment Page
Returns a server-rendered HTML payment page. This is the URL returned as payment_url by POST /v1/payment/invoice.
Behavior
- Displays the deposit address, expected amount, currency, and current status.
- Includes a one-click copy button for the address.
- Auto-refreshes every 15 seconds while in a non-terminal state (
WaitingorPaying). - Stops polling once the status reaches
Paid,Expired, orCancelled. - Returns a
404HTML response if thetrackIddoes not exist.
https://orbchain.io/pay/cmo39n6oe000k145n4chw827o
Redirect your user to this URL immediately after creating an invoice payment. The page is mobile-friendly and works without JavaScript for address display.
No-JS fallback v2.3
The page is now fully functional with JavaScript disabled. A <meta http-equiv="refresh" content="15"> tag drives polling on non-terminal states, and the buyer coin picker (pay-with-any-coin flow) is wrapped in a real <form method="GET" action="/pay/:trackId"> with a <noscript> submit button. Without JS, selecting a coin and clicking Update submits GET /pay/:trackId?pay_currency=<COIN>; the server mutates the Payment and 303-redirects back to the clean URL. Invalid pay_currency values are silently ignored (no 5xx from scraped input). The JS fast-path still wins when enabled.
Tip / Donation Page v2.3
Shareable public tip page per merchant. The merchant's ORB-XXXXXXXX account ID (created at signup, surfaced on /dashboard/account) is the public slug. No auth; rate-limited at 60 requests/minute per IP.
Query parameters
| Parameter | Description |
|---|---|
coin | Optional. Coin to render (must be in the merchant's accepted_coins; otherwise the first accepted coin is used). |
Behavior
- Renders a branded page — theme, brand color, logo, description all inherited from the merchant's dashboard settings.
- Shows a QR + copy-able deposit address for the selected coin.
- Coin picker:
<form method="GET">with a<noscript>submit fallback (same no-JS pattern as/pay). - Credits land on the merchant's own ledger via the
__orbchain_self__static-address pool — a tip arrives at the same address the merchant sees in their Receive dialog. - Public name shown is the
accountId. The merchant email is never rendered. - Returns
404if the merchant doesn't exist or is disabled.
https://orbchain.io/tip/ORB-4UmDRKEadWBi1uyfUDpg https://orbchain.io/tip/ORB-4UmDRKEadWBi1uyfUDpg?coin=ETH
Merchants can post the URL anywhere — Twitter bio, blog, stream overlay — with no technical integration required.
Payment Link Resolver v2.1
Resolves a payment link into a fresh invoice.
- Enabled + fixed amount -> creates a white-label Payment and
302-redirects to/pay/:trackId. - Enabled + buyer-entered -> expects
?amount=<decimal>. If missing, returns a minimal HTML form that submits back with?amount=.... - Any buyer-provided
amounton a fixed-amount link is ignored -- prevents client-side underpayment games. - Each successful resolution increments the link's
usescounter. - Disabled link ->
410 Gone. Missing slug ->404.
GET /l/AuCU2vJS3nCd -> 302 Location: /pay/cmo44563a000wt3pblhj5ctof GET /l/D34ZhcuNC1s8 -> 200 HTML (amount prompt for buyer-entered link) GET /l/D34ZhcuNC1s8?amount=0.01 -> 302 Location: /pay/cmo44564h0014t3pb1bztueuq GET /l/disabled-slug -> 410 Gone
Donation Button Embed v2.1
A one-line JavaScript embed that renders a donation button on any website. Reads data-* attributes from a mount <div>, builds the donation URL from the link's slug, and opens it in a popup (falls back to same-tab redirect when popups are blocked).
No API key required on the embedding site -- merchant identity is carried by the slug. Grab a ready-to-paste snippet via GET /dashboard/payment-links/:id/embed.
<script src="https://orbchain.io/assets/donate.js" async></script> <div class="orbchain-donate" data-slug="AuCU2vJS3nCd" data-label="Donate crypto"></div>
Supported attributes
| Attribute | Default | Description |
|---|---|---|
| data-slug | -- | Required. Payment-link slug. |
| data-label | Donate crypto | Button text. |
| data-color | #f59e0b | CSS color for the button background. |
| data-target | popup | popup (480x720 window) or blank (new tab). |
| data-amount | -- | Prefill for buyer-entered links. Ignored for fixed-amount links. |
| data-origin | script's origin | Override the gateway host (testing/staging). |
SPA / framework integration
window.OrbchainDonate.mount(el); // mount one element window.OrbchainDonate.mountAll(); // rescan the DOM for any .orbchain-donate[data-slug]
Elements are idempotent -- each is tagged with data-orbchain-mounted after first render.
Supported Coins
| Coin | Symbol | Chain | Decimals | Min Confirmations | Min Payout |
|---|---|---|---|---|---|
| Bitcoin | BTC | bitcoin | 8 | 2 | 0.0001 |
| Litecoin | LTC | litecoin | 8 | 6 | 0.001 |
| Dogecoin v2.2 | DOGE | dogecoin | 8 | 6 | 10 |
| Ethereum | ETH | ethereum | 18 | 12 | 0.001 |
| Tether ERC-20 | USDT_ERC20 | ethereum | 6 | 12 | 5 |
| USD Coin ERC-20 v2.2 | USDC_ERC20 | ethereum | 6 | 12 | 5 |
| Shiba Inu v2.2 | SHIB | ethereum | 18 | 12 | 200000 |
| Dai v2.2 | DAI | ethereum | 18 | 12 | 5 |
| BNB v2.2 | BNB | bsc | 18 | 15 | 0.01 |
| Tether BEP-20 v2.2 | USDT_BEP20 | bsc | 18 | 15 | 5 |
| USD Coin BEP-20 v2.2 | USDC_BEP20 | bsc | 18 | 15 | 5 |
| Polygon v2.2 | POL | polygon | 18 | 30 | 1 |
| Tether Polygon v2.2 | USDT_POL | polygon | 6 | 30 | 5 |
| USD Coin Polygon v2.2 | USDC_POL | polygon | 6 | 30 | 5 |
| Tron v2.2 | TRX | tron | 6 | 3 (default 19) | 5 |
| Tether TRC-20 | USDT_TRC20 | tron | 6 | 3 (default 19) | 1 |
| USD Coin TRC-20 v2.2 | USDC_TRC20 | tron | 6 | 3 (default 19) | 1 |
| Solana | SOL | solana | 9 | 32 | 0.01 |
| Monero (source-only) v2.3 | XMR | monero | 12 | — | n/a |
Monero (XMR) — source-only v2.3
OrbChain never custodies XMR. Merchants can accept XMR only when they have a non-XMR defaultSettlementCoin configured; incoming XMR is swapped into that coin by the configured swap provider. Supports standard, subaddress, and integrated address formats. XMR has no standing receive address — GET /dashboard/receive-address?coin=XMR returns 400 xmr_receive_unsupported by design.
Contract Addresses (Mainnet)
The gateway pins these addresses and validates against them; deposits sent to any other contract will not be detected.
| Token | Contract |
|---|---|
USDT_ERC20 | 0xdAC17F958D2ee523a2206206994597C13D831ec7 |
USDC_ERC20 | 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48 |
SHIB | 0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE |
DAI | 0x6B175474E89094C44Da98b954EedeAC495271d0F |
USDT_BEP20 | 0x55d398326f99059fF775485246999027B3197955 |
USDC_BEP20 | 0x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d |
USDT_POL | 0xc2132D05D31c914a87C6611C10748AEb04B58e8F |
USDC_POL | 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359 |
USDT_TRC20 | TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t |
USDC_TRC20 | TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8 |
Shared EVM address. For a given (merchant, user) pair, ETH, BNB, POL and every ERC-20 / BEP-20 / Polygon token resolve to the same 0x… address. Similarly, TRX / USDT_TRC20 / USDC_TRC20 share the same T… address. Token events are tracked per contract.
Merchants can increase their confirmation thresholds above the platform minimums via PUT /dashboard/settings/confirmations. They cannot go below the floor.
Address Formats
| Coin | Format | Example Prefix |
|---|---|---|
| BTC | Bech32 native SegWit | bc1q... |
| LTC | Bech32 | ltc1q... |
| DOGE | Legacy P2PKH (Base58Check) | D... |
| ETH / BNB / POL + all EVM tokens | EIP-55 checksummed hex | 0x... |
| TRX / USDT_TRC20 / USDC_TRC20 | Base58Check | T... |
| SOL | Base58 (43-44 chars) | -- |
Each merchant is issued an isolated address space at signup, so deposit addresses never collide across merchants.
Error Codes
HTTP Status Codes
| Status | Meaning |
|---|---|
| 200 | Success |
| 400 | Bad request -- validation error, invalid address, burn address, amount <= 0, currency not accepted, insufficient balance |
| 401 | Unauthorized -- missing or invalid API key, invalid JWT, incorrect password, invalid TOTP code |
| 403 | Forbidden -- CAPTCHA check failed, or access denied |
| 404 | Not found -- payment or payout does not exist, or belongs to a different merchant |
| 409 | Conflict -- email already registered |
| 429 | Rate limit exceeded -- see Retry-After response header |
| 500 | Internal server error |
| 503 | Service unavailable -- maintenance mode, payout backlog circuit breaker, required provider not configured |
Common Error Messages
| Message | Cause |
|---|---|
validation error: ... | Request body failed schema validation. The detail object lists field-level errors. |
currency not accepted by merchant | The requested coin is not in the merchant's accepted_coins list. |
invalid destination address | The payout address failed per-coin format validation. |
destination is a burn/null address | The payout address is a known burn address. |
insufficient balance | Merchant's balance is below the requested payout amount. |
payout backlog too large | Circuit breaker: 500+ payouts are already queued. |
maintenance | The gateway is in maintenance mode. All write operations are rejected. |
email already registered | Signup attempted with an existing email. |
invalid credentials | Login email or password is incorrect. |
not found | The requested resource does not exist or belongs to another merchant. |
not configured | A required feature is not enabled on this deployment. |
Rate Limits
| Category | Limit | Window | Keyed By |
|---|---|---|---|
Read endpoints (GET) | 300 requests | 1 minute | API key or IP |
Write endpoints (POST, PUT) | 60 requests | 1 minute | API key or IP |
Auth endpoints (/signup, /login) | 10 requests | 1 minute | IP address |
All v2.2 endpoints follow the same read/write classification — e.g. POST /dashboard/transfer, POST /dashboard/swap, POST /v1/transfer are write (60/min); GET /dashboard/portfolio, GET /dashboard/deposit-addresses, GET /dashboard/transfers, GET /dashboard/swaps, GET /dashboard/swap/:id, and POST /dashboard/swap/quote are read (300/min). Merchants integrating a swap flow should call the quote endpoint liberally (no state change) and only hit POST /dashboard/swap on user confirmation.
When a limit is exceeded the server responds with:
{
"data": null,
"message": "too many requests",
"error": { "type": "rate_limit", "message": "too many requests" },
"status": 429,
"version": "2.0.0"
}
Tips: Cache /v1/common/prices on your side (60s server cache). Use GET /v1/payment with order_id filtering instead of polling individual payments. Subscribe to webhooks to eliminate polling entirely.
SDKs & Plugins
Official SDKs and CMS plugins are on the roadmap.
| Platform | Status |
|---|---|
| Node.js SDK | Coming soon |
| Python SDK | Coming soon |
| PHP SDK | Coming soon |
| WooCommerce plugin | Coming soon |
| Shopify app | Coming soon |
| WHMCS module | Coming soon |
In the meantime, the REST API is simple enough to integrate with a few lines of curl, fetch, or axios. All endpoints — including every v2.2 addition (portfolio, deposit addresses, internal transfer, balance swap) — use the same JWT / API-key auth described in §2; no SDK is required. See the Quick Start below.
Quick Start Guide
1. Create an account
curl https://orbchain.io/dashboard/auth/signup \ -X POST \ -H "Content-Type: application/json" \ -d '{"email":"[email protected]","password":"SecurePass123"}'
Save the three API keys from the response -- they are shown only once.
2. Generate a deposit address for a user
curl https://orbchain.io/v1/payment/static-address \ -X POST \ -H "Content-Type: application/json" \ -H "merchant_api_key: YOUR_MERCHANT_KEY" \ -d '{"currency":"USDT_TRC20","user_id":"player-1"}'
Display the address from the response to your user with a QR code.
3. Receive and verify the payment webhook
When the deposit confirms, the gateway POSTs to your webhook_url:
# In your webhook handler -- verify before trusting: EXPECTED=$(echo -n "$RAW_BODY" | openssl dgst -sha512 -hmac "$MERCHANT_API_KEY" | awk '{print $2}') [ "$EXPECTED" = "$RECEIVED_HMAC_HEADER" ] || exit 1
Credit the user after verifying the signature and checking status == "Paid".
4. Send a payout
curl https://orbchain.io/v1/payout \ -X POST \ -H "Content-Type: application/json" \ -H "payout_api_key: YOUR_PAYOUT_KEY" \ -H "X-Idempotency-Key: wd-player1-20260415-001" \ -d '{ "currency": "USDT_TRC20", "address": "TJYeasTPGpTKFR5HFqqPUVaHhtnD51RMRZ", "amount": "50.00", "description": "Player withdrawal" }'
You receive a payout webhook at Confirmed (or Failed).
5. Check your balance
curl https://orbchain.io/v1/general/account/balance \ -H "general_api_key: YOUR_GENERAL_KEY"
6. Create a hosted payment page
curl https://orbchain.io/v1/payment/invoice \ -X POST \ -H "Content-Type: application/json" \ -H "merchant_api_key: YOUR_MERCHANT_KEY" \ -d '{ "amount": "29.99", "currency": "USD", "order_id": "order-abc-123", "return_url": "https://yoursite.com/thank-you" }'
Redirect the user to the payment_url from the response. The gateway handles the rest.
7. Send an internal transfer to another merchant v2.2
Zero-fee, instant. The recipient's account_id is their public ORB-… identifier.
curl https://orbchain.io/v1/transfer \ -X POST \ -H "Content-Type: application/json" \ -H "merchant_api_key: YOUR_MERCHANT_KEY" \ -d '{ "to_account_id": "ORB-3qvxq5yVwnS6c3c3Eycj", "coin": "USDT_TRC20", "amount": "25.00" }'
The transfer commits atomically; both merchants receive transfer.sent / transfer.received webhooks moments later. Find your own account_id in GET /dashboard/account.
8. Swap a balance into another coin v2.2
Always quote first (no state change), then confirm with POST /dashboard/swap.
# Quote curl https://orbchain.io/dashboard/swap/quote \ -X POST \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{"from_coin":"BTC","to_coin":"USDT_TRC20","amount":"0.01"}' # Commit (debits immediately; executes asynchronously) curl https://orbchain.io/dashboard/swap \ -X POST \ -H "Authorization: Bearer $JWT" \ -H "Content-Type: application/json" \ -d '{"from_coin":"BTC","to_coin":"USDT_TRC20","amount":"0.01"}' # Poll until status is CONFIRMED or FAILED curl https://orbchain.io/dashboard/swap/$SWAP_ID \ -H "Authorization: Bearer $JWT"
On CONFIRMED, read actual_to_amount for the real credited amount (may drift ~2% from the quote on floating-rate pairs). On FAILED, the source coin is refunded automatically — no action needed.
Need help? Contact our developer support at [email protected] or visit your Dashboard to generate API keys and start integrating.