LedgerLite · docs
Home · Terms

Everything you need to take stablecoin payments in about ten minutes. No accounts to register with us, no funds ever held by us.

Quickstart — three calls to your first payment

1

Get an API key + tell us your wallet

During open beta, email your merchant name and a watch-only xpub (or ask us to generate setup instructions for your wallet) to the contact given in your onboarding note. You receive LL_API_KEY. We store only the public key — funds arrive directly in a wallet you already control.

What is an xpub? Your wallet (e.g. Ledger, Trust, Rabby, any BIP-32 HD wallet) can export an "extended public key". LedgerLite uses it to derive fresh deposit addresses per invoice. Search "export xpub" in your wallet's docs. Ethereum: m/44'/60'/0', Tron: m/44'/195'/0'.

2

Create an invoice

curl -s https://api.xstn.com/v1/invoices \
  -H "Authorization: Bearer $LL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "chain": "tron",
    "token": "USDT",
    "amountMinor": 10000000,
    "customerRef": "order-1042",
    "ttlMinutes": 60
  }'

Response (the invoice's depositAddress is unique to this order — give it to your customer):

{ "invoice": {
    "id": "inv_kAU7yk4REOCV",
    "status": "created",
    "chain": "tron", "token": "USDT",
    "contract": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
    "decimals": 6,
    "amountMinor": 10000000,        // = 10.000000 USDT
    "depositAddress": "TLdUbUprk7BDxdVBgqgmm4EUPw1mAadVSd",
    "customerRef": "order-1042",
    "createdAt": 1789500270000,
    "expiresAt": 1789503870000
} }
3

Receive the payment event

Either poll GET /v1/invoices/inv_kAU7yk4REOCV every few seconds, or — recommended — point webhooks at your server (see Webhooks). Your job ends here: when the webhook says invoice.confirmed, the money is in your wallet, confirmed on-chain, with the exact amount.

Authentication

Every request carries your key as a bearer token:

Authorization: Bearer <LL_API_KEY>

Wrong or missing key → HTTP 401 {"error":"unauthorized"}. Keep the key server-side only; never embed it in browser/mobile code.

API reference

POST /v1/invoices — create

FieldTypeRequiredNotes
chainstringyes"tron" · "eth" · "btc"
tokenstringyesUSDT / USDC (tron, eth) · native ETH (eth) · native BTC (btc). Decimals: tokens 6, ETH 18, BTC 8.
amountMinorinteger or numeric stringyesSmallest unit as a non-negative integer: USDT/USDC = 6 decimals ($10 → 10000000), ETH = 18 (1 ETH → 1000000000000000000), BTC = 8 (1 BTC → 100000000). For amounts above 2⁵³ send a digit-string — precision-safe at any size. The response echoes amountMinor as a string.
customerRefstringnoyour order id; echoed back in reads & webhooks
ttlMinutesnumbernodefault 1440 (24 h). After expiry the invoice goes to expired; a late payment still reopens it (see lifecycle).
webhookUrlstringnoper-invoice override of your default webhook

GET /v1/invoices/:id — read

Full invoice object incl. status, flags (underpaid, overpaid, late) and, once paid, paymentTx: { txHash, paidMinor, confirmations, chain }.

GET /v1/invoices — list

?ref=order-1042&limit=50&status=confirmed for your reconciliation page.

POST /v1/invoices/:id/claim — optional hint

{ "txHash": "0x8c3f…" }

Speeds detection up to the next tick (≈ a few seconds). Purely a hint: the hash is re-verified on-chain; a fake or unrelated hash can never mark anything paid (§20 of our design). Safe to expose to untrusted customers.

Invoice lifecycle

created ──(first scan sees the address funded)──▶ funding_awaiting
   funding_awaiting ──(transfer detected)──▶ funding_detected
   funding_detected ──(N confirmations + exact amount)──▶ confirmed ✅
   any pre-paid state ──(ttl over)──▶ expired
   expired ──(payment arrives late)──▶ funding_detected → confirmed  (flags.late = true)
StatusMeaningYou should
createdaddress assigned, nothing seen yetshow address to customer
funding_awaitingwe are actively watching this addressshow "waiting for payment" UI
funding_detectedtransaction found on-chain, confirmingshow "confirming…" UI
confirmedenough confirmations, exact contract & amount ✅ money in your walletship the goods; this is your payment signal
expiredTTL passed unpaidre-issue a new invoice or contact buyer

Flags

FlagMeaningHandling
underpaidless than asked arrived (chain-fee mistakes, wrong amount)invoice stays unconfirmed; send buyer a second invoice for the difference, or refund-less partial policy is up to you
overpaidmore than asked arriveddoes not block confirmation; keep the surplus or refund the excess off-band
latepayment after expiry — invoice reopened automaticallytreat as paid; honour the order

Webhooks

We POST JSON to your webhookUrl:

POST https://your-shop.example/ledgerlite-hook
Content-Type: application/json
x-ledgerlite-event: invoice.confirmed
x-ledgerlite-timestamp: 1789500333000
x-ledgerlite-signature: sha256=<hex>
{
  "event": "invoice.confirmed",
  "ts": 1789500333000,
  "invoice": { "id": "inv_…", "status": "confirmed",
               "customerRef": "order-1042",
               "paymentTx": { "txHash": "…", "paidMinor": 10000000,
                              "confirmations": 20 } }
}

Events

EventWhen
invoice.awaitingaddress now being watched
invoice.payment_detectedon-chain transfer seen, confirming
invoice.confirmedpayment final — deliver the order
invoice.underpaiddetected but short
invoice.expiredTTL reached unpaid
invoice.reopened_late_paymentexpired invoice got paid later

Verify the signature (Node/Express example)

import crypto from 'node:crypto';
import express from 'express';

const app = express();
app.post('/ledgerlite-hook',
  express.raw({ type: 'application/json' }),   // ⚠ verify against RAW body
  (req, res) => {
    const ts  = req.headers['x-ledgerlite-timestamp'];
    const sig = (req.headers['x-ledgerlite-signature'] || '').replace('sha256=', '');
    const expected = crypto
      .createHmac('sha256', process.env.LL_WEBHOOK_SECRET)
      .update(ts + '.' + req.body.toString('utf8'))
      .digest('hex');
    if (!sig || !crypto.timingSafeEqual(Buffer.from(sig, 'hex'), Buffer.from(expected, 'hex')))
      return res.status(401).end();
    const payload = JSON.parse(req.body.toString('utf8'));
    if (payload.event === 'invoice.confirmed') {
      markOrderPaid(payload.invoice.customerRef);
    }
    res.status(200).end();          // non-2xx = we retry
  });

Retries: exponential backoff (≈1 m, 5 m, 30 m, 2 h, 6 h …), events persisted in an outbox and replayed if your endpoint is down. Idempotent consumers: process each invoice.id once (guard on paymentTx.txHash).

Errors & how to fix them

HTTPBodyCauseFix
400bad jsonmalformed bodysend valid JSON, Content-Type: application/json
400unknown chain "…"typo in chainonly tron / eth (others on roadmap)
400unknown token …token not supported for chainUSDT / USDC only; check spelling & case
400amountMinor must be a positive integer (number or digit-string)float, negative, or non-digit stringmultiply dollar amount by 10⁶ and round to an integer (or pass the digit-string)
401unauthorizedmissing/wrong bearer keyheader exactly Authorization: Bearer <key> — no "Basic", no quoting issues with env vars
404not foundinvoice id typo or belongs to another accountstore the returned id, use it verbatim
404no such routeendpoint typo (e.g. /v1/invoice)plural /v1/invoices; see reference above
500internalchain-data provider hiccup (TronGrid/Etherscan rate limit)retry with backoff; state is persisted, nothing is lost — detection is poll-resumed

Behavioural "errors" (no HTTP code)

SymptomLikely causeFix
Customer paid, invoice still funding_awaitingpaid to the old address, wrong network (e.g. USDT-TRC20 sent to an ETH address or vice-versa), or sent native coin (ETH/TRX) instead of the tokencheck the tx on explorer with the depositAddress; wrong-network transfers need manual recovery by you; always display chain + token + address together, unambiguous
Status stuck funding_detected for minutesconfirmations accumulating (that's normal: ~9 blocks for Tron detection→safety, several minutes Ethereum)wait; or have customer send txhash via /claim for faster attention
Marked underpaidbuyer deducted chain fee from amount, or copied amount with fewer decimalscreate second invoice for exact difference; in UI show amount as plain "10 USDT (TRC20)" copy-field
Invoice expired although paid at minute 59.9race between expiry & detectionno action: late payment automatically reopens it (flags.late, event invoice.reopened_late_payment)
Webhook never arrives but invoice is confirmedyour endpoint returned non-2xx / timeout, or public URL blocked bots/firewalledread GET /v1/invoices/:id as fallback; ensure 200 within 10 s; disable WAF challenge pages on the hook path
Duplicate fulfilmentretried webhook processed twiceidempotency: store processed invoice.id / txHash before acting

Testing before go-live

Security model in one paragraph

LedgerLite is watch-only. We derive addresses from your xpub, watch the public chain, and sign nothing. Money moves customer → your wallet directly; we cannot freeze, redirect or hold funds — and you cannot lose your money by losing access to us. Your API key only creates/reads invoices in your own namespace; webhook payloads are HMAC-signed with a shared secret so nobody can forge a "paid" event into your shop.

Pricing

Pay as you goStarter (monthly)Growth (monthly)
Fee per confirmed invoice1‰0%0%
Included volumeunlimited, pay-only300 / mo1,500 / mo
Price—€1.5 / mo · €12 / year (2 months free)€4.5 / mo · €36 / year
Webhooks, xpub watch-only, multi-currency✅✅✅ + priority confirmation

Open beta: every tier is currently free — including the 1‰. For scale: crypto payment processors typically charge 1–2% and card rails ~2.9% — our rate is a tenth of the industry standard, because non-custodial infrastructure costs us almost nothing per invoice. Rates lock in only when we announce GA, and existing merchants keep beta pricing for 6 months. Billing itself happens in stablecoins through LedgerLite, naturally.

FAQ

Do customers need an account?

No. They see one address and one amount. Any wallet or exchange withdrawal works.

Which chains/tokens now?

USDT & USDC on Ethereum (ERC-20), USDT on Tron (TRC-20) — plus native ETH and native BTC (BIP-84 bc1q… addresses, derived from the same master key). BTC confirms at 2 blocks (~20 min); native ETH needs no API key. Ethereum token scanning requires an Etherscan key on our side (beta: Tron-first, ETH tokens queued). BSC/Polygon/USDC-Tron on the roadmap.

Any per-invoice amount limits?

No. Pass amountMinor as a digit string (e.g. "200000000000000000" = 0.2 ETH) and amounts beyond JavaScript's safe-integer range work exactly — internally we compare in BigInt. Whale-friendly.

What if I want the money in a cold wallet?

Point the xpub at a receiving wallet you control, sweep whenever. Or derive the xpub from your hardware wallet directly.

Is my order data visible to you?

Only what you send: amounts, your customerRef, addresses. No names, no cards, nothing personal.

Refunds?

Not in v1 — the chain is final; refunds are you sending from your own wallet back to the customer's. A refund API is planned.

Docs version 2026-09-16 · API is versioned at /v1; backwards-incompatible changes only at /v2 with 6-month parallel run.