> Documentation index: https://docs.sellhidden.com/llms.txt

# Buy button and live price

Source: https://docs.sellhidden.com/buy-button

Include one script and give any element your store and product: clicking it opens your checkout on your own page. A price span keeps itself current, and your page gets an event for every order.

## Buy button

A live Buy button demo is on the HTML page.

Give any element `data-hidden-store` and `data-hidden-product`, include `embed.js` once, and clicking opens a checkout modal with your live product page. The buyer never leaves the host page; the modal closes with its X, Escape, a click outside, or from checkout once the order is placed.

```html
<button data-hidden-store="my-store" data-hidden-product="lifetime-license">Buy now</button>
<script src="https://sellhidden.com/embed.js" async></script>
```

The script sets no cookies, reads nothing on the host page and takes the checkout host from its own `src`, so a copied snippet cannot be redirected. Messages from the modal are accepted only from that origin. The v1 snippet (`<script data-store data-product>`) keeps working and now opens the modal too.

## Live price

A price span keeps itself current from the public API (cached 60 seconds). Optional attributes: `data-hidden-price-prefix`, `data-hidden-currency-symbol` and `data-hidden-format="raw"`. Once loaded the span gets `data-hidden-in-stock="true|false"`.

```html
<span data-hidden-price="my-store/lifetime-license" data-hidden-price-prefix="from "></span>
```

## JavaScript API

Open and close the modal from your own code, or rebind after rendering new elements in a single-page app:

```js
window.HiddenEmbed.open("my-store", "lifetime-license"); // open the checkout modal
window.HiddenEmbed.open("my-store", "lifetime-license", {  // same, prefilled
  email: "buyer@example.com", variant: "Team", quantity: 2, ref: "user_42", meta: { discord_name: "neo" },
});
window.HiddenEmbed.close();                                // close it
window.HiddenEmbed.scan();                                 // bind elements added after load
```

Prefer your own markup? Fetch the product and render the price yourself:

```js
const res = await fetch("https://api.sellhidden.com/v1/public/products/my-store/lifetime-license");
const { ok, product } = await res.json();
if (ok) priceEl.textContent = `$${product.displayPriceUsd.toFixed(2)}`;
```

## Prefill

Fill in what you already know about the buyer. The attributes are read when the button is clicked, so you can set them after your page loads (for example once the visitor signs in). The buyer can still change everything, and the checkout checks every value again.

```html
<button data-hidden-store="my-store" data-hidden-product="lifetime-license"
  data-hidden-email="buyer@example.com"
  data-hidden-coupon="SPRING"
  data-hidden-variant="Team"
  data-hidden-quantity="2"
  data-hidden-ref="user_42"
  data-hidden-meta-discord_name="neo">Buy now</button>
```

| Attribute | Type | Description |
| --- | --- | --- |
| `data-hidden-email` | email | The buyer's email, where the order link and receipt go. Filled in only when the script tag carries a valid site key for your page and the buyer's browser has no remembered email; otherwise it is offered as a suggestion the buyer accepts. |
| `data-hidden-coupon` | string | A coupon code, applied at checkout as if the buyer typed it. A code that does not apply shows its reason. |
| `data-hidden-variant` | string | The variation to preselect: its id, or its exact name. |
| `data-hidden-quantity` | 1-100 | The quantity to preselect (never more than is in stock). |
| `data-hidden-ref` | string | Your own reference, up to 64 of A-Z a-z 0-9 _ . : @ + / = -. Saved on the order as customFields.ref (webhooks, API, dashboard) and returned in every embed event. |
| `data-hidden-meta-<key>` | string | Fills your checkout field with that key (lowercase letters, digits and _). data-hidden-meta-discord_user_id fills the Discord user ID of Discord products (with a valid site key; otherwise it is offered as a suggestion). |

The same values work on a direct link as query parameters: `/p/lifetime-license?buy=1&email=buyer@example.com&coupon=SPRING&meta.discord_name=neo`.

## Checkout events

The checkout modal sends hidden:success, hidden:error and hidden:close to your page.

`embed.js` tells your page what happens in the modal. Listen for the `hidden:success`, `hidden:error` and `hidden:close` events on `window` (works even before the script loads), or register callbacks with `HiddenEmbed.on("success", fn)` once it has. Every `detail` carries `store`, `product` and your `ref`.

| Event | Detail | Sent when |
| --- | --- | --- |
| `hidden:success` | orderId, status | The order is paid. status is paid, delivered, oversold or failed (paid, delivery needs attention). Once per order. orderId is null unless your page holds a site key. |
| `hidden:error` | code, message, status | Something stopped the purchase: checkout_failed (the order could not be created; message says why), order_expired, order_cancelled or sold_out. |
| `hidden:close` | completed, status, orderId | The modal closed, however it was closed. completed is true when an order was paid in it. Once per modal. |

```html
<!-- Listeners work before or after embed.js loads -->
<script>
  window.addEventListener("hidden:success", (e) => {
    const { orderId, status, store, product, ref } = e.detail;
    // status: paid | delivered | oversold | failed. orderId is null without a site key.
    showThankYou(ref);
  });
  window.addEventListener("hidden:error", (e) => {
    console.warn(e.detail.code, e.detail.message); // checkout_failed, order_expired, ...
  });
  window.addEventListener("hidden:close", (e) => {
    if (!e.detail.completed) offerHelp(); // closed before paying
  });
  // After embed.js loaded, the same as callbacks:
  // window.HiddenEmbed.on("success", (detail) => { ... });
</script>
<script src="https://sellhidden.com/embed.js" data-hidden-key="hsk_your_site_key" async></script>
```

> **The order id needs a site key.** An order id opens the order page and its delivered items, so the modal only hands it to a site you named: create a site key for your website address under **Developers, Embeds** and add it to the script tag as `data-hidden-key`. The modal learns your page's origin from the browser, checks the key against it and then posts to that exact origin only. On any other page `orderId` is `null`. After removing a team member or giving up a domain, the store owner can revoke every site key at once in the same place; your sites keep selling and get the order id again once they have a new key. Treat the event as a signal for your interface; fulfil from the signed `order.paid` webhook (match it with `customFields.ref`).

## Your own iframe

Framing the checkout yourself instead of using embed.js? Answer the modal's `hidden:ready` message with `hidden:hello` and you receive the same messages:

```js
// Your own <iframe src=".../p/lifetime-license?embed=1"> instead of embed.js:
window.addEventListener("message", (e) => {
  if (e.origin !== "https://my-store.sellhidden.com") return;
  const msg = e.data;
  if (msg && msg.type === "hidden:ready") {
    // Answer every page load in the frame; the browser tells it your origin.
    e.source.postMessage({ type: "hidden:hello", v: 2, key: "hsk_your_site_key" }, e.origin);
  }
  if (msg && msg.type === "hidden:success") console.log(msg.status, msg.orderId);
  if (msg && msg.type === "hidden:close") iframe.remove();
});
```

[Open the configurator](https://sellhidden.com/embed)
