Skip to main content

Payments

Take crypto payments on your own site, or at a URL you send to someone. Both run on the same primitive: ChainOS derives a fresh address from your extended public key the moment it is needed, watches it, and tells you what arrived.

Nothing about this changes the custody model. The address belongs to your mnemonic, which only your Edge has ever seen. Ziklag cannot spend from it and neither can anybody who compromises us.

The two surfaces are different products

Pick by where the payer is, not by which sounds more flexible.

Payment buttonPayment link
WhereEmbedded on your checkout, in a cross-origin iframeA page we host, at a URL you send out
AddressOne per checkout sessionOne per link
PayersOneOne (fixed) or many (donation)
AmountSet by your cart, per purchasePreconfigured on the link
Closes whenPaid, or the countdown expiresPaid once, a threshold is reached, or a date passes
You writeA few lines of JavaScriptNothing

A link is not a factory that mints a session per visitor. It owns an address and a running total, which is why it has no countdown: a link closes on a threshold or a date.

Payment button · Payment links

Four things to know before you start

1. A payment is irreversible. There is no chargeback, no issuer and no recall. That is why the checkout is served in a cross-origin iframe rather than drawn in your page: the deposit address is the payload, and any XSS, any compromised tag-manager script and any browser extension with host permissions on your domain could otherwise rewrite it silently and take every payment. It also means the honest version of the claim — a frame makes address substitution a visible impersonation that has to rebuild the UI pixel for pixel, rather than making it impossible.

2. Your server decides what is paid, always. Read Verifying payments before you write any fulfilment code. It is the shortest page here and the only one that can cost you money.

3. Amounts are decimal strings, never numbers. "100.00", not 100.0. JSON has one numeric type and it is a double, and these figures decide whether an order is paid. More decimal places than the asset carries is refused, not rounded — rounding would quietly ask a payer for less than you asked for and then report the order paid in full.

4. A payment that lands late is still yours. Expiry and archival are two separate deadlines. expiresAt stops the checkout accepting the payment; monitorUntil — weeks later — is when we stop watching the address. A payer who broadcasts at 14:59:50 against a 15:00:00 deadline has sent real money, and we credit it. See Late payments.

Where a payment can be

A payment runs created, awaiting_payment, detected, confirmed, completed; underpaid sits below and a reorg can push a detected payment back to it; expired and cancelled lead to expired_paid, which rejoins the ordinary course.

status on the session is one of these nine, lowercase, and it is the only thing your fulfilment code should branch on.

The two worth reading twice are underpaid and expired_paid. Neither is a failure. underpaid means the payer committed real money and it fell short — usually a fee the sending exchange took — so the page asks for the remainder at the same address and extends the window once. expired_paid means money arrived after the clock ran out, which is money the payer cannot get back; calling that "expired" and moving on is how a customer loses funds because our timer expired.

completed is terminal, including after a reorg. The invoice is issued and goods may already have shipped, and a state machine that un-completes is one that claims it can un-ship. A post-confirmation reorg is recorded on the session and raises a webhook, and a person decides.

Chains and assets

Checkout supports seven of the ten chains ChainOS monitors: Bitcoin, Ethereum, BNB Smart Chain, Polygon, Avalanche, Base and TRON. Stablecoins are the intended case and TRC-20 USDT on TRON is plausibly the highest-volume asset you will see.

Solana, Stellar and XRP are refused at session creation with an error that names the reason, rather than failing later and obscurely:

  • Solana and Stellar issue addresses from a pool your Edge pre-derives. Checkout on the other seven keeps working through an Edge outage — Cloud holds the extended public key and derivation is pure computation — but on these two it would stop.
  • Stellar additionally cannot receive an issued asset until the address has signed its own trustline, and the sender cannot create it for them.
  • XRP cannot receive anything below the base reserve until the account is funded, so a freshly derived per-session address is unusable for small payments.

Ask the server rather than hardcoding the list:

curl $CHAINOS/v1/payments/readiness -H "X-API-Key: $CHAINOS_PAYMENT_KEY"

Every chain comes back with available, a reason when it is not, the assets you may price in, and the confirmation threshold. Read it before offering a chain rather than after somebody has picked one — a gateway that answers 403 at the moment a customer presses Pay is a lost sale with nothing on any screen to explain it.

Dust: the failure that costs you the payment

A 5 USDT payment on Ethereum costs more in gas to collect than it is worth, and batching does not help — on an account-model chain every consolidation is its own transaction.

So a session below the chain's economic minimum is refused at creation, with a message that names a cheaper chain:

5 USDT is below the 40 USDT minimum for Ethereum at current gas. Use Base, BSC or Polygon.

The same figure is on /v1/payments/readiness as nativeMinimum and estimatedSweepFee, so you can steer the chain choice before the payer commits. That is the only moment this is free to fix.

nativeMinimum is null for tokens, and that is not an omission: the fee comes back in ETH or BNB while the amount is in USDT, and comparing them needs a rate we deliberately keep off the payment path.

Keys

Two kinds, minted as a pair.

pmk_live_… / pmk_test_…ppk_live_… / ppk_test_…
CalledSecret keyPublishable key
LivesYour serverYour page source
May doEverything on the payments APIOpen a checkout session, and nothing else
Bounded byNothingAn exact origin list and a per-session cap

They are minted together and not by two separate calls, because issuing them separately is how the secret one ends up in a page.

Server-created sessions are the documented default. In publishable-key mode the browser asserts the amount, which means a customer can pay 0.01 for a 100.00 order and the webhook will faithfully report a confirmed payment. Origin locking and the cap bound the upside only. See Verifying payments.

Invoices

An invoice is issued and emailed automatically on the first confirmation, numbered INV-2026-000123 — per account, per year, so the number does not tell your customer how many invoices the whole platform has issued.

It is a hosted HTML page with a print stylesheet, not a PDF. Resending mints a new link and stops the old one working, which is what you want when the first email went astray.

One case issues no invoice at all: a donation link using one shared address. A shared address cannot tell two concurrent donors apart well enough to put a name on a receipt, and that is the stated cost of choosing it.