> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shoppex.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Embed SDK reference

> Complete reference for the Shoppex Checkout Embed SDK — script and versioning, snippet parameters, the JavaScript API, every public event, and the exact CSP directives your site needs

This page documents the current behavior of the Checkout Embed SDK (`window.Shoppex`): how the script is served, every snippet parameter, the full JavaScript API, every event the embed emits, the Content Security Policy your site needs, and the security model.

Everything here is derived from the shipped SDK. Where a parameter exists but does nothing on a given path, this page says so instead of listing it as supported.

## The script

```html theme={"system"}
<script src="https://checkout.shoppex.io/embed/embed.iife.js" defer></script>
```

The script defines one global, `window.Shoppex`, and initializes itself on `DOMContentLoaded`. You do not need to call `Shoppex.init()` unless you pass configuration.

### Versioning and integrity

| Property         | Current behavior                                                                                                                 |
| ---------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| URL              | One path, `/embed/embed.iife.js`. No version segment.                                                                            |
| Mutability       | Mutable. Each checkout deploy replaces the file at the same URL.                                                                 |
| Caching          | `Cache-Control: public, max-age=0`. Browsers revalidate on every page load, so a fix reaches your buyers on their next visit.    |
| CORS             | The response carries no `Access-Control-Allow-Origin` header.                                                                    |
| Protocol version | The `postMessage` protocol is versioned separately and is currently `"1"`. Every message the embed sends carries `version: "1"`. |

<Warning>
  **Do not add an `integrity` attribute to the embed script tag today.** Subresource Integrity on a cross-origin script also requires `crossorigin="anonymous"`, and the browser then requires a CORS response header that this URL does not send. The script would be blocked outright, and your checkout button would stop working. A pinned hash would also break at the next checkout deploy, because the URL is mutable by design.
</Warning>

If you audit third-party scripts, take the digest as a record rather than an enforcement mechanism:

```bash theme={"system"}
curl -sS https://checkout.shoppex.io/embed/embed.iife.js \
  | openssl dgst -sha384 -binary \
  | openssl base64 -A
```

