API reference
Take crypto with one call.
A REST API in the shape you know from Stripe: amounts in cents, prefixed IDs, form or JSON bodies, signed webhooks. Buyers pay into your own wallet; we watch the chain and tell your server. Every request goes to https://orboni.com/v1.
Quick start
- 1Create an account, add a wallet under Wallets, and make a test key under Developers → API keys.
- 2Create a checkout session on your server and send the buyer to its
url. - 3Fulfil the order when
checkout.session.completedreaches your webhook endpoint.
curl https://orboni.com/v1/checkout/sessions \
-u sk_test_…: \
-d amount=2490 \
-d client_reference_id=order_1042 \
-d success_url=https://shop.example/thanks \
-d cancel_url=https://shop.example/cartThe answer is the session itself, with status: "open" and the url of its payment page on https://orboni.com/pay/…. The buyer picks a coin there; you never handle addresses or rates.
Authentication
Send your secret key as a bearer token, or as the user of HTTP basic auth with an empty password — which is what Stripe’s client libraries do.
curl https://orboni.com/v1/checkout/sessions/cs_test_… \
-H "Authorization: Bearer sk_test_…"Keys are per project and per mode, made and revoked in the dashboard under Developers → API keys — by the project’s owner, or by someone on its team with the Admin or Developer role; each is in the project’s activity. A key is shown once; we keep only its hash. Keep it on your server — never in a browser or an app. A project holds at most 20 secret keys in each mode; revoke one to make another. A missing or revoked key answers 401, a suspended account 403. When an account passes to the person who proves its email, every secret key it had is revoked and every webhook signing secret is changed at once.
Test and live
sk_test_… keys make test sessions (cs_test_…), sk_live_… keys live ones (cs_live_…). The two never meet: a test key cannot see a live session, its events or its refunds, and each mode has its own webhook endpoints. Test payments are made in a test network’s coins, never real ones, and are never billed; a test page shows where to pay only to the project’s owner, or an Admin or Developer of its team, signed in. Your production server should take only live events: refuse an event whose livemode is false or whose session is a cs_test_… one, so a test payment can never fulfil a real order. See Testing.
Errors
A failed request answers a 4xx or 5xx status and one object, the same shape as Stripe’s:
{
"error": {
"type": "invalid_request_error",
"message": "The amount field must be at least 50.",
"param": "amount"
}
}| Status | type | When |
|---|---|---|
| 400 | invalid_request_error | A bad list parameter or cursor; an event no endpoint takes; a test event resent while too many test webhooks of the project wait. |
| 400 · 409 | idempotency_error | A key reused with other parameters; its first request still running. |
| 401 | authentication_error | No key, a wrong one or a revoked one. |
| 402 | account_past_due | Live only: an Orboni invoice is past due and you were emailed when it fell due. Pay it under Billing; live payments resume as soon as the payment is final. |
| 403 | permission_error | The account is suspended. |
| 404 | invalid_request_error | No such object in this project and mode, or no such URL. |
| 413 | invalid_request_error | A request body over 64 KB. |
| 422 | invalid_request_error | A parameter failed validation; param names it. |
| 429 | rate_limit_error | Too many requests, or too many open sessions (see Limits); wait Retry-After seconds. |
| 5xx | api_error | Ours, or an update in progress (503 with Retry-After). Retry — with an Idempotency-Key it is safe. |
Idempotency
A network can fail after we have made a session and before you hear about it. Send an Idempotency-Key header on a POST — your order ID is a good one — and a retry with the same key gets the first answer again (with Idempotent-Replayed: true) instead of a second session or a second refund.
- Keys are kept for 24 hours, per project and mode, up to 255 characters: two API keys of one project share them.
- The same key with different parameters is refused with 400 idempotency_error. The same parameters in another order are the same request.
- A replay is the same JSON as the first answer, though not always byte for byte: letters and slashes may come unescaped.
- While the first request is still running, a second one gets 409; retry a moment later. A request that never finishes gives its key up 15 minutes after it was sent.
- Only a success is kept. A 4xx or 5xx answer is not, so a retry with the same key is checked and run again.
Lists
Every list answers newest first, in one shape, and pages by cursor:
curl -G https://orboni.com/v1/checkout/sessions \
-u sk_live_…: \
-d limit=50 \
-d status=complete \
-d "created[gte]=1767225600"- limitinteger
- 1 to 100, 10 by default.
- starting_afterid
- The last ID of the page you have: the next, older page.
- ending_beforeid
- The first ID of the page you have: the page before it.
- createdobject
- Unix seconds (not milliseconds; ten digits at most): created[gte], created[gt], created[lte], created[lt]. A longer value is refused with 400.
Checkout sessions
A session is one payment you wait for: a price in US dollars that the buyer pays in any coin your project takes, on a page we host.
Create a session
POST/v1/checkout/sessions
- amountinteger, required
- In US cents, 50 ($0.50) to 10,000,000 ($100,000).
- currencystring
- usd — the only one for now.
- payment_methodsarray
- The ways to pay to offer, by key (see Payment methods). Default: every one the project has a wallet for.
- networkstring
- Narrows the offer to one network, as solana or tron.
- success_urlurl
- Where the payment page sends the buyer once paid: only the browser that opened the page, within 60 minutes of paying. {CHECKOUT_SESSION_ID} in it is replaced with the session id. It must carry no secret (a download token, a login): look the order up from the session id, behind your own sign-in.
- cancel_urlurl
- Where its back link goes, for the browser that opened the page. {CHECKOUT_SESSION_ID} works here too.
- client_reference_idstring
- Your order number, up to 200 characters. Comes back in every event.
- customer_emailstring
- The buyer’s email, if you have it: the page then does not ask for one. Comes back in the session and every event, and is shown in the dashboard and exports.
- customer_email_collectionstring
- Whether the payment page asks the buyer for an email before it shows where to pay: required — they cannot go on without one; optional — they may skip it; off — not asked. Without it, the project’s own choice in Settings → Payment page (required unless you change it). The email they give comes back as customer_email. It is asked once, and a customer_email you pass is not asked again. The page shows it only masked (a•••@example.com).
- localestring
- The language of the payment page, as an IETF tag (id, pt-BR, zh-TW, de-AT): the closest of the 85 it speaks — the list is under Payment page → Languages. auto (the default) is the buyer’s own; a language we lack is the buyer’s own too, never an error.
- descriptionstring
- What is being paid for, shown to the buyer, in lists and exports: “Premium features × 2”. Up to 250 characters; the payment page shows two lines of it and the rest behind Show more. Neither it nor details may name an Orboni fee, bill, invoice or invoice number (422).
- detailsstring
- What the buyer gets, in more words: shown under description on the payment page only — four lines, the rest behind Show more. Plain text, line breaks kept, up to 500 characters; a longer one, up to 1,000, is cut there at a word.
- appearancestring
- light or dark draws the payment page so — pass the theme your buyer has on your site, and the page does not change colour under them; auto is the buyer’s own. Without it, the project’s setting (Settings → How it looks), auto unless you fixed one.
- metadataobject
- Up to 50 string values of your own, returned as given; keys up to 40 characters, 8 KB in all.
- expires_ininteger
- Seconds the link stays open: 5 min to 1 day, 30 min by default. A test session stays open 2 h at most: a longer one is shortened, not refused.
Retrieve a session
GET/v1/checkout/sessions/{id}
List sessions
GET/v1/checkout/sessions
Narrowed by status, payment_status, client_reference_id, customer_email and created — see Lists.
Expire a session
POST/v1/checkout/sessions/{id}/expire
Closes the link now, for an order cancelled on your side — also one whose link has already run out, which stays expired and is not announced again. From then on whatever still arrives — a payment already on its way included — is held for your review (review_reason closed), whatever your late-payment setting. closed_at says when.
An account holds at most 1,000 open live sessions and 200 open test ones at once, all its projects together; payment links’ sessions are not counted. Past that, creating one answers 429 rate_limit_error with Retry-After until some complete, run out or are expired — expire the ones whose orders you cancelled.
The session object
{
"id": "cs_live_9fK2…",
"object": "checkout.session",
"livemode": true,
"status": "complete",
"payment_status": "paid",
"amount_total": 2490,
"currency": "usd",
"amount_received": "24.90",
"amount_due": "24.90",
"amount_remaining": "0.00",
"amount_refunded": 0,
"payment_methods": ["usdt_tron", "usdc_solana", "btc"],
"payment": {
"asset": "usdt_tron",
"chain": "tron",
"coin": "USDT",
"amount": "24.90",
"rate_usd": "1",
"amount_usd": "24.90",
"pay_address": "TQ…",
"memo": null
},
"transactions": [
{
"signature": "a1b2…",
"asset": "usdt_tron",
"amount": "24.90",
"amount_coin": "24.90",
"rate_usd": "1",
"status": "finalized",
"confirmations": 20,
"dust": false,
"delivery": "held",
"from": "TR7NHq…",
"block_time": 1767225900,
"explorer_url": "https://tronscan.org/#/transaction/a1b2…"
}
],
"dust_count": 0,
"url":"https://orboni.com/pay/cs_live_9fK2…",
"client_reference_id": "order_1042",
"payment_link": null,
"customer_email": "ana@example.com",
"customer_email_collection": "required",
"locale": "auto",
"appearance": null,
"description": "Pro plan",
"details": null,
"metadata": {},
"paid_late": false,
"needs_review": false,
"review_reason": null,
"created": 1767225600,
"expires_at": 1767227400,
"completed_at": 1767225912,
"closed_at": null
}| Field | Values |
|---|---|
| status | open — waiting; complete — paid, final; expired — its time ran out unpaid (still watched for a late payment). An expired session with payment_status held is not unpaid: the money is there and waits for your review, whatever expires_at says. Do not cancel that order, and do not fulfil it until it completes. |
| payment_status | unpaid; processing — a transfer is seen, not final yet; partially_paid — final but short (the rest is amount_remaining); held — paid in full, but held for your review (needs_review), not to fulfil on yet; paid — only ever with status complete: fulfil on it. |
| amount_total | What you asked, in US cents, as you sent it. |
| amount_due | The same total in US dollars. It does not go down as money arrives. |
| amount_received | US dollars received in all coins together, each transfer at the rate quoted when it was sent. Money sent after completion still adds to it (checkout.session.extra_received), for as long as late payments are watched. A transfer that leaves the chain after it counted (status reversed) no longer adds to it (checkout.session.payment_reversed). |
| amount_remaining | US dollars still to send: amount_due less amount_received, and 0.00 once what arrived counts as paid (within your tolerance). |
| amount_refunded | US cents you have recorded as sent back (see Refunds). |
| payment | The coin most of the counted money came in, else the one the buyer picked last. Dust names neither, so after a coin switch it is the coin and address last picked. |
| transactions · dust_count | Every transfer of $0.01 or more, and the first 20 worth less (dust, below), oldest first; dust_count is how many dust transfers came in all. |
| payment_link | The id of the payment link a buyer opened it from (plink_…), or null for a session your server created. |
| paid_late | The money came after the link expired. |
| closed_at | When you closed the link (expire), or null. Whatever arrives after is held for your review (review_reason closed). |
| needs_review · review_reason | needs_review: we hold the payment for you to look at until you accept it in the dashboard (then the session completes). review_reason says why, and stays once you have accepted it: late — it came after expiry and your project holds late payments for review; amount — more than twice the amount and more than $50 above it reached a Bitcoin-style address, at any time (paid_late may then be true or false); refunded — the rest arrived after you had recorded a refund of it; closed — it arrived after you closed the link (expire); peg — part of it came in a stablecoin trading more than 2% away from the dollar; handed_over — the link was made before the account passed to the person who proved its email, and we closed it then; reversed — money that had counted as final left a Bitcoin-style chain (a reorg, or a double-spend mined in its place) and what is left no longer pays the session, or it was held already: it is no longer complete, and can be accepted only once what is on the chain pays it again. null when it was never held. A session whose money you have refunded in full cannot be accepted. |
A payment within your underpayment tolerance (1% by default, set on the payment page settings) counts as paid: exchanges often take their withdrawal fee from the amount sent.
When someone of your project accepts a held payment, counts a transfer or closes a link in the dashboard, everyone else who wants payment emails is still mailed; only the person who did it is not. A “Check network now” that finds the payment mails everyone, the person who pressed it included.
A transaction
Each entry of transactions is one transfer to the payment’s address, as the chain reports it.
| Field | Meaning |
|---|---|
| signature | The transaction’s hash (a signature on Solana); sim_… for one made by the simulator. |
| asset | The coin on its chain, as in payment_methods (usdt_tron). |
| amount | US dollars it counted for, at the rate quoted when it was sent. |
| amount_coin | The amount in the coin, with the coin’s own decimals ("24.90", "0.0332"). |
| rate_usd | The coin’s US dollar rate the transfer was counted at; "1" for a stablecoin. |
| status | confirmed — seen, not final yet; finalized — final, it counts; reversed — it left the chain after it had counted (a reorg or a double-spend) and no longer counts. |
| confirmations | Blocks mined since it, as last read. |
| dust | true for a transfer worth less than $0.01, which is never counted (below). |
| delivery | On Ethereum, its relatives and TRON, where the money is: waiting — not final yet; held — final, at the payment’s own address, to withdraw; sending — a withdrawal is on its way; delivered — in your wallet; left — withdrawn to the wallet the payment was quoted for, which is no longer the project’s wallet. null on networks that pay your wallet directly. |
| from | The address the transfer was sent from, as the chain reports it: for a buyer paying from an exchange that is the exchange’s hot wallet, on EVM and TRON possibly a contract. Never a refund address — ask the buyer for one on the same network. null when the chain does not say. |
| block_time | Unix time of the block it is in; null while unconfirmed. |
| explorer_url | The transaction on the chain’s explorer; null for a simulated one. |
A transfer worth less than $0.01 to a payment’s address — a zero-value or one-unit token transfer anyone can send, as in address poisoning — is listed in transactions with dust: true (the first 20; dust_count counts them all) and is never counted: it does not make the session processing or partially paid, sends no event, does not hide the pay address on the page, is in no amount (amount_received, payment.amount, payment.amount_usd, the receipt), and is never offered for withdrawal. A payment of nothing is not recorded at all.
Payment page
A session’s url is a page we host: your name and logo, what is bought and its price, the ways to pay, then where to send the coin, how much, and its confirmations as they come. It is drawn on its own — your site is not in a frame — and asks the buyer for nothing but an email when you want one. At its foot it says whom the buyer pays (“You are paying your name. Orboni only checks the payment.”) beside a Report this page link that writes to us; the payment link’s page does the same.
Set once, in the dashboard
Settings of each project, for every page it sends buyers to:
| Setting | On the page |
|---|---|
| Name on the page | Your name at the top, and in the page’s title. Without it, the project’s name. Neither may look like Orboni or a well-known wallet, exchange or payment brand (MetaMask, Binance, PayPal…), however it is spelled. |
| Logo | Beside the name. PNG, JPEG or WebP, at least 32 px, up to 2 MB; we scale it down to 256 px at most. Without it, your name’s first letter. |
| Accent colour | The buttons, the rings and your initial when there is no logo. Text on a button is black or white, whichever has the higher contrast. Standard: our aqua. |
| Background colour | The order panel — your name, what is paid for, the amount and the time left — in your colour, in light and dark alike; its text turns black or white, whichever reads better. Standard: a light tint of the accent colour. |
| Light or dark | As the buyer has it (their own choice, else their system’s), light or dark. A session’s appearance comes first. |
| Support email · website · terms · privacy policy · refund policy | At the foot: “Questions about this payment?” with the email, then a link to each page you give. |
| Email the buyer a receipt | Once a live payment completes, a receipt from Orboni to the buyer’s email (customer_email): the amount, the coin and the transaction, your website as text, and replies to your support email. One address gets at most 3 a day from every store together; an account’s projects send 1,000 a day together, 50 until the account is 30 days old or has paid an Orboni invoice. The rest are not sent. Spam reports or bounces on a project’s receipts switch them off: check the addresses you pass before you turn them on again. |
| Underpayment tolerance · late payments | What counts as paid — see needs_review above. Not shown to the buyer. |
Per session, through the API
| Parameter | On the page |
|---|---|
| description | What is bought, under your name: “Premium features × 2”. Two lines; a longer one opens with Show more. |
| details | Under it, smaller: what the buyer gets, in your words. Four lines, then Show more. |
| appearance | light or dark, else the project’s setting. Pass the theme your buyer has on your site, so the page keeps it. |
| locale | Its language (see Languages below); else the buyer’s own. |
| payment_methods · network | Which ways to pay it lists. |
| customer_email · customer_email_collection | Whether it asks for an email first. |
| expires_in | The countdown beside the price. |
| cancel_url · success_url | Once paid, the page takes the buyer back to success_url after a few seconds (“Taking you back to the store…”). cancel_url is “Cancel and go back” under the price, and “Back to the store” on a page whose time ran out. |
A test session’s page says Test mode, and shows where to pay only to the project’s owner, or an Admin or Developer of its team, signed in: a test address is often a real one too.
Languages
The payment page speaks 85 languages — among them Indonesian, Vietnamese, Thai, Hindi, Arabic and Hebrew (right to left), Turkish, Swahili and Haitian Creole. Without locale it opens in the buyer’s own language, from their browser; pass locale only to pin it to your store’s. Its messages follow the page.
The codes it takes: en, ru, uk, pl, es, es-mx, pt, pt-br, fr, de, ja, it, nl, ro, cs, zh-hans, zh-hant, ko, id, ms, vi, th, fil, hi, bn, ur, ta, te, mr, gu, kn, ml, pa, ne, si, my, km, lo, mn, kk, uz, ky, tg, az, ka, hy, ar, he, fa, tr, ps, el, hu, sv, da, nb, fi, sk, sl, hr, sr, bs, bg, mk, sq, lt, lv, et, be, ca, eu, gl, is, sw, am, ha, yo, ig, zu, xh, af, so, rw, mg, ht.
Payment methods
The keys for payment_methods. A project offers a method once it has a wallet for its network. Stablecoins are priced 1:1 with the dollar; while one trades more than 2% away from it, it is not offered and a payment in it is held for your review. Other coins are priced at a rate fixed when the buyer picks them, for 15 minutes or until the link expires, whichever comes first; each transfer is worth the rate quoted when it was sent, and one sent after the link expired is worth the rate when we see it. We take no rate our two price feeds disagree on, nor one far from the last when only one answers: the coin cannot be picked until they agree again. On Solana and the EVM networks, a coin the buyer has switched away from, or picked more than an hour ago, is read about once a minute rather than every few seconds: money sent to it may show up to a minute later.
| Key | Coin | Network | Final after | Live |
|---|---|---|---|---|
| usdc_arbitrum | USDC USD Coin | Arbitrum· via forwarder | Ethereum finality, about 15–20 minutes | Test mode only |
| usdc_avalanche | USDC USD Coin | Avalanche· via forwarder | a few seconds | Test mode only |
| usdc_base | USDC USD Coin | Base· via forwarder | Ethereum finality, about 15–20 minutes | Test mode only |
| usdc_bsc | USDC USD Coin | BNB Chain· via forwarder | 15 blocks, under a minute | Test mode only |
| usdc_ethereum | USDC USD Coin | Ethereum· via forwarder | 12 blocks, about 3 minutes | Test mode only |
| usdc_optimism | USDC USD Coin | Optimism· via forwarder | Ethereum finality, about 15–20 minutes | Test mode only |
| usdc_polygon | USDC USD Coin | Polygon· via forwarder | its finality, a few minutes at most | Test mode only |
| usdc_solana | USDC USD Coin | Solana | about 15 seconds | Yes |
| usdc_stellar | USDC USD Coin | Stellar | a few seconds | Yes |
| usdt_arbitrum | USDT Tether USD | Arbitrum· via forwarder | Ethereum finality, about 15–20 minutes | Test mode only |
| usdt_avalanche | USDT Tether USD | Avalanche· via forwarder | a few seconds | Test mode only |
| usdt_bsc | USDT Tether USD | BNB Chain· via forwarder | 15 blocks, under a minute | Test mode only |
| usdt_ethereum | USDT Tether USD | Ethereum· via forwarder | 12 blocks, about 3 minutes | Test mode only |
| usdt_optimism | USDT Tether USD | Optimism· via forwarder | Ethereum finality, about 15–20 minutes | Test mode only |
| usdt_polygon | USDT Tether USD | Polygon· via forwarder | its finality, a few minutes at most | Test mode only |
| usdt_solana | USDT Tether USD | Solana | about 15 seconds | Yes |
| usdt_ton | USDT Tether USD | TON | about 10 seconds | Yes |
| usdt_tron | USDT Tether USD | TRON· via forwarder | about 1 minute (solidified) | Test mode only |
| avax | AVAX Avalanche | Avalanche· via forwarder | a few seconds | Test mode only |
| bch | BCH Bitcoin Cash | Bitcoin Cash | 11 blocks, about two hours | Yes |
| bnb | BNB BNB | BNB Chain· via forwarder | 15 blocks, under a minute | Test mode only |
| btc | BTC Bitcoin | Bitcoin | 2 blocks, about 20 minutes | Yes |
| doge | DOGE Dogecoin | Dogecoin | 20 blocks, about 20 minutes | Yes |
| eth_arbitrum | ETH Ether | Arbitrum· via forwarder | Ethereum finality, about 15–20 minutes | Test mode only |
| eth_base | ETH Ether | Base· via forwarder | Ethereum finality, about 15–20 minutes | Test mode only |
| eth | ETH Ether | Ethereum· via forwarder | 12 blocks, about 3 minutes | Test mode only |
| eth_optimism | ETH Ether | Optimism· via forwarder | Ethereum finality, about 15–20 minutes | Test mode only |
| ltc | LTC Litecoin | Litecoin | 6 blocks, about 15 minutes | Yes |
| pol | POL Polygon | Polygon· via forwarder | its finality, a few minutes at most | Test mode only |
| sol | SOL Solana | Solana | about 15 seconds | Yes |
| ton | TON Toncoin | TON | about 10 seconds | Yes |
| trx | TRX TRON | TRON· via forwarder | about 1 minute (solidified) | Test mode only |
| xlm | XLM Stellar Lumens | Stellar | a few seconds | Yes |
| xrp | XRP XRP | XRP Ledger | a few seconds | Yes |
List the project’s payment methods
GET/v1/payment_methods
What a session made with this key would offer now: the ways to pay of every wallet your project has, in the key’s mode, in the order the payment page lists them. Show them beside your buy button (“BTC · USDT”) from here instead of a list of your own: add or remove a wallet in the dashboard and the answer follows. Cache it for a few minutes rather than asking on every page view. Live, the list is empty while the project could not take a payment — receiving frozen, its email not confirmed, an Orboni invoice past due — since a session would be refused then.
- amountinteger
- US cents. Leaves out the methods whose minimum is above that amount (see minimum_amount).
curl -G https://orboni.com/v1/payment_methods \
-u sk_live_…: \
-d amount=499| Field | Values |
|---|---|
| key | What payment_methods on a session takes. |
| coin · name | The coin, as USDT and Tether. One coin on several networks is several methods: show it once. |
| chain · chain_name | Its network, as tron and TRON. |
| stable | A dollar stablecoin, priced 1:1. |
| from_exchange | It can be sent from an exchange account, not only from a wallet. |
| minimum_amount | US cents: the least a session paid in it may ask — on the networks you withdraw from yourself, so that taking it out costs less than it brings (see Withdrawals). null: any amount from the API’s minimum. |
| simulated_only | Test mode: there is no test network we watch for it, so it is paid only with Simulate on the page. |
On Ethereum, its relatives and TRON a payment goes to an address of its own, which only ever sends to the wallet your project had when the payment was quoted — your wallet, as a rule; you take the money out yourself, in batches, from the dashboard (see Withdrawals). The session is paid and checkout.session.completed fires once the transfer to that address is final. Each transaction’s delivery says where the money is: waiting (not final yet), held (final, at the payment’s address), sending (a withdrawal is on its way), delivered (in your wallet), left (it left the payment’s address for the wallet the payment was quoted for, which is not your project’s wallet now: one you changed or removed since, or one a freeze withdrew) — and is null on networks that pay your wallet directly. delivered and left describe your wallet as it is today: change it and an earlier delivery to the old wallet reads as left; put the old wallet back and it reads as delivered again.
An EVM forwarder has the same address on every EVM network. In live mode, for 14 days after a payment, we look on the other EVM networks for the stablecoins listed here, their bridged twins (USDC.e, USDbC) and the coin sent to its address by mistake. What we find waits on the Withdrawals page under that network, and goes to your address there — only when it is the same wallet (a contract wallet deployed only where you use it is not); it never counts toward the payment. Other tokens, or money sent later, are not looked for. In test mode these networks show no address to copy for an exchange: pay from a test-network wallet, or simulate.
When you save an EVM address we check it on every EVM network we have a node for — Ethereum included, before it is live here — so a contract wallet (a Safe) deployed on one network is not offered the coins of the others, where its address belongs to nobody. A network that does not answer is not a pass: we tell you which one it was, we check again every hour, and until a check passes a payment on a network we cannot read right now is refused rather than quoted. A Safe deployed on a network is paid there whatever the other networks say.
A coin whose wallet needs something from you first — an extended key not yet compared, a Stellar or XRP account not activated, a trustline missing — is not offered to buyers in live mode, nor in sessions already open, until its next check passes (hourly, or Check again on the Wallets page). Nor is a wallet connected before the account’s email was proven, until you press This is my wallet on it or save another. An address already given to a buyer stays theirs to pay.
Bitcoin, Litecoin, Dogecoin and Bitcoin Cash take an extended public key, and every payment gets an address of its own from it. Until you confirm on the Wallets page that the first addresses match your wallet, the coin is not offered in live mode. Use an account you receive nothing else on: your wallet’s Receive tab shows the very addresses we hand to buyers, and whatever reaches one counts for that buyer’s payment. Only far more than asked is held for your review (review_reason amount); a smaller payment from someone else still completes the session, so the dedicated account is what keeps them apart. An amount below the network’s dust limit is asked as that limit (546 satoshi for Bitcoin). A wallet looks 20 addresses ahead: once that many wait unpaid, an address whose link has been expired for 24 hours is used again, and failing that the next one goes further. The payment is still seen and counted, and your wallets page says what gap limit to set so your wallet shows it too. One key hands out at most 20 addresses past that limit an hour, whoever asks; beyond them the coin is refused until the hour turns. One visitor (an IPv6 one by its /64) takes at most 10 new addresses of a project while they are held — the session and its late watch, about 1 day — and is then answered 429 for that coin; in test mode an account’s team takes 20.
A larger payment waits for more blocks: Bitcoin 3 from $1,000 and 6 from $10,000; Litecoin 12 from $10,000; Dogecoin 40 from $10,000. What decides it is the transfer or the amount asked, whichever is more — and, since one reorg takes back whole blocks, every payment to your account on that network in the same block or a later one: several payments in one transaction, or in the same blocks, wait as long as their total must.
On these networks a block can still be undone after a transfer counts as final: a reorg deeper than its confirmations, or a double-spend mined in its place. We read a final transfer again until it is buried 3 times as deep as it had to be, or for 24 hours after its block. One that stays out of every block is reversed: it no longer counts, and checkout.session.payment_reversed is sent. If what is left no longer pays the session, it is no longer complete: it is held for your review (review_reason reversed) — do not ship an order not shipped yet, and look at one that was. A reversed transfer mined again deep enough counts again.
Dogecoin is read through a provider with an hourly and a daily budget, so in live mode it takes only a few payments at once — 2 open per account, 8 across all stores — and none while the provider is paused or the hour’s or the day’s reads are spent. A session that runs for many hours takes more of the day’s reads, so fewer long Dogecoin sessions fit at once, and one of a whole day may be refused. The payment page then leaves it out when the session offers another coin, and a quote in it is refused. An unpaid Dogecoin address is read after its session closes only about 10 minutes, an hour and 6 hours later and an hour before its late watch ends: a late payment to it shows then.
A transfer we see on its way that no block takes within 3 h no longer keeps its session open past its time. When a block takes it later, it counts as sent at that block’s time: that decides whether it is late and, for a coin that is not a stablecoin, which rate it is worth. Its session is watched until a block takes it or it is gone, for up to 14 days, even past the late watch.
Withdrawals
On Ethereum, BNB Chain, Base, Polygon, Arbitrum, Optimism, Avalanche and TRON every payment gets an address of its own: a small contract (a forwarder) that can send what it holds to the wallet your project had when the payment was quoted, and nowhere else. You empty them from the dashboard’s Withdrawals page with your own wallet — MetaMask or any browser wallet on EVM networks, TronLink on TRON. One transaction takes up to 25 payments at once — 10 on TRON, where a transaction has 80 ms of CPU time — and your wallet pays its gas.
Why it works this way
A buyer paying from an exchange account cannot add a memo or an exact amount we could match, so a payment needs an address of its own — and money at a fresh address needs gas to move again. We hold no keys to your money, so nothing can move it but a contract that pays only you, and someone has to pay that gas. Paying it ourselves would mean holding funds on every network and charging you for it in your rates; instead the gas is yours, paid when you choose, and our fee stays the one fee on every network: the same percentage, on the same monthly invoice. Nothing is taken out of a payment on its way to you.
You get three emails about each invoice — when it is issued, three days before it is due, and once it is past due. New live payments pause only after that last email, and only while the invoice can be paid online from your dashboard; test mode and payment pages already open keep working.
What it costs
A withdrawal costs the network’s gas for emptying each address in it. On BNB Chain, Base, Polygon, Arbitrum, Optimism and Avalanche that is cents for a whole batch. On Ethereum it is dollars, so wait until several payments add up. On TRON it is energy for each address: about 130,000 energy (13 TRX at today’s 100 sun per energy) for a USDT payment when your wallet already holds USDT, about 195,000 (19.5 TRX) for the first USDT your wallet ever receives, about 70,000 (7 TRX) for a TRX payment — burnt as TRX unless your wallet has staked energy. The energy price is a chain parameter and has been four times today’s; a batch of ten USDT payments is about 130 TRX. Before you sign, the Withdrawals page says the most a batch may burn (its fee limit, sized from the energy price of the day and the tokens the batch moves, rounded up to a whole TRX): your wallet must hold that much TRX, or the energy; TRON charges only what is used. That is why a payment on these networks must be at least $1, and why the cheaper networks are the better choice for small amounts.
Good to know
The payment is complete and its webhook sent as soon as the transfer to its address is final; withdrawing is only about when the money reaches your wallet, and you may do it whenever you like. Anybody on your team who can handle payments can withdraw: whichever wallet signs, the money goes where each address pays: the project’s wallet as it was when the payment was quoted. If you changed or removed that wallet since, those payments wait on a card of their own under the old address, and withdrawing them sends the money there — not to your wallet now; the change’s record and its email say how many payments still wait there. You need not use the dashboard either: flushMany(owners, salts, tokens) — or flush(owner, salt, tokens) for one — on the factory, with the values shown on each payment, does the same from any wallet. We read the network to see what has left each address, every minute for an hour after you withdraw and once a day otherwise.
After a withdrawal from the dashboard we also read the transaction’s own record once it is final. One that moved nothing — it reverted, ran out of gas or energy, or on TRON ran out of its 80 ms of CPU time — still cost your wallet its fee: it shows as failed in the list with the network’s reason, and its payments are offered again at once. One the network never mined — replaced from your wallet, or never sent — is treated the same a day later, and nothing was paid for it. An address the factory could not empty (a token that refuses to pay from it, as a blacklist does, or a flush over its budget) is listed under “Could not be delivered” with the transaction, left out of the usual batch, and tried again only when you ask; if it fails again, write to support.
Each transaction’s delivery says delivered only once the network’s own record or events show the money left its address, never because a balance reads empty — and dust someone sends to an address after you emptied it changes nothing. A wei or a single unit of a token sent to a payment’s address on another network (address poisoning) is never offered: nobody pays a deployment to move nothing. Totals in the dashboard, reports and exports count a payment once it is final at its address, withdrawn or not; the dashboard’s “Waiting to withdraw” and the export’s delivery column say how much is still there.
You cannot delete your Orboni account while live money you can withdraw still waits at a project’s payment addresses: withdraw it first. Money on a network whose withdrawals are paused does not stop the deletion; it stays where it is until withdrawals are on there. If the account is closed by our team or erased later, the money stays where it is — only the project’s wallet can receive it — and support can hand you the withdrawal call (the factory’s flushMany with each address’s owner and salt) to send from that wallet.
Payment button
orboni.js draws a “Pay with crypto” control on your own page, in your site’s colours and fonts, with the coins your project takes now. It does not take the payment there: a click asks your server for a checkout session, as in the quick start, and sends the buyer to its page on https://orboni.com/pay/…. Two elements:
| Element | What it is |
|---|---|
| button | A button: “Pay $4.99 with crypto” and the coins. A click starts the checkout. |
| option | A row for a list of ways to pay, beside your card form: a radio, “Crypto”, the coins, and a line on how it works once picked. Your own Pay button calls element.checkout(). |
Add it to a page
Load the script from our host (not a copy of your own: v1 only changes in ways that keep pages working) and start it with your publishable key, pk_test_… or pk_live_…, under Developers → API keys. It is safe in a page: it only reads which coins you take. The secret key stays on your server. A change to v1 reaches every page within five minutes: the script is served with that much cache, and a page already open keeps its copy until it is reloaded.
Content Security Policy. If your page sends one, allow our host in two directives: script-src https://orboni.com for the script and connect-src https://orboni.com for the coins it reads (an element given its coins reads nothing). Nothing else: the control draws its styles through a stylesheet it builds itself, which style-src does not govern, so no 'unsafe-inline' is needed. A browser without constructed stylesheets (before Chrome 73, Firefox 101, Safari 16.4) gets a <style> inside the control instead; where your style-src names a nonce, put it on the script tag too — its nonce, or data-nonce — and that <style> carries it.
<div id="crypto-pay"></div>
<script src="https://orboni.com/js/v1/orboni.js"></script>
<script>
const orboni = Orboni('pk_test_…', { locale: 'auto' });
orboni.create('button', {
amount: 2490, // US cents: "Pay $24.90 with crypto"
checkout: async () => {
// Your server creates the session (secret key) and answers its url.
const res = await fetch('/orders/1042/crypto', { method: 'POST' });
return res.json(); // { "url": "https://orboni.com/pay/cs_test_…" }
},
}).mount('#crypto-pay');
</script>checkout may answer the session, its url, { checkout_url } or its id cs_…. The buyer is only ever sent to a payment page on https://orboni.com: any other answer is refused, shown as an error on the element and followed nowhere. While no coin can be taken (no wallet yet, the amount too small for every network, live receiving frozen) the element hides itself, so no control leads nowhere.
Options
- Orboni(key, options)function
- key: the publishable key; it may be null when every element is given its coins. options.locale: auto (the browser’s language, the default) or a tag such as uk or pt-BR; options.appearance: for every element made from it.
- amountinteger
- US cents. Shown on the button and sent with the coin lookup, so a coin whose network cannot carry that amount is left out.
- checkoutfunction
- Creates the session on your server; returns (or resolves to) its url, the session or its id.
- coinsstring[]
- The coins to show, if you read them yourself (GET /v1/payment_methods on your server). Leave it out to have the element read them with the publishable key.
- labelsobject
- Your own words for option, button (with :price), buttonPlain (no amount), hint and error, in place of ours.
- showCoins · maxCoinsboolean · integer
- Show the coin badges (true) and how many before “+2” (4).
- hintboolean
- false: no explanation under a picked option.
- disabledboolean
- Greys the control out, as while your form is incomplete.
- appearanceobject
- This element’s look, over the one given to Orboni().
Our texts come in English, Russian, Ukrainian, Polish, Spanish, Portuguese (and Brazilian), French, German, Japanese, Italian, Dutch, Romanian and Czech; another language falls back to its base, then to English. The price is written in the buyer’s language, in US dollars.
Methods and events
| Call | What it does |
|---|---|
| mount(target) | Draws the element into a CSS selector or an element of yours. |
| update(options) | Changes any option; a new amount reads the coins again. |
| checkout() | Starts the checkout, as a click on the button does. |
| select() · deselect() | Picks the option, or takes it back. |
| destroy() | Removes it from the page. |
| on('ready') | { coins }: drawn with these coins; an empty list means it is hidden. |
| on('change') | { selected }: the option was picked or taken back. |
| on('click') | The checkout started. |
| on('error') | { type, message }: payment_methods (the coins could not be read) or checkout (your checkout failed or answered no session). |
| orboni.redirectToCheckout(session) | Sends the buyer to a session’s page with no element: its url or id. |
| orboni.paymentMethods({ amount }) | The methods, as the public endpoint below lists them. |
Appearance
As Stripe’s Appearance API: a theme, then variables over it. The element draws in its own shadow root, so your page’s CSS does not leak into it; the font is your page’s unless you name one.
Orboni('pk_live_…', {
appearance: {
theme: 'auto', // light (default), dark, or auto: the buyer's
variables: {
colorPrimary: '#ff5a1f',
colorPrimaryText: '#ffffff',
borderRadius: '999px',
fontFamily: '"Inter", sans-serif',
buttonHeight: '52px',
},
},
});| Variable | Light | Dark | Used for |
|---|---|---|---|
| colorPrimary | #16171b | #f2f3f5 | The button, a picked option’s border and radio |
| colorPrimaryText | #ffffff | #16171b | Text on the button |
| colorBackground | #ffffff | #1b1d22 | The option |
| colorText | #16171b | #f2f3f5 | Labels |
| colorTextSecondary | #5d6170 | #a3a7b3 | Coins, the hint, an unpicked radio |
| colorBorder | #d9dbe3 | #3a3d46 | The option, coin badges |
| colorDanger | #c0362c | #f0776c | The error line |
| fontFamily | inherit | inherit | All text |
| fontSizeBase | 15px | 15px | All text, scaled from it |
| fontWeightNormal · fontWeightMedium | 400 · 600 | 400 · 600 | Text · labels and the button |
| borderRadius | 10px | 10px | The button and option; badges take half |
| spacingUnit | 4px | 4px | Padding and gaps, in multiples |
| buttonHeight | 48px | 48px | The button’s least height |
Parts: button, option, option-selected, option-header, radio, icon, label, coins, coin, hint, error. A value with ;, { } or < > is cut out of it.
The coins, with a publishable key
GET/v1/public/payment_methods
What the element reads: the same list as GET /v1/payment_methods, for the key’s mode, without simulated_only. The key goes in the query, any site may ask (CORS), and the answer may be cached for a minute. An unknown key answers 401; a project that could not take a payment now answers an empty list. At most 120 requests a minute from one address; beyond it 429 with Retry-After, which any site can read.
curl "https://orboni.com/v1/public/payment_methods?key=pk_live_…&amount=2490"Building your own control instead? Read the coins on your server with GET /v1/payment_methods, draw them as you like, and send the buyer to the session’s url. orboni.js is a shortcut, not a requirement.
Payment links
No code at all: in the dashboard, under Payment links, say what it is for and how much, and share the link you get — https://orboni.com/l/plink_… — in a message, a post, an invoice, or as the QR code its card draws (SVG for print, PNG for a post). Each buyer who opens it gets a checkout session of their own, on the same payment page; one who comes back while theirs is still open is sent to it rather than given a second.
The amount is fixed, or the buyer chooses it (a tip, a donation, an invoice of any sum): the link’s page then asks for it in US dollars, between the least and the most you allow, starting from the amount you suggest. A link may also stop by itself — after so many paid payments, or at the end of a day you pick.
The sessions are like any other: they are in Payments (filtered by the link from its card), in exports, in the API and in every webhook, where payment_link names the link. A link has no order number of its own, so match such payments by payment_link and customer_email. With “After payment, send to” set, the buyer goes there once paid, as with success_url.
Changing a link applies to the payments opened after; the ones already open keep what it said. A link switched off, past its date or with all its payments taken says so, and while your account cannot take live payments (email not confirmed, receiving frozen, an invoice of ours past due) it says it cannot take payments now and asks the buyer to contact you. A program reading the link — a messenger drawing its preview, a search engine — is shown what is paid for and opens no payment, and so is another page loading it as an image, a script, a frame or a prefetch: only a visit opens one. A link with all its payments taken takes no new coin choice on payments already open from it either.
A link keeps at most 100 open payments in which nobody has picked a coin; past that a new visitor closes the oldest. Such a payment — and one that runs out with no coin picked — closes without checkout.session.expired. A project has at most 200 links switched on in each mode. A change to a live link’s amount, its limits or where it sends buyers is emailed to the project’s owner.
Create a payment link
POST/v1/payment_links
- descriptionstring, required
- What is paid for, up to 250 characters. The buyer sees it on the link’s page and the payment page.
- amountinteger
- US cents, from 50 to 10000000. Required unless custom_amount; with it, the amount suggested to the buyer.
- custom_amountboolean
- The buyer types the amount. Default false.
- min_amount, max_amountinteger
- With custom_amount: the least and the most the buyer may pay, in US cents. Default 50 and 10000000; the least is raised to what the networks you take can carry at the time.
- max_paymentsinteger
- The link takes no new payment once this many are paid.
- expires_attimestamp
- Unix seconds: the link takes no new payment from then on. Up to a year ahead.
- detailsstring
- What the buyer gets, in your words, up to 500 characters.
- success_urlstring
- Where the buyer goes once paid.
curl https://orboni.com/v1/payment_links -u sk_live_…: -H "Idempotency-Key: link-invoice_2207" -d description="Logo design" -d amount=4900 -d max_payments=1status says whether the link takes payments: active; inactive — switched off; expired — past expires_at; completed — payments_paid reached max_payments.
List payment links
GET/v1/payment_links
Newest first; active=true or false narrows it to links switched on or off.
Retrieve a payment link
GET/v1/payment_links/{id}
Update a payment link
POST/v1/payment_links/{id}
Send only what changes: any field of create, and active=false to switch the link off (true to switch it on again). A link is never deleted. An empty expires_at or max_payments removes the limit.
Refunds
Orboni never holds your money, so it cannot send a refund for you. Ask the buyer for a refund address on the same network and send the coins back from your own wallet first, then record it here: the session’s amount_refunded, your reports and a refund.created webhook follow. Never send to the address the payment came from (transactions[].from): an exchange’s or a contract’s address does not credit the buyer. Our fee on the payment is not returned. A refund recorded by mistake can be voided: it then no longer counts, and refund.voided is sent.
Record a refund
POST/v1/refunds
- checkout_sessionid, required
- The session refunded.
- amountinteger
- US cents. Default: all that is left. Never more than was received less what was refunded.
- reasonstring
- requested_by_customer, duplicate, fraudulent, overpaid, other.
- transactionstring
- The hash of your refund transfer, or any reference you keep.
- notestring
- Up to 500 characters.
- metadataobject
- Up to 50 string values of your own; keys up to 40 characters.
curl https://orboni.com/v1/refunds \
-u sk_live_…: \
-H "Idempotency-Key: refund-order_1042" \
-d checkout_session=cs_live_9fK2… \
-d amount=990 \
-d reason=requested_by_customer \
-d transaction=5h9x…List refunds
GET/v1/refunds
Newest first; checkout_session narrows it to one session's.
Retrieve a refund
GET/v1/refunds/{id}
Void a refund
POST/v1/refunds/{id}/void
Only for a refund recorded by mistake: no coins move. It answers the refund with status voided and voided_at set, takes its amount out of the session's amount_refunded and sends refund.voided. Voiding it again changes nothing.
A refund can also be recorded on the payment in the dashboard; it then says source: "dashboard".
Events
Everything we send to your webhooks is kept as an event, so a server that was down longer than our retries can catch up, and any event can be sent again.
List events
GET/v1/events
Narrowed by type, checkout_session and created. Each carries pending_webhooks: the endpoints that have not taken it yet.
Retrieve an event
GET/v1/events/{id}
Send an event again
POST/v1/events/{id}/resend
Queues it now to every enabled endpoint of the mode that takes its type, with a fresh set of retries — an endpoint added since gets its first copy. 30 a minute.
# Everything that completed since your last good webhook
curl -G https://orboni.com/v1/events \
-u sk_live_…: \
-d type=checkout.session.completed \
-d "created[gt]=1767225600" \
-d limit=100Webhooks
Add an endpoint under Developers → Webhooks, one set per mode. We POST each event to it as JSON. Answer any 2xx within 5 seconds, connected within 3, your host’s name answering within 2; anything else is retried after 1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 1 day, 1 day — about 3 days in all — and then marked failed. Failed deliveries can be sent again from the dashboard or the API.
An endpoint that does not answer — nothing in time, or a failure that took more than half of it — 3 times in a row is paused for 5 min, twice as long after each such failure since, up to 1 h. A try that falls in the pause is not sent: it fails — the dashboard says why — and counts as one of the event’s tries, and the next comes after the pause. A 2xx ends it — a delivery’s, or a test event’s sent from the dashboard. A quick 4xx or 5xx never pauses an endpoint.
An account’s endpoints, all its projects together, get about 20 seconds of sending a minute in each mode; past that its deliveries wait their turn, using no try, so a slow server delays its own account’s webhooks and nobody else’s. Past 600 events a minute in a mode, the rest go out with the retries, 100 of an account’s a minute. A suspended project’s webhooks wait until it is active again.
An endpoint is a public address of your server: a live one starts with https://, and addresses on a private network, localhost or ours are refused; the address is checked again before every send. Write the name in Latin letters with no dot at the end (a name in another script in its xn-- form). A redirect is not followed — it fails the delivery, so give the final address. We keep the first kilobyte of your answer for the dashboard. Up to 16 endpoints in each mode.
To try an endpoint before a payment depends on it, press Send test event on it and pick a type: a sample of that event, signed with the endpoint’s secret, goes once and at once, and the dashboard shows what your server answered and what was sent. Its ids are all zeros (evt_000…, cs_live_000…), it is in no list of events and it is never retried — look such an id up and nothing is found, so fulfil nothing for it. Each person may send 10 a minute; when many are being sent at once it asks you to try again in a few seconds.
| Event | Sent when |
|---|---|
| checkout.session.payment_detected | A transfer is seen on the chain, not final yet. Tell the buyer to wait; do not fulfil. |
| checkout.session.partially_paid | Final transfers came, short of the amount. The link stays open for the rest. |
| checkout.session.completed | Paid. Fulfil the order on this one. |
| checkout.session.expired | The link’s time ran out unpaid. Not sent for a payment link’s payment in which nobody picked a coin. |
| checkout.session.needs_review | Money is held for your review (review_reason; payment_status held): it came after expiry and your project holds late payments for review, far more than asked reached a Bitcoin-style address, it came after a refund or after you closed the link, or in a stablecoin off its dollar. Do not fulfil yet; accept it in the dashboard, and checkout.session.completed follows. |
| checkout.session.extra_received | More money reached a completed session (a second payment, say): amount_received says how much in all. It is not a second order — send back what is not owed, then record the refund. Ask the buyer for a refund address on the same network; never send to the address the payment came from (transactions[].from): an exchange’s or a contract’s address does not credit the buyer. |
| checkout.session.payment_reversed | A transfer that had counted as final left the chain (a reorg, or a double-spend on a Bitcoin-style network): its status is reversed and amount_received no longer counts it. If what is left no longer pays the session, it is held for your review (review_reason reversed) and no longer complete: do not fulfil on it, and check an order you already did. It counts again if it is mined again. |
| refund.created | You recorded a refund. data.object is the refund, not the session. |
| refund.voided | A refund recorded by mistake was voided: it no longer counts in amount_refunded. data.object is the refund, with status voided. |
Check the signature
Every request carries Webhook-Signature: t=…,v1=…: v1 is the hex HMAC-SHA256 of {t}.{raw body} with the endpoint’s signing secret (whsec_…, under the endpoint in the dashboard) — Stripe’s scheme. Compare in constant time, use the raw body as received (not re-encoded JSON), and refuse a t more than 5 min old.
Roll secret under the endpoint makes a new one. For 24 hours the old secret signs too, so each request carries one v1 per secret (t=…,v1=…,v1=…): accept it when any of them matches, as the samples below do, and move your server to the new secret within that time. When the account changes hands its secrets are changed at once, with no such overlap.
public function __invoke(Request $request)
{
$body = $request->getContent();
$t = '';
$signatures = [];
foreach (explode(',', (string) $request->header('Webhook-Signature')) as $part) {
[$k, $v] = array_pad(explode('=', $part, 2), 2, '');
if ($k === 't') {
$t = $v;
} elseif ($k === 'v1') {
$signatures[] = $v;
}
}
$expected = hash_hmac('sha256', $t.'.'.$body, config('services.orboni.webhook_secret'));
// While a secret is rolled there is one v1 per secret: any may match.
$valid = array_filter($signatures, fn ($v1) => hash_equals($expected, $v1)) !== [];
abort_unless(ctype_digit($t) && abs(time() - (int) $t) <= 300 && $valid, 400);
$event = json_decode($body, true);
// Production takes live events only: a test payment fulfils nothing.
if (!$event['livemode'] && app()->isProduction()) {
return response()->noContent();
}
if ($event['type'] === 'checkout.session.completed') {
// Once per event: the same one may arrive twice.
Order::whereKey($event['data']['object']['client_reference_id'])
->where('paid', false)
->update(['paid' => true]);
}
return response()->noContent();
}Good to know
- Fulfil on checkout.session.completed, not on the buyer coming back to success_url: the buyer may close the tab, and a URL proves nothing.
- The same event can arrive more than once. Its id (evt_…) is unique: keep the ones you have handled.
- Events can arrive out of order. Read status and payment_status from the object, or retrieve the session.
- Answer first, work after: a slow endpoint times out, is retried, and is paused if it keeps timing out.
- In production, ignore events with livemode false and sessions whose id starts with cs_test_: a test key must never fulfil a real order.
Testing
With a test key everything works as in live mode — the page, the statuses, the webhooks — and no real money moves. A test page shows where to pay only to the project’s owner, or an Admin or Developer of its team, signed in; anyone else is told it is a test. Pay on test networks (Solana devnet, Sepolia, TRON Nile, Bitcoin testnet and the rest) or skip the faucets: open a test payment in the dashboard, or its page, and press Simulate payment for any coin, in full or in part. A simulated transfer goes through the same code as a real one, your webhooks included. On the payment page the button is shown only to them: a buyer given a test link cannot mark it paid.
- Part of it: the session becomes partially_paid and waits for the rest.
- Late: a payment that comes after the link ran out by itself is paid late, or held for review if you chose so; it is watched for 24 hours after expiry. Simulate works only while a session is open, so try it on a test network: create a session with a short expires_in and pay it once it has run out.
- Closed: a link you close yourself (expire) holds whatever still arrives for your review (review_reason closed), whatever your late-payment setting.
- Webhooks: point a test endpoint at a tunnel (ngrok, Cloudflare Tunnel) to your machine.
Test mode is for trying things, not for load: a test session stays open 2 h at most, an account holds 200 open at once, test keys make 120 POSTs a minute per account, and test payments are read on a budget of their own, so a large test run is seen more slowly. Test sessions, with their payments, events and deliveries, are deleted 30 days after they were made.
Limits
| What | Limit |
|---|---|
| Requests | 600 a minute per project and mode, 1,200 per account (every project and mode together), and of those 120 POSTs with test keys; 429 beyond it, with Retry-After. |
| Open sessions | 1,000 live and 200 test per account at once, payment links’ not counted; 429 beyond it. |
| Sending an event again | 30 a minute. |
| Amount | $0.50 to $100,000 a session. |
| EVM and TRON payments | At least $1; up to 25 withdrawn in one transaction (10 on TRON). |
| A link stays open | 5 min to 1 day; a test one 2 h at most. |
| description · details | 250 and 500 characters; longer is refused (422), not cut. |
| metadata | 50 keys of up to 40 characters, 500 characters a value, 8 KB in all as JSON (a character outside ASCII counts as 6 bytes). |
| Secret keys · payment links | 20 keys and 200 switched-on links per project and mode. |
| Request body | 64 KB. |
| Webhook endpoints | 16 in each mode. |
| Test webhooks | 120 sent a minute per project; the rest wait their turn, up to a day of them, and any beyond fail. Once 1,000 wait, new test events are not sent and a test event’s resend answers 400. |
| Webhook sending | 5 seconds a try; about 20 seconds of sending a minute per account and mode. |
| Test data | Deleted 30 days after it was made. |
| Exports | 50,000 payments a file. |
| Lists | 100 objects a page. |
Something missing or unclear? Write to us from the dashboard’s Help & Support.