The buy button
The buy button is a few lines of HTML you paste on any page. No framework, no install step on your end. The button renders on first paint; the provider picker and the payment-provider SDKs load on demand the first time a buyer clicks. It works in any site or framework that renders HTML, including the no-code site builders that let you paste HTML embeds.
Confirmed working with: Angular, Hugo, Jekyll, plain HTML, Webflow (Custom Code embed), Carrd (Embed element), Framer (HTML embed), Squarespace (Code Block), WordPress (Custom HTML block), Ghost, Notion sites (HTML embed widgets). If your site lets you paste HTML on a page, the buy button works there.
Attribute reference
| Attribute | Required | What it accepts | Notes |
|---|---|---|---|
endpoint | No | URL string | Almost always omit this. The button auto-detects its API base: https://api.coinmoebius.com in production, http://localhost:8787 on localhost. Only set it as an override for an unusual setup (e.g., a self-hosted proxy in front of the API). |
project-id | Yes | String, prefixed proj_ | From your project page in the dashboard. Safe to expose publicly, it's an identifier, not a credential. |
product-id | Yes (in strict mode) / Recommended (in ad-hoc mode) | String, any format you choose | Your internal product identifier. In strict mode (the default), this is how the worker looks up the price from your catalog. In ad-hoc mode, it's still passed through to your webhooks in metadata.productId so you can map transactions back to inventory. See the Strict mode vs ad-hoc mode section below. |
amount | Only in ad-hoc mode | Decimal number as string | The price in major units (e.g., dollars, not cents). Must be positive. Omit this in strict mode. The worker reads the canonical price from your catalog and ignores any value in the HTML. Required only when the project is in ad-hoc mode and the product-id is not in the catalog. |
currency | Only in ad-hoc mode | Three-letter ISO 4217 code or your own unit name | USD, EUR, etc. for cards / crypto rails. The pay-by-mail provider accepts any string (e.g., GBK) because the merchant is the one settling the transaction. Same rule as amount: omit in strict mode, required in ad-hoc mode for products not in the catalog. |
label | No | String | The button text. Defaults to Buy. Use this to distinguish buttons on the same page. |
customer-ref | No | String, any format you choose | An opaque identifier for the buyer in your own system, like a logged-in user id. The button forwards it to the worker as metadata.customerRef, and the worker tags the transaction with it. Later you can ask "which of my users paid?" using your own id, without us holding any actual customer data. Omit for anonymous checkouts. |
buyer-email | No | Email address | Where to send a digital download. Set this when your site already knows the buyer, like a members area or a logged-in app, and the button will not ask them for an email. When it is not set, the button asks the buyer for one at checkout if the product is a download on a provider that does not return the email itself. We use the address only to send the download and then delete it. We never store it. |
theme | No | dark or light | Picks the built-in color scheme for the button and popup together. Defaults to dark. Set theme="light" for the light scheme. Your CSS variables override either one. See the styling guide. |
editable-amount | No | true or false | When true, the popup shows an amount field the buyer fills in (tips, donations, pay-what-you-want). Defaults to false, where the amount you set is fixed and no field appears. The payment buttons stay disabled until the amount is a positive number. |
disabled | No | Attribute present/absent, or the el.disabled property | The standard button contract: while present, the button is dimmed and ignores clicks. Set it in your HTML, or flip the property from code the way you would on a native button. Separate from this, the button always locks itself while a click is processing so a double-click can't start two checkouts; that internal lock never touches a value you set here. |
spinner | No | true or false | The built-in loading spinner shown on the button while checkout starts. On by default. Set spinner="false" to turn it off and drive your own loading UI from the cm-busy and cm-idle events. Turning it off never turns off the double-click protection. The spinner draws in your button's own text color and size, so it matches your styling automatically; restyle it with ::part(spinner) or replace it entirely with <span slot="spinner">your markup</span>. |
One payment method? No popup.
When a product has exactly one payment method enabled and a fixed price, the button skips the picker: the buyer clicks and goes straight into that method's flow. Hosted checkouts navigate immediately with no popup at all, while pay-by-mail, in-page coins, and download-email prompts still open the popup at their own first step. Products with two or more methods, and buyer-priced products (tips, donations), always show the picker. There is nothing to configure; the button decides from your product's own settings, per product, so a two-method product and a one-method product on the same page each do the right thing. If a checkout fails on the direct path, a short error line appears under the button (styleable via ::part(error)).
Multiple buttons on one page
The script registers the button once, then any number of instances can render on a page. Each instance is independent. The example below uses strict mode (the default): no amount or currency in the HTML, because the worker looks the price up from each product in your catalog.
<coin-moebius-buy
project-id="proj_YOUR_ID"
product-id="t-shirt-medium"
label="T-shirt (medium)">
</coin-moebius-buy>
<coin-moebius-buy
project-id="proj_YOUR_ID"
product-id="mug-blue"
label="Blue mug">
</coin-moebius-buy>If your project is in ad-hoc mode (donation widgets, tip jars), each button also includes amount and currency. See Strict mode vs ad-hoc mode for the full picture.
Theming the button
The buy button renders inside a Shadow DOM, which means none of your page's existing CSS reaches into it. That's deliberate: it keeps the button looking the same on every site, and stops a stray * { box-sizing: ... } rule from breaking the picker modal. You style it through two surfaces from your own stylesheet: CSS custom properties for colors, fonts, and shape, and ::part() selectors for everything else. No JavaScript involved.
Every variable and part is documented on the styling guide, with live examples you can click and copy:
Open the interactive styling guide →
The success and cancel URLs
When you connect Stripe or NOWPayments in the dashboard's Providers tab, you set two URLs:
- Success URL, where the buyer lands after a successful payment. Stripe and NOWPayments append a query parameter (
?session_id=...for Stripe,?NP_id=...for NOWPayments) so your success page can identify which transaction completed. Most static sites just show a generic "Thanks, your payment is processing" message and rely on the dashboard for the source of truth. - Cancel URL, where the buyer lands if they back out before paying. Often the same as your cart page.
Both URLs are configured per-provider in the dashboard, the element doesn't need to know about them.