Immutable versioned delivery, which is what makes SRI pinning meaningful, is tracked in [Future work](#future-work).

### Self-hosting

Do not copy the script to your own domain. It must be served from the same origin as the checkout iframe: the SDK validates every incoming message against the checkout origin, and a self-hosted copy pointed at a different origin will silently drop every event.

## Integration modes

<Tabs>
  <Tab title="Declarative (data attributes)">
    The SDK binds click handlers to any element matching one of these selectors, including elements added to the DOM later (a `MutationObserver` rebinds automatically):

    * `[data-shoppex-product-id]`
    * `[data-shoppex-group-id]`
    * `[data-shoppex-checkout]`
    * `[data-shoppex-add-to-cart]`
    * `[data-shoppex-cart-toggle]`
    * `[data-shoppex-cart-checkout]`
    * `[data-shoppex-widget]`

    Single product:

    ```html theme={"system"}
    <button
      data-shoppex-shop-id="YOUR_SHOP"
      data-shoppex-product-id="PRODUCT_ID"
      data-shoppex-variant-id="VARIANT_ID"
      data-shoppex-quantity="1"
      data-shoppex-theme="auto"
    >
      Buy now
    </button>
    ```

    Multiple items in one checkout:

    ```html theme={"system"}
    <button
      data-shoppex-checkout="1"
      data-shoppex-shop-id="YOUR_SHOP"
      data-shoppex-items='[
        {"productId":"PRODUCT_ID_1","variantId":"VARIANT_1","quantity":1},
        {"productId":"PRODUCT_ID_2","quantity":2}
      ]'
    >
      Checkout
    </button>
    ```

    Product group:

    ```html theme={"system"}
    <button
      data-shoppex-group-id="GROUP_ID"
      data-shoppex-affiliate-code="CREATOR123"
      data-shoppex-return-url="https://your-site.com/thank-you"
    >
      Open group
    </button>
    ```
  </Tab>

  <Tab title="Programmatic (JS API)">
    ```javascript theme={"system"}
    Shoppex.open({
      shopId: 'YOUR_SHOP',
      items: [
        { productId: 'PRODUCT_ID', variantId: 'VARIANT_ID', quantity: 1 }
      ],
      theme: 'auto',
      locale: 'en',
      email: 'customer@example.com',
      couponCode: 'SAVE20',
      affiliateCode: 'CREATOR123',
      returnUrl: 'https://your-site.com/thank-you'
    });
    ```
  </Tab>

  <Tab title="Cart (add to cart, then check out)">
    ```html theme={"system"}
    <button
      data-shoppex-add-to-cart
      data-shoppex-shop-id="YOUR_SHOP"
      data-shoppex-product-id="PRODUCT_ID"
      data-shoppex-quantity="1"
    >
      Add to cart
    </button>

    <button data-shoppex-cart-toggle data-shoppex-shop-id="YOUR_SHOP">
      Open cart
    </button>

    <button data-shoppex-cart-checkout data-shoppex-shop-id="YOUR_SHOP">
      Checkout
    </button>
    ```

    The cart lives in `localStorage` under `shoppex_embed_cart_<shopId>` and survives reloads. Carts are scoped per shop, so two shops embedded on one page keep separate carts.
  </Tab>
</Tabs>

## Snippet parameters

Every attribute below has a matching `Shoppex.open()` option, listed in the last column.

### Identity

| Attribute                 | Option              | Notes                                                                                  |
| ------------------------- | ------------------- | -------------------------------------------------------------------------------------- |
| `data-shoppex-shop-id`    | `shopId`            | Shop slug or UUID. Required for cart and multi-item checkout.                          |
| `data-shoppex-product-id` | `items[].productId` | Required unless you pass a group or `data-shoppex-items`.                              |
| `data-shoppex-group-id`   | `groupId`           | Opens the group picker, then continues into product checkout.                          |
| `data-shoppex-items`      | `items`             | JSON array of item objects. Adds to any single-product attributes on the same element. |

### Per item

| Attribute                            | Option                         | Notes                                                                                                                                                                                                                                                                                                                               |
| ------------------------------------ | ------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data-shoppex-variant-id`            | `items[].variantId`            | Product price variant ID. Used when `priceVariantId` is not set.                                                                                                                                                                                                                                                                    |
| `data-shoppex-price-variant-id`      | `items[].priceVariantId`       | Product price variant ID. Takes precedence over `variantId` when both are set, on single-product and cart checkout.                                                                                                                                                                                                                 |
| `data-shoppex-quantity`              | `items[].quantity`             | Integer, clamped to `1..999999`. Invalid values fall back to `1`.                                                                                                                                                                                                                                                                   |
| `data-shoppex-product-title`         | `items[].title`                | Display only, used in the cart modal.                                                                                                                                                                                                                                                                                               |
| `data-shoppex-variant-title`         | `items[].variantTitle`         | Display only.                                                                                                                                                                                                                                                                                                                       |
| `data-shoppex-product-image-url`     | `items[].imageUrl`             | Display only.                                                                                                                                                                                                                                                                                                                       |
| `data-shoppex-custom-fields`         | `items[].customFields`         | JSON object of string values, prefills the product's custom fields. A key that names one of the product's fields answers it for the buyer, so the field is not shown — unless the value fails the field's validation, in which case it stays on screen to be corrected. Keys that match no field are still recorded on the invoice. |
| `data-shoppex-delivery-instructions` | `items[].deliveryInstructions` |                                                                                                                                                                                                                                                                                                                                     |
| `data-shoppex-addons`                | `items[].addons`               | JSON array of `{"id":"…","quantity":1}`. **Cart checkout only** — ignored with a console warning on a single-product button, because the single-product checkout contract has no add-on field.                                                                                                                                      |

### Checkout behavior

| Attribute                       | Option           | Notes                                                                                                                                                                                                                |
| ------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `data-shoppex-theme`            | `theme`          | `light`, `dark`, or `auto` (default). `auto` follows the shop's configured mode, then the visitor's `prefers-color-scheme`.                                                                                          |
| `data-shoppex-locale`           | `locale`         | BCP 47 tag. Defaults to `navigator.language`.                                                                                                                                                                        |
| `data-shoppex-flow`             | `flow`           | `embed` (default) or `hosted_after_details`, which collects details in the modal and finishes on hosted checkout.                                                                                                    |
| `data-shoppex-embed-max-height` | `embedMaxHeight` | CSS length for the modal shell, for example `720`, `85vh`, or `min(90dvh, 800px)`. Viewport-relative terms (`vh`, `dvh`, `%`) are measured against your page's viewport; pixel terms cap the shell inside the modal. |
| `data-shoppex-return-url`       | `returnUrl`      | Where the buyer goes after purchase. See [Return URL safety](#return-url-safety).                                                                                                                                    |
| `data-shoppex-email`            | `email`          | Prefills the buyer email field.                                                                                                                                                                                      |
| `data-shoppex-coupon-code`      | `couponCode`     | Prefills a coupon.                                                                                                                                                                                                   |
| `data-shoppex-affiliate-code`   | `affiliateCode`  | Affiliate or creator attribution.                                                                                                                                                                                    |
| `data-shoppex-referral-code`    | `referralCode`   | Alias for the above. `affiliateCode` wins when both are set.                                                                                                                                                         |
| `data-shoppex-gateway`          | `gateway`        | Preselects a payment method, for example `stripe`. See the warning below.                                                                                                                                            |
| `data-shoppex-metadata`         | `metadata`       | JSON object of string values. See [Metadata](#metadata).                                                                                                                                                             |

### Rewards

| Attribute                              | Option                  | Values                                                                 |
| -------------------------------------- | ----------------------- | ---------------------------------------------------------------------- |
| `data-shoppex-show-rewards-summary`    | `showRewardsSummary`    | `true`, `1`, `yes`, `false`, `0`, `no`. A bare attribute means `true`. |
| `data-shoppex-allow-reward-redemption` | `allowRewardRedemption` | Same values.                                                           |
| `data-shoppex-rewards-mode`            | `rewardsMode`           | `summary`, `activity`, `full`, `hidden`.                               |

### Prebuilt widgets

`data-shoppex-widget` renders a small UI inside a shadow root on the host element, so you do not have to write button markup.

| Attribute                    | Values                                                                    |
| ---------------------------- | ------------------------------------------------------------------------- |
| `data-shoppex-widget`        | `buy-button`, `product-card`, `cart-launcher`                             |
| `data-shoppex-widget-label`  | Button label. Defaults to `Buy now`, or `Open cart` for `cart-launcher`.  |
| `data-shoppex-product-price` | Price text shown on `product-card`. Display only, not a price you charge. |

```html theme={"system"}
<div
  data-shoppex-widget="product-card"
  data-shoppex-shop-id="YOUR_SHOP"
  data-shoppex-product-id="PRODUCT_ID"
  data-shoppex-product-title="Pro License"
  data-shoppex-product-price="$49"
  data-shoppex-product-image-url="https://cdn.example.com/pro.png"
></div>
```

<Note>
  Widget styles are injected into the widget's shadow root without a CSP nonce. Under a strict `style-src` that has no `'unsafe-inline'`, the widget renders unstyled. If you enforce a strict style policy, write your own markup and put `data-shoppex-product-id` on your own button instead.
</Note>

<Warning>
  `data-shoppex-gateway` is an instruction with a price consequence, not a hint. A preselected gateway clears the "buyer must pick a method" step, which commits the invoice to that method **and its merchant-configured fee** without the buyer clicking anything. A key your shop does not offer is ignored, and the buyer picks as usual. On an add-to-cart button the attribute does nothing — put it on the `data-shoppex-cart-checkout` button.
</Warning>

### Metadata

`metadata` lands on the invoice as `custom_fields` on both checkout paths. How it travels differs, but the result is the same:

| Checkout path                                                                                                         | How `metadata` travels                                                                                                           |
| --------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Cart or multi-item (`data-shoppex-cart-checkout`, `data-shoppex-items` with more than one item, `Shoppex.checkout()`) | Posted as the invoice's `custom_fields` when the invoice is created.                                                             |
| Single product (one item, or a group that resolves to one product)                                                    | Appended to the checkout URL as `metadata[key]=value`, read by the checkout page, and merged into the invoice's `custom_fields`. |

Either way it is readable on the invoice and through the Developer API.

```html theme={"system"}
<button
  data-shoppex-product-id="PRODUCT_ID"
  data-shoppex-metadata='{"campaign":"summer","source":"landing"}'
>
  Buy now
</button>
```

`data-shoppex-custom-fields` reaches the same place and stays the right attribute for per-item values on a single product or on one line of `data-shoppex-items`. `data-shoppex-metadata` is button-wide. If both set the same key on a single-product checkout, the `data-shoppex-custom-fields` value wins, because it is the more specific of the two.

Only `data-shoppex-custom-fields` can answer a field for the buyer. `data-shoppex-metadata` never hides a field and is never submitted as a buyer's answer: if a metadata key happens to have the same name as one of the product's own custom fields, the field is still shown, the buyer's answer is what lands on the invoice under that key, and the metadata value is dropped with a console warning. Prefill the field with `data-shoppex-custom-fields`, or rename the metadata key, if you meant to set it.

Some keys are reserved for Shoppex invoice state and are refused, whether they arrive through `data-shoppex-custom-fields` or `data-shoppex-metadata`: `license_uid`, `custom_fields`, `delivery_instructions`, `custom_field_definitions`, `payment_method_terms_accepted`, `payment_method_terms_accepted_at`, `discord_integration`, and anything starting with `billing_`, `payment_link_`, `payment_method_`, `affiliate_`, `reseller_`, `supplier_`, or `_import`. `data-shoppex-custom-fields` keys are refused twice — the SDK keeps them out of the checkout URL, and the checkout drops them again on arrival, because that URL is buyer-editable and shareable. Rejected keys log a console warning. `source` and `campaign` are explicitly merchant-owned and always allowed.

## JavaScript API

### Checkout

#### `Shoppex.init(config?)`

Binds elements and keyboard handlers. Called automatically on load, so you only need it to pass configuration. Safe to call repeatedly; later calls merge into the existing config.

```javascript theme={"system"}
Shoppex.init({
  shopId: 'YOUR_SHOP',
  locale: 'en',
  nonce: 'YOUR_CSP_NONCE',
  checkoutBaseUrl: 'https://checkout.shoppex.io',
  apiBaseUrl: 'https://api.shoppex.io'
});
```

`shopId` and `locale` act as defaults for every call. `nonce` is required under a strict `style-src` — see [Content Security Policy](#content-security-policy). `checkoutBaseUrl` and `apiBaseUrl` exist for local development and are validated as `http(s)`; an invalid value logs a warning and falls back to production.

#### `Shoppex.open(options)`

Opens the checkout modal. Needs at least one item with a non-empty `productId`, or a `groupId`; otherwise it logs a warning and does nothing. Opening while a modal is already open closes the previous one first.

More than one item routes through cart checkout, which creates the invoice before the iframe opens and therefore requires `shopId`.

#### `Shoppex.close()`

Closes the modal. Emits `shoppex:close`.

### Cart

Every cart method takes an optional `{ shopId }` and returns the resulting item array. Each one also emits `shoppex:cart-change`.

| Method                                                             | Description                                                                |
| ------------------------------------------------------------------ | -------------------------------------------------------------------------- |
| `Shoppex.addItem(item, options?)`                                  | Adds an item, or increases the quantity of a matching line.                |
| `Shoppex.setItem(item, options?)`                                  | Sets a line to an exact quantity.                                          |
| `Shoppex.updateQuantity(productId, variantId, quantity, options?)` | Sets the quantity of one line.                                             |
| `Shoppex.removeItem(productId, variantId?, options?)`              | Removes one line.                                                          |
| `Shoppex.clearCart(options?)`                                      | Empties the cart.                                                          |
| `Shoppex.getItems(options?)`                                       | Returns the current items.                                                 |
| `Shoppex.getItemCount(options?)`                                   | Returns the summed quantity.                                               |
| `Shoppex.openCart(options?)`                                       | Opens the cart modal.                                                      |
| `Shoppex.checkout(options?)`                                       | Checks out the stored cart. Warns and does nothing when the cart is empty. |

```javascript theme={"system"}
Shoppex.addItem({ productId: 'PRODUCT_ID', quantity: 2 }, { shopId: 'YOUR_SHOP' });

document.addEventListener('shoppex:cart-change', (event) => {
  document.querySelector('#cart-count').textContent = event.detail.totalQuantity;
});
```

## Events

Every public event is a `CustomEvent` dispatched on `document`. They bubble and cross shadow boundaries, so one listener on `document` sees all of them.

```javascript theme={"system"}
document.addEventListener('shoppex:success', (event) => {
  analytics.track('purchase', { invoiceId: event.detail.invoiceId });
});
```

### Checkout lifecycle

| Event                     | Detail                                                   | When                                                                                                                                                                                                                                                                                                                           |
| ------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `shoppex:ready`           | `{}`                                                     | The checkout iframe finished mounting and is visible. Fires again after each in-modal navigation step, so treat it as "a step is ready", not "the checkout started".                                                                                                                                                           |
| `shoppex:invoice-created` | `{ invoiceId?: string }`                                 | An invoice exists. The buyer has not paid yet.                                                                                                                                                                                                                                                                                 |
| `shoppex:redirect`        | `{ url: string }`                                        | The buyer is being sent to a payment provider, either in a popup or in the top window.                                                                                                                                                                                                                                         |
| `shoppex:success`         | `{ invoiceId?: string, completionAccessGrant?: string }` | The invoice is paid. This is the moment to fire a conversion. `completionAccessGrant` is a short-lived token that lets your return page read the completed order.                                                                                                                                                              |
| `shoppex:error`           | `{ error?: string, source: 'iframe' \| 'embed' }`        | Checkout failed. Covers both failures reported by the checkout iframe (`source: 'iframe'`) and a cart checkout that could not be started at all (`source: 'embed'`, which has no iframe to report it). Read `source` only if you also read the raw postMessage traffic below — an `iframe` error reaches you on both channels. |
| `shoppex:close`           | `{ completed: boolean, invoiceId?: string }`             | The modal closed, whatever closed it: the close button, `Escape`, the backdrop, `Shoppex.close()`, or the checkout itself. `completed` is `false` when the buyer left without paying, which is the signal for abandonment tracking. Fires once per checkout.                                                                   |

Abandonment tracking, end to end:

```javascript theme={"system"}
document.addEventListener('shoppex:close', (event) => {
  if (event.detail.completed) return;
  analytics.track('checkout_abandoned');
});
```

### Cart

| Event                 | Detail                                                              |
| --------------------- | ------------------------------------------------------------------- |
| `shoppex:cart-change` | `{ shopId?: string, items: CheckoutItem[], totalQuantity: number }` |

### Rewards

| Event                     | Detail                                                    | Status                                                                                           |
| ------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------ |
| `shoppex:rewards-updated` | `{ invoiceId?: string, rewards: RewardsPayload \| null }` | Emitted when the checkout recalculates rewards.                                                  |
| `shoppex:rewards-applied` | `{ invoiceId?: string, rewards: RewardsPayload \| null }` | Accepted by the SDK, not emitted by the checkout yet. Safe to subscribe to; do not depend on it. |
| `shoppex:rewards-error`   | `{ invoiceId?: string, error?: string }`                  | Accepted by the SDK, not emitted by the checkout yet.                                            |

`rewards` carries `summary`, `activity`, `earned_after_invoice`, and `pending_after_invoice`.

### Locale

| Event               | Detail                                                                     |
| ------------------- | -------------------------------------------------------------------------- |
| `shoppex:setLocale` | `{ locale: string }` — the buyer changed the language inside the checkout. |

### Diagnostics

| Event                         | Detail                                                                                                                                                                                                  |
| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `shoppex:embed-load-fallback` | `{ reason, checkoutOrigin, checkoutPath, elapsedMs }` — the iframe finished loading without sending `shoppex:ready`. Useful when debugging a blocked or broken embed; also logged as a console warning. |

### The underlying postMessage protocol

You normally never touch this: the SDK validates the sender and re-dispatches everything as the document events above. It is documented so you can recognise the traffic in a debugger.

Messages are `{ version: "1", type, payload }` and travel from the checkout iframe to your page, addressed to your exact origin when the checkout can determine it. Message types: `shoppex:ready`, `shoppex:resize`, `shoppex:close`, `shoppex:success`, `shoppex:error`, `shoppex:invoice-created`, `shoppex:redirect`, `shoppex:external-redirect`, `shoppex:rewards-updated`, `shoppex:rewards-applied`, `shoppex:rewards-error`, `shoppex:setLocale`, and `shoppex:style-update` (used by the dashboard Style Center preview, never by a live embed).

`shoppex:resize` and `shoppex:style-update` are consumed by the SDK to size and theme the modal, and have no document event. `shoppex:external-redirect` reaches your page as `shoppex:redirect`.

## Content Security Policy

If your site sends a `Content-Security-Policy` header, the embed needs these directives. Each one is required by a specific thing the SDK does on your page.

| Directive     | Value                                     | Why                                                                                                                                   |
| ------------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `script-src`  | `https://checkout.shoppex.io`             | Loads `embed.iife.js`.                                                                                                                |
| `frame-src`   | `https://checkout.shoppex.io`             | The checkout iframe (`/e/…`, `/g/…`, `/invoice/…`).                                                                                   |
| `connect-src` | `https://api.shoppex.io`                  | The SDK calls the API from **your** page: it fetches your checkout styling, and creates the invoice for cart and multi-item checkout. |
| `style-src`   | `'nonce-YOUR_NONCE'` or `'unsafe-inline'` | The SDK appends style elements to its shadow root. A nonce is cleaner — pass the same nonce to `Shoppex.init({ nonce })`.             |
| `img-src`     | The hosts serving your product images     | Only if you use the cart modal or the `product-card` widget, which render `data-shoppex-product-image-url`.                           |

A complete working policy:

```http theme={"system"}
Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-YOUR_NONCE' https://checkout.shoppex.io;
  frame-src https://checkout.shoppex.io;
  connect-src 'self' https://api.shoppex.io;
  style-src 'self' 'nonce-YOUR_NONCE';
  img-src 'self' data: https://cdn.example.com;
```

```javascript theme={"system"}
Shoppex.init({ nonce: 'YOUR_NONCE' });
```

Notes that save an afternoon of debugging:

* **`default-src` does not cover `frame-src` on its own** in every browser configuration you will meet. Name `frame-src` explicitly.
* **Omitting `connect-src` looks like a styling bug.** Your checkout styling silently falls back to defaults, and cart checkout fails with a generic error, because both are `fetch` calls from your page.
* **Omitting `style-src`** leaves the modal open but unstyled.
* **Payment provider popups are windows, not frames.** They are not covered by `frame-src`, and they need no CSP entry.
* If you override `checkoutBaseUrl` or `apiBaseUrl` for a staging environment, use those origins in the policy instead.
* Test the policy on the production domain. A local dev server usually sends no CSP at all, so violations only appear once you deploy.

### Referrer policy

The checkout addresses its messages to your exact origin, which it learns from the referrer of the iframe request. Under `Referrer-Policy: no-referrer` it cannot, and falls back to a wildcard target origin. The SDK still validates the sender, so the embed keeps working, but the browser's default `strict-origin-when-cross-origin` gives you the tighter behavior. Keep it.

## Security model

<Note>
  The SDK validates the origin of every incoming message before acting on it, and — once the iframe window is known — that the message came from that window. A forged `postMessage` from another origin or another frame is ignored.
</Note>

Trusted origins are `https://checkout.shoppex.io` plus the local checkout development origins. Once a modal is open, the SDK pins to that iframe's exact origin.

Redirects are checked by kind, because they carry different trust:

* **In-checkout redirects** must stay on the checkout origin.
* **External payment redirects** must be `http(s)` and open in a popup, never as a top-window navigation. They intentionally allow arbitrary hosts, because a custom manual gateway legitimately redirects to its own payment host. The trust boundary is the verified iframe origin that sent the URL.
* **Post-purchase merchant redirects** may navigate the top window only to your own origin over HTTPS.

Popups sever `window.opener` before navigating. If a popup is blocked, the SDK navigates the current tab instead rather than stranding the buyer.

Merchant custom CSS is sanitized in the SDK before it reaches the DOM — `@import`, `expression()`, `behavior:`, and `javascript:`/`vbscript:`/`data:` URLs are stripped — on top of authoritative server-side validation.

### Return URL safety

<Warning>
  Never pass unvalidated user input as `returnUrl`. Treat it as an open-redirect surface and use a hardcoded or server-validated URL.
</Warning>

## Production checklist

<Steps>
  <Step title="Serve your page over HTTPS">
    The modal runs in a cross-origin iframe, and browsers block mixed content.
  </Step>

  <Step title="Load the script from checkout.shoppex.io">
    Never self-host or proxy it. It has to match the iframe origin or every event is dropped.
  </Step>

  <Step title="Set shopId once">
    `Shoppex.init({ shopId })` saves repeating it on every button, and cart and multi-item checkout require it.
  </Step>

  <Step title="Handle success, error, and close">
    `shoppex:success` for conversions, `shoppex:error` for failures, `shoppex:close` with `completed: false` for abandonment.
  </Step>

  <Step title="Validate your CSP on the production domain">
    Especially `connect-src`, whose absence looks like a styling bug rather than a policy violation.
  </Step>

  <Step title="Check dark mode and mobile">
    The modal follows `prefers-color-scheme` and switches to a sheet layout on small screens.
  </Step>

  <Step title="Verify product, variant, and group IDs">
    A wrong ID does not open the modal. Watch the console.
  </Step>
</Steps>

## Troubleshooting

### The modal does not open

* The script loaded and `window.Shoppex` exists.
* The element carries a valid `data-shoppex-product-id`, `data-shoppex-group-id`, or `data-shoppex-items`.
* Your click handler does not call `stopPropagation()` before the SDK's handler runs.
* The console shows no `[Shoppex]` warning. Every rejected configuration logs one.

### The modal opens but stays empty

Almost always CSP. Check the console for a `frame-src` violation, then for `connect-src`.

### Multi-item or cart checkout fails immediately

It needs `shopId`, and it calls the API from your page. Check `connect-src`, then the `shoppex:error` detail for the API's message.

### Events do not fire

* Listen on `document`, not on the button.
* The SDK only listens while a modal is open. Register your listeners once, at load.

### Styling looks wrong

* Under a strict CSP, pass `nonce` to `Shoppex.init()`.
* Your checkout styling is fetched from the API. If `connect-src` blocks it, the modal falls back to default styling.

### Debug helpers

```javascript theme={"system"}
console.log('Shoppex available:', typeof window.Shoppex !== 'undefined');

document.addEventListener('shoppex:error', (event) => {
  console.error('Embed error:', event.detail.error);
});

document.addEventListener('shoppex:embed-load-fallback', (event) => {
  console.warn('Embed loaded without a ready signal:', event.detail);
});
```

## Future work

These are known gaps. They are listed here so integrations are built against what exists rather than what is assumed.

**Immutable versioned script URL with SRI.** Real SRI pinning needs two things this URL does not have: a version segment whose content never changes, and an `Access-Control-Allow-Origin` header so a cross-origin `integrity` check can run at all. Shoppex already delivers its storefront commerce runtime this way — content-addressed, immutable per build, with an auto-injected `integrity` attribute — so the pattern is proven in-house and the embed script can follow it. Until then, the digest above is a record, not an enforcement mechanism.

**Affiliate attribution beyond the invoice.** `affiliateCode` and `referralCode` travel with the checkout and land on the invoice. Attribution that survives across sessions — a first-touch cookie, a click identifier, or a referral captured on one page and honored on a purchase made later — needs backend persistence that does not exist yet.

**Reward events the checkout does not emit.** `shoppex:rewards-applied` and `shoppex:rewards-error` are part of the protocol and are re-dispatched by the SDK, but the checkout never sends them. They will start firing without any change on your side.

**Explicit parent origin.** The checkout learns your origin from the iframe request's referrer. Passing it explicitly as a query parameter — which the protocol already supports on the receiving side — would make message targeting exact regardless of your referrer policy.

**Nonced widget styles.** Styles for `data-shoppex-widget` are injected without the CSP nonce, so the prebuilt widgets need `'unsafe-inline'` in `style-src`. The modal itself is fully nonce-aware.

<Card title="Integration patterns and framework snippets" icon="window-maximize" href="/developers/embeds/overview">
  Next.js, React, WordPress, and Webflow snippets, plus the hybrid product-cards pattern.
</Card>
