BTC$68,420▲ 0.82% / ETH$3,841▼ 0.31% / LTC$94.05▲ 1.14% / SOL$221.60▲ 3.02% / USDT·TRC20$1.0000 / USDC·ERC20$0.9999 / XMR$168.42▼ 0.18% / TON$6.02▲ 0.47% / XRP$2.41▼ 0.09% / GAS·ETH18 gwei / Indicative prices · illustrative ticker / No-KYC · non-pooled · 0.4% flat / BTC$68,420▲ 0.82% / ETH$3,841▼ 0.31% / LTC$94.05▲ 1.14% / SOL$221.60▲ 3.02% / USDT·TRC20$1.0000 / USDC·ERC20$0.9999 / XMR$168.42▼ 0.18% / TON$6.02▲ 0.47% / XRP$2.41▼ 0.09% / GAS·ETH18 gwei / Indicative prices · illustrative ticker / No-KYC · non-pooled · 0.4% flat
OrbChain
UTC 18:47:02 Get started →

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:

Success Response
{
  "data": { // ... endpoint-specific payload },
  "message": "OK",
  "error": null,
  "status": 200,
  "version": "2.0.0"
}
Error Response
{
  "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 NameHeaderScope
merchant_api_keymerchant_api_keyCreate/read payment addresses
payout_api_keypayout_api_keyInitiate and check payouts
general_api_keygeneral_api_keyQuery balances

Pass the key in the corresponding HTTP header (not Authorization):

API Key Header Example
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

GET /health No Auth

Shallow liveness probe. Always returns 200 if the process is running.

Response
{ "ok": true }

List All Currencies

GET /v1/common/currencies No Auth

Returns all supported coins with network details and confirmation requirements.

Response -- 200 OK
{
  "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

GET /v1/common/prices No Auth

Returns live USD prices for all supported coins. Cached for 60 seconds.

Response -- 200 OK
{
  "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

GET /v1/common/receipt-public-key No Auth

Ed25519 public key used to sign settlement receipts. Fetch once and pin in your own config -- verify every receipt against the pinned value.

Response -- 200 OK
{
  "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

POST /v1/payment/static-address merchant_api_key

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

FieldTypeRequiredDescription
currencystringrequiredCoin to receive: BTC, LTC, ETH, USDT_ERC20, USDT_TRC20, SOL
user_idstringoptionalYour internal user/player ID (max 128 chars). Same (user_id, currency) always returns the same address.
order_idstringoptionalYour reference string (max 128 chars). Echoed back on webhooks.
descriptionstringoptionalHuman-readable note (max 500 chars).
callback_urlstringoptionalOverride the merchant's default webhook URL for this payment only.
Request
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"
  }'
Response -- 200 OK
{
  "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

Create White Label Payment

POST /v1/payment/white-label merchant_api_key

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

FieldTypeRequiredDescription
currencystringrequiredCoin to receive.
amountstringconditionalExpected 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.1stringconditionalPrice the invoice in fiat. Gateway locks a coin amount at creation using the live rate. Requires fiat_currency.
fiat_currency v2.1stringconditionalUSD, EUR, GBP, JPY, CAD, AUD, CHF, CNY, INR, RUB.
pay_currencystringoptionalCoin the user should actually send. Defaults to currency.
order_idstringoptionalYour reference string (max 128 chars).
lifetimeintegeroptionalMinutes until the address expires (1-43200). Default: 30.
callback_urlstringoptionalOverride default webhook URL.
descriptionstringoptionalMax 500 chars.
user_idstringoptionalMax 128 chars.
splits v2.4arrayoptionalRevenue-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.4stringoptionalCoin 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.
Request
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"
  }'
Response -- 200 OK
{
  "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

Request
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"
  }'
Response -- locked rate frozen on Payment
{
  "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

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:

Request (inside POST /v1/payment/white-label)
{
  "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

Auto Settle (settlement coin) v2.4

Accept any coin at checkout, receive the coin you actually want to hold. Two modes, freely combinable:

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:

payment.paid webhook — settlement fields
{
  "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

POST /v1/payment/invoice merchant_api_key

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

FieldTypeRequiredDescription
amountstringrequiredAmount in the specified currency. Must be > 0.
currencystringoptionalUSD, EUR, or a crypto symbol. Defaults to the merchant's first accepted coin.
order_idstringoptionalYour reference string (max 128 chars).
lifetimeintegeroptionalMinutes until expiry (1-43200). Default: 60.
callback_urlstringoptionalOverride default webhook URL.
return_urlstringoptionalRedirect user to this URL after a successful payment.
descriptionstringoptionalShown on the payment page (max 500 chars).
emailstringoptionalPre-fill the payer's email.
Request
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]"
  }'
Response -- 200 OK
{
  "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

Get Accepted Currencies

GET /v1/payment/accepted-currencies merchant_api_key

Returns the list of coins the merchant has enabled. Use this to populate a coin-selection UI before creating a payment.

Response -- 200 OK
{
  "data": {
    "accepted_coins": ["BTC", "LTC", "ETH", "USDT_ERC20", "USDT_TRC20", "SOL"]
  },
  "message": "OK",
  "status": 200,
  "version": "2.0.0"
}

Get Payment Status

GET /v1/payment/:track_id merchant_api_key

Fetch the current status of a payment. The transactions array contains every on-chain deposit attributed to this payment.

Request
curl https://orbchain.io/v1/payment/cmo39n6n10008145nt2fyroas \
  -H "merchant_api_key: 1802bbed16da84f0667ebd4f21c1a3b9"
Response -- 200 OK
{
  "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

StatusDescription
WaitingNo deposit detected yet.
PayingDeposit detected on-chain but below the confirmation threshold.
PaidConfirmed and merchant balance credited.
ExpiredLifetime elapsed with no credited payment.
CancelledManually cancelled.

Error Cases

Get Signed Settlement Receipt

GET /v1/payment/:track_id/receipt merchant_api_key

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.

Request
curl -H "merchant_api_key: YOUR_KEY" \
  https://your-domain.com/v1/payment/cmo3yhg6m000bfao625pjluvh/receipt
Response -- 200 OK
{
  "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

FieldDescription
receiptStructured payment record. All amounts preserve the string precision used during settlement.
canonicalDeterministic JSON of receipt (sorted keys, no whitespace). Sign the bytes of this string, not a re-serialization of receipt.
signatureEd25519 signature over the UTF-8 bytes of canonical (base64).
public_key_pemSPKI PEM of the signing key. Prefer this for most libraries.
ephemeralfalse 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

  1. Fetch and pin public_key from /v1/common/receipt-public-key at integration time. Store in your own config; do not re-fetch per verification.
  2. Reject any receipt whose public_key does not match the pinned value.
  3. Reject receipts with ephemeral: true in production.
  4. 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, verify deposits[].tx_hash against a block explorer.

Error Cases

List Payments

GET /v1/payment merchant_api_key

List payments with pagination and optional filters.

Query Parameters

ParameterTypeDefaultDescription
statusstring--Filter: WAITING, PAYING, PAID, EXPIRED, CANCELLED
typestring--Filter: STATIC_ADDRESS, WHITE_LABEL, INVOICE
order_idstring--Exact match on your order reference.
currencystring--Filter by coin symbol.
limitinteger20Max records per page (1-200).
offsetinteger0Records to skip for pagination.
sort_orderstringdescSort direction by created_at: asc or desc.
Request
curl "https://orbchain.io/v1/payment?status=PAID&currency=USDT_TRC20&limit=50" \
  -H "merchant_api_key: 1802bbed16da84f0667ebd4f21c1a3b9"
Response -- 200 OK
{
  "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

GET /v1/deposits merchant_api_key

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

ParameterTypeDefaultDescription
coinstring--Filter by coin symbol, e.g. USDT_TRC20.
statusstring--Filter: PENDING, CONFIRMED, CREDITED, FAILED.
addressstring--Exact deposit address (e.g. a static address).
user_idstring--Your external_user_id for per-user static addresses.
sinceinteger--Unix seconds; only deposits created at or after this time.
limitinteger50Max records per page (1-200).
offsetinteger0Records to skip for pagination.
sort_orderstringdescSort by created_at: asc or desc.
Request
curl "https://orbchain.io/v1/deposits?address=TRrqEHua7YXsv7HYaDp1chAP6ErPMhR18e&limit=50" \
  -H "merchant_api_key: 1802bbed16da84f0667ebd4f21c1a3b9"
Response -- 200 OK
{
  "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

POST /v1/payout payout_api_key

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

FieldTypeRequiredDescription
currencystringrequiredCoin to send: BTC, LTC, ETH, USDT_ERC20, USDT_TRC20, SOL
addressstringrequiredDestination on-chain address (max 128 chars). Validated per-coin format. Burn/null addresses are rejected.
amountstringrequiredAmount to send in coin units (e.g. "0.001" BTC). Must be > 0.
callback_urlstringoptionalOverride default webhook URL for this payout.
descriptionstringoptionalInternal note (max 500 chars). Not sent on-chain.
idempotency_keystringoptional8-128 chars. Prevents duplicate payouts on network retry. Also accepted as X-Idempotency-Key header.
Request
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"
  }'
Response -- 200 OK
{
  "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

Get Payout Status

GET /v1/payout/:track_id payout_api_key

Fetch the current status of a payout.

Request
curl https://orbchain.io/v1/payout/cmo3b1xyz9000a145n7qrst8u \
  -H "payout_api_key: 6bcebc3adb04752d889f3a01c2d4e5f6"
Response -- 200 OK
{
  "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

StatusDescription
ConfirmingTransaction signed and broadcast. Waiting for on-chain confirmation.
ConfirmedConfirmed on-chain. tx_hash is populated.
FailedBroadcast or confirmation failed. failure_reason describes the cause.

Error Cases

Mass Payouts v2.4

POST /v1/payout/batch payout_api_key

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.

Request
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"
      }
    ]
  }'
Response -- 200 OK
{
  "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

POST /v1/payment/:track_id/refund payout_api_key

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.

Request
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

Internal Transfers v2.2

POST /v1/transfer merchant_api_key

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

FieldTypeRequiredDescription
to_account_idstringrequiredRecipient's public ORB-… account ID. Self-transfers are rejected.
coinstringrequiredAny coin you hold a balance in.
amountstringrequiredAmount in coin units, e.g. "25.00".
idempotency_keystringoptionalBody field or X-Idempotency-Key header. Retries with the same key return the original transfer instead of sending twice. Strongly recommended.
totp_codestringconditionalRequired on every transfer when your account has 2FA enabled (TOTP step-up — a leaked API key alone can't move balance).
Request
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"
  }'
Response -- 200 OK
{
  "data": {
    "transfer_id": "cmo4s7tvh0003jak6gl2bj5wf",
    "to_account_id": "ORB-3qvxq5yVwnS6c3c3Eycj",
    "coin": "USDT_TRC20",
    "amount": "25.00",
    "created_at": 1776543645
  }
}

Error Cases

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

POST/v1/subscriptionmerchant_api_key

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

FieldTypeRequiredDescription
plan_idstringrequiredOne of your plan ids (from GET /dashboard/plans).
customer_refstringoptionalYour tenant reference (max 128). Echoed on every subscription.paid.
customer_emailstringoptionalWhere OrbChain emails the pay link each cycle.
start_atstringoptionalRFC3339 timestamp. Delay the first invoice (e.g. now + 7 days for a trial). Defaults to now.
Request
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]"
  }'
Response -- 200 OK
{
  "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

GET/v1/subscriptionmerchant_api_key

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

GET/v1/subscription/:idmerchant_api_key

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

POST/v1/subscription/:id/cancelmerchant_api_key

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

POST /v1/payment/white-label

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:

Pay out over Lightning (BOLT11 or Lightning Address)

POST /v1/payout

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

POST /dashboard/plans JWT Bearer
Request
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

URLMethodPurpose
/subscribe/:planIdGETEnrollment form (plan details, email input, subscribe button).
/subscribe/:planIdPOSTemail + customer_ref (both optional) -- creates subscription, redirects to manage page.
/subscribe/manage/:magicTokenGETStatus + pause/resume/cancel buttons + link to current invoice.
/subscribe/manage/:magicToken/pausePOSTFlip to PAUSED.
/subscribe/manage/:magicToken/resumePOSTFlip to ACTIVE, nextBillingAt reset to now.
/subscribe/manage/:magicToken/cancelPOSTFinal. 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.

subscription.invoiced -- fired when the billing tick opens an invoice
{
  "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"
}
subscription.paid -- fired the moment the invoice is confirmed paid (grant/renew access here)
{
  "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.

subscription.failed -- after two consecutive expired invoices
{
  "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

GET /v1/general/account/balance general_api_key

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.

Request
curl https://orbchain.io/v1/general/account/balance \
  -H "general_api_key: 62e1270e345f38fd9a1b2c3d4e5f6a7b"
Response -- 200 OK
{
  "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.

HeaderBody fieldDescription
X-Event-Idevent_idStable 26-char Crockford-base32 id, deterministic per logical event.
X-Event-Typeevent_typepayment.paid, payout.confirmed, payout.failed, deposit.credited.
X-Event-Timestampevent_timestampUnix 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 VersionHeaderAlgorithmSecret
V2 (default)HMACHMAC-SHA512Your 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.

Node.js Verification
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");
});
Python Verification
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 Verification
<?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.

Payment Webhook Body
{
  "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.

Payout Webhook Body -- Confirmed
{
  "type": "payout",
  "track_id": "cmo3b1xyz9000a145n7qrst8u",
  "status": "Confirmed",
  "coin": "USDT_TRC20",
  "amount": "25.00",
  "destination": "TJYeasTPGpTKFR5HFqqPUVaHhtnD51RMRZ",
  "tx_hash": "b3c4d5e6f7a8b9c0d1e2f3a4...",
  "failure_reason": null
}
Payout Webhook Body -- Failed
{
  "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.

Body
{
  "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

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

POST /dashboard/auth/signup No Auth

Create a new merchant account. API keys are generated automatically and returned once.

Request Body

FieldTypeRequiredDescription
emailstringrequiredValid email address. Must be unique.
passwordstringrequired8-128 characters.
turnstile_tokenstringoptionalBot-protection CAPTCHA token. Required when the signup form renders a CAPTCHA widget.
Request
curl https://orbchain.io/dashboard/auth/signup \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "password": "securePassword123"
  }'
Response -- 200 OK
{
  "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

Login

POST /dashboard/auth/login No Auth

Exchange email + password for a JWT.

Request Body

FieldTypeRequiredDescription
emailstringrequiredRegistered email address.
passwordstringrequiredAccount password.
turnstile_tokenstringoptionalCAPTCHA token (when the form renders a bot-protection widget).
Response -- 200 OK
{
  "data": {
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "merchant_id": "cmo39mvd8000014n5abc123xy",
    "email": "[email protected]"
  },
  "message": "OK",
  "status": 200,
  "version": "2.0.0"
}

Error Cases

Google OAuth

GET /dashboard/auth/google No Auth

Redirects the browser to the Google OAuth consent screen. Not a JSON API -- this is a browser redirect.

OAuth Flow

  1. Send the user's browser to GET /dashboard/auth/google.
  2. Google redirects back to /dashboard/auth/google/callback with a code parameter.
  3. The server exchanges the code for tokens, creates or logs in the merchant account, then redirects to /app#google-auth=<encoded-payload>.
  4. 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

GET /dashboard/account JWT Bearer

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.

Response -- 200 OK
{
  "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

GET /dashboard/balances JWT Bearer

Returns per-coin merchant balances. Equivalent to /v1/general/account/balance but authenticated with a JWT.

Response -- 200 OK
{
  "data": {
    "balances": [
      { "coin": "USDT_TRC20", "amount": "4287.50" },
      { "coin": "BTC",        "amount": "0.21830000" }
    ]
  },
  "message": "OK",
  "status": 200,
  "version": "2.0.0"
}

Get Transactions

GET /dashboard/transactions JWT Bearer

Returns recent deposits and payouts for the merchant.

Query Parameters

ParameterTypeDefaultDescription
limitinteger100Max records (1-500). Applies independently to deposits and payouts.
Response -- 200 OK
{
  "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)

GET /dashboard/payments JWT Bearer

List the merchant's payments. Accepts the same filters as GET /v1/payment.

Query Parameters

ParameterTypeDefaultDescription
statusstring--WAITING, PAYING, PAID, EXPIRED, CANCELLED
typestring--STATIC_ADDRESS, WHITE_LABEL, INVOICE
currencystring--Filter by coin symbol.
limitinteger20Max per page.
offsetinteger0Pagination offset.

Response shape is identical to GET /v1/payment.

Get Payment Detail (Dashboard)

GET /dashboard/payments/:trackId JWT Bearer

Fetch a single payment by track_id. Response shape is identical to GET /v1/payment/:track_id.

List Payouts (Dashboard)

GET /dashboard/payouts JWT Bearer

List the merchant's payouts.

Query Parameters

ParameterTypeDefaultDescription
limitinteger20Max per page (1-200).
offsetinteger0Pagination offset.
Response -- 200 OK
{
  "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)

GET /dashboard/api-keys JWT Bearer

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.

Response -- 200 OK
{
  "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

GET /dashboard/api-keys JWT Bearer

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

POST /dashboard/api-keys JWT Bearer

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

FieldTypeRequiredDescription
labelstring (1–80)yesHuman-readable identifier.
scopesstring[]yesAt least one of: payment:create, payment:read, payout:create, payout:read, balance:read, transfer:create.
coin_allowliststring[]noIf non-empty, the key can only name a coin inside this list (else 403 coin_not_allowed). Empty/absent = all accepted coins.
ip_allowliststring[]noCIDRs (e.g. 1.2.3.4/32, 10.0.0.0/8). Calls from any other IP get 401 ip_not_allowed.
webhook_urlstring (URL)noOptional per-key webhook override (PK-T4).
rate_limit_rpminteger (1–10000)noPer-key write rate-limit override (PK-T5).
expires_atinteger (unix sec)noAbsolute expiry; past-expiry keys return 401.
Response -- 200 OK (plaintext shown once)
{
  "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

POST /dashboard/api-keys/:id/revoke JWT Bearer

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

ScopeRoutes
payment:createPOST /v1/payment/static-address, /white-label, /invoice
payment:readGET /v1/payment/accepted-currencies, /:track_id, /:track_id/receipt, /v1/payment
payout:createPOST /v1/payout
payout:readGET /v1/payout/:track_id
balance:readGET /v1/general/account/balance
transfer:createPOST /v1/transfer
refund:createReserved (future phase).

The three legacy seed keys carry defaults so existing integrations keep working unchanged:

New error classes on authed routes

Auto-withdraw Rules v2.3

GET/dashboard/settings/auto-withdrawJWT Bearer
PUT/dashboard/settings/auto-withdrawJWT Bearer

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

FieldTypeNotes
coinstringCoin enum value.
threshold_nativestring (decimal)Native units. Rule fires when balance ≥ threshold.
destinationstring (8–200)External address. Validated against the same burn-list + address-format guards the payout API uses.
Example PUT body
{
  "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

GET /dashboard/referrals JWT Bearer

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.

Response -- 200 OK
{
  "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

GET /dashboard/webhook-logs JWT Bearer

Returns webhook delivery history with status codes, attempt counts, and response previews.

Query Parameters

ParameterTypeDefaultDescription
limitinteger50Max records (1-200).
offsetinteger0Pagination offset.
Response -- 200 OK
{
  "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

GET /dashboard/analytics JWT Bearer

Returns a 30-day rolling daily breakdown of deposit and payout volume in USD, plus 30-day totals.

Response -- 200 OK
{
  "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)

POST /dashboard/create-payment JWT Bearer

Create a payment from the dashboard. Supports all three payment types in a single endpoint.

Request Body

FieldTypeRequiredDescription
typestringrequiredstatic_address, white_label, or invoice
currencystringconditionalRequired for static_address and white_label.
amountstringconditionalRequired for white_label and invoice.
pay_currencystringoptionalFor white_label only. Defaults to currency.
user_idstringoptionalMax 128 chars.
order_idstringoptionalMax 128 chars.
descriptionstringoptionalMax 500 chars.
lifetimeintegeroptionalMinutes until expiry (1-43200).
callback_urlstringoptionalMust be a valid URL.
return_urlstringoptionalFor invoice type only.
emailstringoptionalFor invoice type only.

Response shape matches the corresponding /v1/payment/* endpoint.

Create Payout (Dashboard)

POST /dashboard/create-payout JWT Bearer

Initiate a payout from the dashboard (JWT-authenticated).

Request Body

FieldTypeRequiredDescription
currencystringrequiredCoin to send.
addressstringrequiredDestination on-chain address.
amountstringrequiredAmount in coin units. Must be > 0.
descriptionstringoptionalMax 500 chars.

Error Cases

Update Webhook URL

PUT /dashboard/settings/webhook JWT Bearer

Update the merchant's default webhook URL. Private/internal IP addresses are rejected.

Request Body

FieldTypeRequiredDescription
webhook_urlstringrequiredValid https:// or http:// URL. Max 500 chars. Private/internal addresses rejected.
Request
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

PUT /dashboard/settings/coins JWT Bearer

Update the list of coins the merchant accepts. Must include at least one coin.

Request Body

FieldTypeRequiredDescription
accepted_coinsstring[]requiredOne or more of: BTC, LTC, ETH, USDT_ERC20, USDT_TRC20, SOL
Request
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

PUT /dashboard/settings/confirmations JWT Bearer

Override per-coin confirmation thresholds. Platform minimums are enforced -- values below the floor are rejected.

Platform Minimums

CoinMinimum
BTC2
LTC6
ETH12
USDT_ERC2012
USDT_TRC203
SOL32
Request
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

POST /dashboard/settings/change-password JWT Bearer

Change the account password. Requires the current password for verification.

Request Body

FieldTypeRequiredDescription
current_passwordstringrequiredCurrent account password.
new_passwordstringrequiredNew password (8-128 characters).

Error Cases

Rotate API Key

POST /dashboard/settings/rotate-key JWT Bearer

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

FieldTypeRequiredDescription
key_typestringrequiredOne of: merchantApiKey, payoutApiKey, generalApiKey
Response -- 200 OK
{
  "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

POST /dashboard/mfa/setup JWT Bearer

Generate a TOTP secret and QR code to configure an authenticator app. MFA is not enabled until you call /dashboard/mfa/verify.

Response -- 200 OK
{
  "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

MFA Verify

POST /dashboard/mfa/verify JWT Bearer

Confirm the TOTP code from the authenticator app and activate MFA on the account.

Request Body

FieldTypeRequiredDescription
codestringrequired6-digit TOTP code from the authenticator app.

Error Cases

MFA Disable

POST /dashboard/mfa/disable JWT Bearer

Disable MFA on the account. Requires the current TOTP code as proof of possession.

Request Body

FieldTypeRequiredDescription
codestringrequired6-digit TOTP code from the authenticator app.

Error Cases

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.

POST/dashboard/payment-linksJWT Bearer
FieldTypeRequiredDescription
coinstringrequiredCoin to receive.
amountstringoptionalFixed native-coin amount. Mutually exclusive with amount_fiat. Omit both for a buyer-entered link.
amount_fiatstringoptionalFixed fiat-priced amount. Requires fiat_currency.
fiat_currencystringoptionalUSD, EUR, GBP, JPY, CAD, AUD, CHF, CNY, INR, RUB.
lifetimeintegeroptionalMinutes each minted invoice stays open (1-43200).
descriptionstringoptionalMax 500 chars.
Response
{
  "data": {
    "id": "cmo4455ud000lt3pbd8xpnetn",
    "slug": "AuCU2vJS3nCd",
    "url": "https://orbchain.io/l/AuCU2vJS3nCd",
    "coin": "USDT_TRC20",
    "amount_fiat": "50",
    "fiat_currency": "USD",
    "enabled": true,
    "uses": 0
  }
}
GET/dashboard/payment-linksJWT Bearer

List all links owned by the merchant (newest first), each with its full url.

PATCH/dashboard/payment-links/:idJWT Bearer

Toggle the enabled flag: {"enabled": false}. Disabled links return 410 Gone on resolution.

GET/dashboard/payment-links/:id/embedJWT Bearer

Return a paste-ready donation-button snippet built against the caller's request host. Optional ?label=Support+us customizes the button text.

Response
{
  "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.

POST/dashboard/payout-schedulesJWT Bearer
FieldTypeRequiredDescription
coinstringrequiredCoin to withdraw.
destinationstringrequiredValidated per-coin (rejects burn addresses + malformed).
min_amountstringrequiredSkip the cycle if balance is below this.
interval_secintegerrequiredCadence in seconds. Floor 3600 (1h), ceiling 2592000 (30d).
Response
{
  "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.

GET/dashboard/payout-schedulesJWT Bearer
PATCH/dashboard/payout-schedules/:idJWT Bearer

Patch any subset of {enabled, min_amount, interval_sec}.

DELETE/dashboard/payout-schedules/:idJWT Bearer

In-flight payouts from a prior tick complete; only future ticks are stopped.

Error Cases

Customer-Static Mode v2.1

PUT/dashboard/settings/customer-static-modeJWT Bearer

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.

Request
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

GET/dashboard/brandsJWT Bearer
POST/dashboard/brandsJWT Bearer
GET/dashboard/brands/:idJWT Bearer
PUT/dashboard/brands/:idJWT Bearer
DELETE/dashboard/brands/:idJWT Bearer

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)

FieldTypeRequiredDescription
namestringrequiredOperator-facing label, 1-80 chars.
slugstringoptionalURL-safe, max 40 chars, unique within your account. Auto-derived from name when omitted. default is reserved.
logo_urlstringoptionalAbsolute URL, rendered on the hosted checkout.
theme_modestringoptionalLIGHT | DARK. Falls back to account default when null.
brand_colorstringoptionalHex color, #RRGGBB.
webhook_urlstringoptionalPer-brand webhook endpoint. Unset → inherits the account webhook. Same SSRF validation as the account-level URL.
webhook_secretstringoptional16-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

GET/dashboard/portfolioJWT Bearer

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.

Response -- 200 OK
{
  "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

GET/dashboard/receive-address?coin=<Coin>JWT Bearer

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

ParamRequiredDescription
coinyesOne 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

GET/dashboard/deposit-addressesJWT Bearer

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.

Response -- 200 OK (abbreviated)
{
  "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

POST/dashboard/transferJWT Bearer

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.

Request
{
  "to_account_id": "ORB-3qvxq5yVwnS6c3c3Eycj",
  "coin": "USDT_TRC20",
  "amount": "25.00"
}
Response -- 200 OK
{
  "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

GET/dashboard/transfersJWT Bearer

Merchant's internal transfers (both sent and received). Query param limit (default 100, max 500).

Balance Swap Quote v2.2

POST/dashboard/swap/quoteJWT Bearer

Preview a balance swap without debiting. Returns a live rate from the configured swap provider.

Request
{ "from_coin": "BTC", "to_coin": "USDT_TRC20", "amount": "0.01" }
Response -- 200 OK
{
  "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

POST/dashboard/swapJWT Bearer

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

GET/dashboard/swap/:idJWT Bearer

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

GET/dashboard/swapsJWT Bearer

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

SurfaceAuth
GET /tg/link/start?t=<token>Self-contained HMAC token (<chatId>.<exp>.<hmac>), 10-min TTL.
POST /dashboard/tg/link/completeDashboard JWT + link token.
GET /tgappNone (HTML only, strict CSP).
POST /tgapp/sessionTelegram WebApp initData — HMAC-SHA256 verified server-side per Telegram's documented scheme.
GET /tgapp/*Authorization: Bearer <tgapp JWT> (src="tgapp", 1h TTL).
GET /tgapp/qrNone — content is a public address.
GET /tg/link/start?t=<token> Self-contained token

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.").

POST /dashboard/tg/link/complete JWT Bearer

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

GET /dashboard/tg/link/status JWT Bearer

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

POST /tgapp/session Telegram initData

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

Mini App — Balances v2.3

GET /tgapp/balances tgapp JWT
{ "data": { "balances": [{ "coin": "USDT_TRC20", "amount": "4.33" }] } }

Mini App — Payments v2.3

GET /tgapp/payments tgapp JWT

Returns the 10 most recent payments for the authed merchant. Shape mirrors formatPaymentResponse but trimmed.

Mini App — Receive v2.3

GET /tgapp/receive?coin=<Coin> tgapp JWT

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

GET /tgapp/prices tgapp JWT

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

GET /tgapp/qr?data=<text> No Auth

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

GET/tgapp/mfa/statustgapp JWT
POST/tgapp/mfa/setuptgapp JWT
POST/tgapp/mfa/verifytgapp JWT

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

POST/tgapp/payouttgapp JWT + TOTP

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

POST/tgapp/swap/quotetgapp JWT
POST/tgapp/swaptgapp JWT + TOTP

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"
//   }
POST/tgapp/unlinktgapp JWT

Mirrors the bot's /unlink command. Clears merchant.telegramChatId + telegramLinkedAt; the Mini App closes itself on success via Telegram.WebApp.close().

Hosted Payment Page

GET /pay/:trackId No Auth

Returns a server-rendered HTML payment page. This is the URL returned as payment_url by POST /v1/payment/invoice.

Behavior

Example URL
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

GET/tip/:accountIdpublic

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

ParameterDescription
coinOptional. Coin to render (must be in the merchant's accepted_coins; otherwise the first accepted coin is used).

Behavior

Example URLs
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.

GET/l/:slugpublic

Resolves a payment link into a fresh invoice.

Examples
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

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.

Minimum markup
<script src="https://orbchain.io/assets/donate.js" async></script>
<div class="orbchain-donate" data-slug="AuCU2vJS3nCd" data-label="Donate crypto"></div>

Supported attributes

AttributeDefaultDescription
data-slug--Required. Payment-link slug.
data-labelDonate cryptoButton text.
data-color#f59e0bCSS color for the button background.
data-targetpopuppopup (480x720 window) or blank (new tab).
data-amount--Prefill for buyer-entered links. Ignored for fixed-amount links.
data-originscript's originOverride the gateway host (testing/staging).

SPA / framework integration

Programmatic mount
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

CoinSymbolChainDecimalsMin ConfirmationsMin Payout
BitcoinBTCbitcoin820.0001
LitecoinLTClitecoin860.001
Dogecoin v2.2DOGEdogecoin8610
EthereumETHethereum18120.001
Tether ERC-20USDT_ERC20ethereum6125
USD Coin ERC-20 v2.2USDC_ERC20ethereum6125
Shiba Inu v2.2SHIBethereum1812200000
Dai v2.2DAIethereum18125
BNB v2.2BNBbsc18150.01
Tether BEP-20 v2.2USDT_BEP20bsc18155
USD Coin BEP-20 v2.2USDC_BEP20bsc18155
Polygon v2.2POLpolygon18301
Tether Polygon v2.2USDT_POLpolygon6305
USD Coin Polygon v2.2USDC_POLpolygon6305
Tron v2.2TRXtron63 (default 19)5
Tether TRC-20USDT_TRC20tron63 (default 19)1
USD Coin TRC-20 v2.2USDC_TRC20tron63 (default 19)1
SolanaSOLsolana9320.01
Monero (source-only) v2.3XMRmonero12—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.

TokenContract
USDT_ERC200xdAC17F958D2ee523a2206206994597C13D831ec7
USDC_ERC200xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
SHIB0x95aD61b0a150d79219dCF64E1E6Cc01f0B64C4cE
DAI0x6B175474E89094C44Da98b954EedeAC495271d0F
USDT_BEP200x55d398326f99059fF775485246999027B3197955
USDC_BEP200x8AC76a51cc950d9822D68b83fE1Ad97B32Cd580d
USDT_POL0xc2132D05D31c914a87C6611C10748AEb04B58e8F
USDC_POL0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359
USDT_TRC20TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t
USDC_TRC20TEkxiTehnzSmSe2XqrBj4w32RUN966rdz8

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

CoinFormatExample Prefix
BTCBech32 native SegWitbc1q...
LTCBech32ltc1q...
DOGELegacy P2PKH (Base58Check)D...
ETH / BNB / POL + all EVM tokensEIP-55 checksummed hex0x...
TRX / USDT_TRC20 / USDC_TRC20Base58CheckT...
SOLBase58 (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

StatusMeaning
200Success
400Bad request -- validation error, invalid address, burn address, amount <= 0, currency not accepted, insufficient balance
401Unauthorized -- missing or invalid API key, invalid JWT, incorrect password, invalid TOTP code
403Forbidden -- CAPTCHA check failed, or access denied
404Not found -- payment or payout does not exist, or belongs to a different merchant
409Conflict -- email already registered
429Rate limit exceeded -- see Retry-After response header
500Internal server error
503Service unavailable -- maintenance mode, payout backlog circuit breaker, required provider not configured

Common Error Messages

MessageCause
validation error: ...Request body failed schema validation. The detail object lists field-level errors.
currency not accepted by merchantThe requested coin is not in the merchant's accepted_coins list.
invalid destination addressThe payout address failed per-coin format validation.
destination is a burn/null addressThe payout address is a known burn address.
insufficient balanceMerchant's balance is below the requested payout amount.
payout backlog too largeCircuit breaker: 500+ payouts are already queued.
maintenanceThe gateway is in maintenance mode. All write operations are rejected.
email already registeredSignup attempted with an existing email.
invalid credentialsLogin email or password is incorrect.
not foundThe requested resource does not exist or belongs to another merchant.
not configuredA required feature is not enabled on this deployment.

Rate Limits

CategoryLimitWindowKeyed By
Read endpoints (GET)300 requests1 minuteAPI key or IP
Write endpoints (POST, PUT)60 requests1 minuteAPI key or IP
Auth endpoints (/signup, /login)10 requests1 minuteIP 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:

429 Too Many Requests
{
  "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.

PlatformStatus
Node.js SDKComing soon
Python SDKComing soon
PHP SDKComing soon
WooCommerce pluginComing soon
Shopify appComing soon
WHMCS moduleComing 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 → Commit → Poll
# 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.