Everything the button does, written down.
The button is a URL. This page says what goes in the URL, what a claimed short link adds, how the button sits on your own site, how a payment is confirmed and what your server hears about it. There is no API key and no account to sign up for. The URL is the API.
The URL
Every button is this URL with your details filled in. The maker on the front page writes it for you, and anything that can write a URL can write one too: a person, a spreadsheet formula, a shop plugin or an AI agent that has been asked to get someone paid.
https://theorangebutton.com ?address={lightning_address} &product={name} &amount={number} ¤cy={USD|EUR|GBP|AUD|CAD|sats} // optional, USD when left out &collect=email // optional &ref={string} // optional ¬ify={https url} // optional
| Parameter | What it does | |
|---|---|---|
address | required | Your lightning address, the one that looks like an email: you@coinos.io. The pay page asks the wallet behind it for the invoice. A raw LNURL string is not accepted here. |
product | required | What the buyer is paying for. It is shown on the pay page, named in your payment email and written on the sales record. Up to 60 characters. |
amount | required | The price, in the currency below. Any number above zero. |
currency | optional | USD, EUR, GBP, AUD, CAD or sats. A fiat price is converted to sats when the buyer opens the page, at that moment's rate. USD when left out. |
collect | optional | Set to email and the buyer is asked for their name (optional) and email (required) before the invoice is made. Both travel with the payment; see the buyer's name and email. No other value is read. |
ref | optional | Any string up to 120 characters: an order number, a table, a song. It comes back to you in the webhook and is kept with the sale on a claimed link. A short link takes it too, as /yourname?ref=4821. |
notify | optional | An https URL. When the payment settles, our server POSTs the webhook to it. This is for URL-built buttons; a claimed short link sets its webhook once, on the edit page. |
Encode the values the way any URL wants them: a space as + or %20, an ampersand inside a product name as %26.
Examples
// A plain price https://theorangebutton.com?address=satoshi@coinos.io&product=Ebook&amount=20 // Ask the buyer who they are https://theorangebutton.com?address=satoshi@coinos.io&product=Cake+recipe&amount=10&collect=email // Priced in sats https://theorangebutton.com?address=satoshi@coinos.io&product=Sticker+pack&amount=5000¤cy=sats // Tell your own server when it is paid https://theorangebutton.com?address=satoshi@coinos.io&product=Order+4821&amount=35&ref=4821¬ify=https://your-site.com/paid
The button as HTML
This is what the maker hands out under "Button for your site": an ordinary link, styled orange, that opens the pay page. It works anywhere HTML does.
<a href="https://theorangebutton.com?address=satoshi@coinos.io&product=Ebook&amount=20" style="display:inline-block;padding:12px 24px;background:#f7931a;color:#fff;border-radius:10px;font-family:sans-serif;font-weight:bold;text-decoration:none;box-shadow:0 4px 0 #c9700d">⚡ Pay with Bitcoin</a>
For AI agents
Construct the URL from the seller's lightning address, the product and the price, and return it as a link. That is the whole integration. Nothing has to be authenticated and nothing has to be registered first; the pay page comes into being the moment somebody opens the URL.
Short links
A short link is theorangebutton.com/yourname. You claim one from the maker after making a button. It is free, and it asks for your email, because that is where your edit link is sent. The short link carries the address, the product, the price and every setting with it, so the URL you share stays clean, and it is what makes the rest of the site work:
- Payment emails. An email lands in your inbox when a payment settles, with the product, the amount, and the buyer's name and email when they gave them. It can be switched off on the edit page.
- Automatic delivery. Attach a message, a link or a file on the edit page. When a payment settles, the buyer sees it on the pay page under Paid ✓ and gets it by email if they gave one. You do nothing.
- A record of who bought. Every confirmed payment is listed on the edit page with the buyer's details, and the whole list downloads as a CSV.
- A link for every item.
theorangebutton.com/yourname?product=Flat%20whiteopens that one thing's pay page, at the price on your list today. Your edit page writes one out beside every item. The name is how it finds the item, so renaming one gives it a new link, and the old one opens your menu instead. - A wallet you can change. Every button, link and QR code you have shared points at the short link, each item's own link included, and the short link points at the wallet. Change the address on the edit page and nothing you have printed or pasted breaks.
- A webhook without a long URL. Set "Also POST to this URL" once on the edit page instead of carrying
notify=in every link.
A URL-built button has none of these, because there is nowhere to keep an email address or a file. It still shows Paid ✓ when the wallet can confirm the payment, and it still fires notify=; it simply cannot email or deliver anything.
The edit link looks like theorangebutton.com/edit?secret=… and it is the whole account. We email it to you when you claim, so keep it somewhere safe. Lost it? Open theorangebutton.com/edit and type your link's name, the email you claimed with or your lightning address: the link goes to the email on file, and only there. A link claimed with no email has nowhere to be sent, so add one on the edit page while you have it.
The button on your own site
There are three ways to put the button on a page you control. The first is the HTML link above, which opens the pay page in the same tab. The other two keep the buyer on your page.
The pay page inside your page
An iframe that shows the pay card alone, without the rest of this site around it. The snippet for a claimed link is on your edit page under "More"; this is its shape:
<iframe src="https://theorangebutton.com/embed/?link=yourname" style="border:0;width:100%;max-width:420px;height:560px" title="Pay yourname with Bitcoin" allow="clipboard-write"></iframe>
An embed always shows one thing to pay for, never the menu — there is no room for a list in a frame this size. On a link that sells several, that one thing is the first on your list. To embed one of the others, add its name: /embed/?link=yourname&product=Flat%20white. Like the link, it reads the wallet and the price from your edit page.
For a URL-built button, put the same parameters after /embed/? in place of link=: https://theorangebutton.com/embed/?address=you@coinos.io&product=Ebook&amount=20. Add bg=f4f4f4 (a hex colour, no #) to paint the frame's background the colour of the panel it sits in; without it the frame is transparent.
The frame talks to your page with postMessage. It reports its height whenever the card changes, so the iframe can grow with the card instead of scrolling, and it says when the payment has settled:
// frame is your <iframe> element addEventListener('message', function (e) { if (e.source !== frame.contentWindow || !e.data) return; // only our frame if (e.data.type === 'ob:height') { frame.style.height = Math.min(Math.max(e.data.px | 0, 180), 1200) + 'px'; } if (e.data.type === 'ob:settled') { // { link, ref, amount_sats, product, pr, verify_url } console.log('paid', e.data.amount_sats, 'sats'); } });
Treat ob:settled as something to update your page with, not as proof. Proof is the webhook, which our server sends only after it has checked the payment with the wallet itself.
A button that opens the pay page in a small window
One script tag. It draws the orange button exactly where it sits, so it can go in a paragraph, a sidebar or a product card:
<script src="https://theorangebutton.com/embed.js" data-link="yourname"></script>
On a desktop the pay page opens in a small window over your page. On a phone, or when the browser blocks the window, it opens in a new tab instead, so the button always works. When the payment settles in the small window, the button's text changes to Paid ✓ and it fires an orangebutton:paid event, which bubbles, with the same detail as ob:settled. A payment made in a new tab pays the seller just the same, but your page is not told about it, so treat the event as a bonus rather than the record of a sale. The webhook below is the record.
document.addEventListener('orangebutton:paid', function (e) {
console.log(e.detail.link, e.detail.amount_sats, e.detail.ref);
});Selling several things? Add data-product beside data-link, and the button pays for that one thing at the price on your list. So a shop can put a button beside each thing it sells, and when it changes a price or its wallet, every button follows.
<script src="https://theorangebutton.com/embed.js" data-link="yourname" data-product="Flat white"></script>
A button built from the details instead — data-address, data-product and data-amount, with no link name — still reaches you. When a payment settles we look for a claimed link on that wallet address, and if the product and the price are on its list, it is recorded as a sale of that item — you get the email, the buyer gets what they bought, and it appears on your edit page. A price that is not on your list buys nothing: it is recorded at your price and flagged short, so somebody editing the number in the URL gets a payment and nothing else. But its wallet and its price are written into the button, so it keeps them: change your price and it keeps charging the old one, change your wallet and it keeps paying the old one, until you update it. A button built from data-link reads both from your link, which is why it is the one to put on a printed QR code.
For a URL-built button, give the script data-address, data-product, data-amount and, if you want them, data-currency and data-collect="email" instead of data-link. Either kind also takes data-ref and data-label (the words on the button; "⚡ Pay with Bitcoin" when left out). The script adds nothing else to your page: no styles, no fonts, no tracking.
How a payment is confirmed
The pay page asks your wallet for an invoice (LNURL-pay) and, when the wallet supports it, is handed a verify URL alongside it (LUD-21). It asks that URL every three seconds, for up to ten minutes, whether the invoice has settled. When the wallet says yes, the page shows Paid ✓ and tells our server.
The server does not take the page's word for it. Before anything is recorded, emailed or delivered it does four things:
- It checks that the verify URL belongs to the wallet behind your lightning address, and it follows no redirect off that host.
- It asks that URL itself and accepts only
settled: truefor that exact invoice. - It reads the amount out of the invoice, never out of the request.
- It keeps one record per invoice, so the same payment reported twice is answered as a duplicate and nothing happens twice.
Nobody can make a payment appear by pressing something on the page or by posting to our server. If the wallet does not say the invoice settled, nothing happened.
A wallet that offers no verify URL (Wallet of Satoshi is the common one) still receives the payment, but there is no way for anyone to confirm it, so the page never says Paid ✓ and our server never hears of it. The section on wallets says which ones confirm.
A payment by card is confirmed differently, because it never touches a wallet. A seller on the business plan connects their own Stripe account, and "Pay by card" on their pages opens Stripe's checkout on that account. Stripe tells our server the payment succeeded, in a signed message, and only then is the sale recorded; the page also reads the payment back from Stripe itself when the message is slow. Nothing a browser sends can create a card sale either.
The webhook
Give the button an https URL, as notify= on a URL-built button or under "Also POST to this URL" on your edit page, and our server POSTs one JSON message to it for every settled payment, after it has confirmed the payment itself as described above:
POST https://your-site.com/paid Content-Type: application/json Idempotency-Key: {sha256 of the invoice} { "event": "paid", "link": "yourname", // null for a URL-built button "product": "Ebook", "amount_sats": 28571, // read from the invoice "amount": 20, // the listed price, or null "currency": "USD", "buyer_name": "Sarah Jones", // null when not given "buyer_email": "sarah@example.com", // null when not given "ref": null, "underpaid": false, // true if it settled short of the price "pr": "lnbc…", "verify_url": "https://…", "settled_at": "2026-09-06T01:02:03Z" }
A payment by card sends the same message with three differences: method is "card", pr and verify_url are null (there is no invoice to re-check; Stripe's own signed event is the proof our server acted on), and payment_intent carries Stripe's id for the payment, with charged holding the amount, currency and Stripe's fee as charged on the seller's account. The Idempotency-Key is then a hash of that id. If a card payment is later refunded in full, one more message arrives with event set to "refunded" and the same payment_intent.
Answer with any 2xx. Delivery is at least once: a reply that is not 2xx is tried once more, and a payment reported to us twice can reach you twice, so dedupe on the Idempotency-Key header or on pr.
Re-check verify_url yourself before trusting the event. Fetch it, and act only when it answers settled: true for the same pr. Our server has already done this, but your server should not have to take our word for it any more than we take the browser's.
underpaid is true when the wallet confirmed less than 95% of the price the link asks. The sale is real, recorded and yours — the sats reached your wallet either way — but nothing is handed over: no file, no link, no message, and no delivery email to the buyer. Your own sale email says what was paid against what was asked, and settling it is between you and the buyer. It is always false on a URL-built button, which has no listed price to fall short of.
buyer_name and buyer_email are what the buyer typed. They are self-reported and never verified, so escape them before they touch a page, an email or a spreadsheet. The verification covers the payment, not the name.
The buyer's name and email
With collect=email on, the buyer is asked for a name (optional) and an email (required) before the invoice is made. They travel two ways:
- To your wallet, as the invoice comment (LUD-12), written
Name <email>. If the two together do not fit the wallet's comment limit, the email goes alone; if the wallet takes no comment, nothing goes. Most wallets show the comment next to the payment. - To the sale, on a claimed link, where they are recorded with the payment, put in your payment email and used to send the buyer what they bought. This happens whatever the wallet did with the comment.
Both are whatever the buyer typed. Nothing checks that the name is theirs or that the email is real, so treat them as display text: escape them, and do not act on them alone.
Wallets
Any Lightning wallet can pay a button, and any wallet that gives you a lightning address can receive from one. Two things depend on which wallet is receiving:
- Confirming payments (LUD-21 verify). Coinos, Alby, Blink and Speed do. Wallet of Satoshi does not. Without it there is no Paid ✓, no payment email, no delivery and no sales record. The sats still arrive.
- Taking a comment (LUD-12). Most wallets do, with a limit somewhere between a hundred and a few hundred characters. Without it the buyer's details do not appear in your wallet, though a claimed link still records them.
The maker and the edit page check the wallet behind the address you type and say which of these it does. The check reads a table our server keeps of the wallet services it has met; a service it has never met is probed once and the answer is kept for everyone. The answer is kept against the wallet service, not against you: what is stored is what that service can do.
Downloads
A file attached to a short link is handed out as theorangebutton.com/d/{token}, a private link made for that one sale. It appears on the pay page under Paid ✓ and in the buyer's email. It works for 30 days from the sale and is sent as a plain attachment, whatever the file type. The file itself sits at no address anyone can guess; there is nothing to find without a sale. Uploading a new file replaces the old one, and the link serves whatever file is attached at the time.
Non-custodial, and what follows from it
We never hold, route or touch any bitcoin. The pay page resolves your lightning address, asks your wallet for an invoice and shows it to the buyer; the sats go from their wallet to yours. Our server's only part comes afterwards, when it asks your wallet whether an invoice it never issued has settled. There is no balance here, nothing to withdraw and nothing that can be frozen.
It follows that there are no refunds, disputes or buyer accounts on this site. A refund is a payment you make back to the buyer from your own wallet, and a buyer with a question reaches you wherever they found your button.
Card payments are the one exception, and they are the seller's own. A seller on the business plan or the venue plan connects their own Stripe account; a card payment is charged on that account, by Stripe, and lands there. We never hold it and take no percentage of it. Refunds and disputes on card payments are between the seller, the buyer and Stripe, in the seller's own Stripe dashboard; when one is refunded in full, the sale comes off the seller's records here.