---
name: sezzle-widgets
description: >
  Add Sezzle on-site messaging widgets to product and cart pages — installment
  messaging that shows shoppers financing options while they browse. Covers the
  dynamic price widget script, document.sezzleConfig customization, static widgets,
  and checkout-installments display. Use when: "add Sezzle messaging to my product
  page", "show 4 payments of $X", "install the Sezzle widget", "configure
  sezzleConfig", "Sezzle price widget not rendering". This is
  presentation-only — for accepting payment use sezzle-checkout.
metadata:
  author: Sezzle
  version: "1.0.0"
---

# Add Sezzle On-Site Messaging Widgets

The widget reads a price out of the page's DOM and renders installment messaging next
to it. It is presentation-only: no keys, no server calls from your backend, no money.

**The most common failure is silent.** The script tag renders nothing unless a config
tells the widget where the price is, and a stored server-side config discards yours just
as quietly. End the install with the Step 6 report: state what you verified and what a
human still has to look at.

## Optional: load the full option reference

If the `sezzle-docs` MCP server is available, `query_docs_filesystem_sezzle` can read:

1. `cat /docs/guides/widgets/sdk.mdx` — full `sezzleConfig` reference
2. `cat /docs/guides/widgets/static.mdx` — static alternative
3. `cat /docs/guides/widgets/checkout-installments.mdx` — checkout page variant
4. `ls /docs/plugins/` — platforms with a one-click widget install

Install with `npx add-mcp https://docs.sezzle.com/mcp --name sezzle-docs`. **This is
optional**: Step 3 below has everything an install needs.

Either way, configure only options you can point to a source for. Unrecognized keys are
silently ignored, so an invented option produces a widget that looks installed but ignores
the setting you thought you made.

## Prerequisites

1. **A Sezzle merchant account in production.** The widget has no sandbox. The script always
   connects to Sezzle's production environment, even on a sandbox or staging store, so a
   sandbox-only account cannot render a working widget.
2. **The production Merchant ID** (36-character UUID) from Dashboard → Settings → Business.
   Sandbox IDs and API keys do not work here. See Step 1.
3. **A page with a price in the DOM** and a selector or XPath that reaches it. Without this
   the widget renders nothing. See Step 5.

If the user has no production account, tell them the widget cannot be tested yet. Do not
substitute a sandbox Merchant ID.

## Workflow

`<skill-dir>` is this skill's base directory, announced when the skill loads.
Each script prints `OK`, `FAIL`, `BLOCKED` or `UNVERIFIED` report lines; a `BLOCKED` result
spans several lines. Exit 0 means proceed, 1 means a check failed, and 2 (verify-render.mjs
only) means the check could not run — never read 2 as a pass. Quote those lines verbatim in
the Step 6 report.

### Step 0: Check for a platform plugin

Shopify, WooCommerce, BigCommerce, Shift4Shop and others have a one-click widget
install that handles all of the below. If the store runs on one of these, point the
user at the plugin instead of hand-rolling the install.

### Step 1: Get the Merchant ID from the user

The widget needs the merchant's 36-character Merchant ID (a UUID). **Ask the user for
it — do not guess or invent one.** They can find it in the Sezzle Merchant Dashboard
under Settings → Business (shown as "ID"), or pre-populated in a copy-paste snippet in
the Dashboard Setup Checklist.

Validate the shape before using it:

```
^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$
```

This is **not** an API key. It is not `sz_pub_...` / `sz_pr_...`, and it is not the
numeric merchant ID some integrations use.

### Step 2: Check the Merchant ID works

```bash
sh <skill-dir>/scripts/check-merchant-id.sh $MERCHANT_ID
```

Exit 0 means proceed. Exit 1 means stop; the last line says why, and none of the
reasons are fixable in code:

- `FAIL` on shape or HTTP 400 — not a production widget-enabled account (sandbox IDs
  land here). The merchant contacts merchantsupport@sezzle.com.
- `BLOCKED` Sezzle-saved config — Sezzle serves its own `document.sezzleConfig` for this
  merchant. A local config shallow-merges on top (`Object.assign`), so local keys win —
  but the merge is shallow, so omitting `configGroups` inherits the served array (which
  may target wrong selectors). Have Support remove the override, or use the static
  widget. The script
  prints the saved
  `targetXPath` / `urlMatch` values. **Check each one against this site before saying
  anything about whether the widget will render** — grep the selectors in the page
  source, and account for generated class names (a CSS-in-JS or utility-class UI emits
  none of the semantic classes a Sezzle-saved config usually targets). Report what you found,
  not what a Sezzle-saved config typically does. If none match, the widget renders nowhere,
  silently. Either Support removes the override, or use the static widget (see
  Alternatives), which does not talk to Sezzle's servers.

Copy every line it prints into the Step 6 report verbatim.

### Step 3: Write the configuration script — required

Step 2 already told you whether a Sezzle-saved config exists. If it did, do not write a
local config — local keys win on overlap, but the merge is shallow and easy to get wrong. If the saved selectors match this site, the widget is
already configured by Sezzle, so skip to Step 5 and verify it renders. If they do not
match, stop and hand off (Support removes the override, or use the static widget).
Otherwise write the config: without one the script renders nothing.

**Ask the user which surfaces get a group — never decide it yourself.** List every price a shopper
sees before buying and name what each costs: a class to add, a strikethrough to ignore, a drawer
`urlMatch` can't scope.

Put `document.sezzleConfig` **before** the widget script in the page source.

```html
<script type="text/javascript">
  document.sezzleConfig = {
    configGroups: [
      {
        targetXPath: ".product-price",   // REQUIRED — element holding the price text
        renderToPath: "..",              // where to render, relative to the target
        urlMatch: "product",             // apply this group only on matching URLs
        theme: "light",
        alignment: "left",
        ignoredPriceElements: ["DEL", "STRIKE"],
        ignoredFormattedPriceText: ["Subtotal", "Total:", "Sold Out"],
        containerStyle: {},
        textStyle: {},
        logoStyle: {},
        hideClasses: []
      }
    ],
    language: "en",
    minPrice: 0,        // CENTS
    maxPrice: 250000,   // CENTS
    observeElements: [
      { eventType: "click", element: ".variant-selector" }
    ]
  };
</script>
```

**`configGroups` is required and must be a non-empty array, and every group must have
`targetXPath`.** The widget throws on a malformed or empty `configGroups`. With no
`document.sezzleConfig` at all it fails silently instead. Everything else has a default.

#### Option reference

Per config group:

| Option | Default | Notes |
|---|---|---|
| `targetXPath` | — | **Required.** Element containing the price text. `/` separates path segments, `#` = id, `.` = class, `TAG-N` = zero-based tag index. e.g. `#ProductSection/.product-price/SPAN-1` |
| `renderToPath` | `".."` | Where to render, relative to `targetXPath`. `.` = sibling, `..` = up one parent, `../../BUTTON-0` = grandparent then first button |
| `urlMatch` | — | Applies the group only on URLs matching this **case-insensitive regex**. Plain words like `product` or `cart` are the norm; escape `.`, `?`, `(` if you need them literally |
| `theme` | auto-detect | `light`, `dark`, `black-flat`, `white-flat`. Unset detects background and picks a contrasting logo |
| `alignment` | `"auto"` | `left`, `center`, `right`, `auto` |
| `alignmentSwitchMinWidth` | — | Viewport width in px below which `alignmentSwitchType` applies. Common: `768` |
| `alignmentSwitchType` | — | Alignment to use below that width |
| `ignoredPriceElements` | `[]` | Child elements to skip when reading the price — use for struck-through original prices |
| `ignoredFormattedPriceText` | `["Subtotal","Total:","Sold Out"]` | Text to skip when reading the price |
| `containerStyle` / `textStyle` / `logoStyle` | `{}` | CSS as JSON, camelCase keys |
| `hideClasses` | `[]` | XPaths of elements to hide while the widget shows — e.g. competitor messaging |
| `relatedElementActions` | `[]` | See below — this is **not** for price updates |

Global (must sit outside `configGroups` — putting them inside throws):

| Option | Default | Notes |
|---|---|---|
| `language` | `<html lang>` | `en`, `fr`, `es`. Anything else falls back to `en` |
| `minPrice` | `0` | **Cents.** Below this the widget still renders, with an added "for orders over $X" note. It does not block checkout |
| `maxPrice` | `250000` | **Cents.** Above this the widget does not render at all |
| `observeElements` | `[]` | `{eventType, element}` pairs — re-initializes the widget on that event |
| `apDualInstall` / `klarnaDualInstall` | `false` | Appends an Afterpay / Klarna logo and modal alongside Sezzle |

Do not set `merchantID` yourself; the served script injects it.

#### Re-rendering on variant / price change

Three different mechanisms, commonly confused:

- **Price text changes in place** — handled automatically. The widget observes the
  target element for mutations and recalculates.
- **A control changes the price without mutating the target** (variant dropdown,
  swatch, quantity stepper) — use **`observeElements`**. This is the option you want
  for a product page with variants.
- **`relatedElementActions`** — runs your callback against a *different* element to
  style or hide the widget. It does not re-render or update the price. Use it for
  things like hiding the widget inside a comparison table:

  ```js
  relatedElementActions: [{
    relatedPath: "..",
    initialAction: function (relatedEl, widgetEl) {
      if (relatedEl.className.indexOf("compare") > -1) widgetEl.style.display = "none";
    }
  }]
  ```

### Step 4: Add the widget script

Add it at the bottom of the page HTML, **after** the config:

```html
<script src="https://widget.sezzle.com/v1/javascript/price-widget?uuid={merchant_id}"></script>
```

If the site sends a Content-Security-Policy, allow:

- `script-src` / `connect-src` — `https://widget.sezzle.com`
- `img-src` — `https://media.sezzle.com` (logo assets)
- `connect-src` — `https://media.sezzle.com` (dual-install modal HTML)
- `frame-src` / `connect-src` — `https://dynamic-modal.sezzle.com` (the info modal)
- `style-src` — `https://fonts.cdnfonts.com`, `https://fonts.googleapis.com` (the bundle `@import`s its fonts)
- `font-src` — `https://fonts.cdnfonts.com`, `https://fonts.gstatic.com`

Then check the assembled page: the URL the server renders, not the file you edited.
Config and script tag often live in different template partials, and only the assembled
page shows the order they actually come out in:

```bash
sh <skill-dir>/scripts/check-page.sh <url> $MERCHANT_ID
```

### Step 5: Verify the render

The widget's copy and installment count vary by locale and cart value, so never assert
on text. Assert on structure, and only against **the script Sezzle serves for this
merchant** — a test that loads a config you wrote proves nothing, because a stored
config would discard it.

1. **Ask the user to run this in the browser console** on a product page and paste the
   result back. Every site has a browser, so this is the default path:

   ```js
   ({groups: document.sezzleConfig?.configGroups?.length,
     widgets: document.querySelectorAll('.sezzle-shopify-info-button').length,
     amounts: [...document.querySelectorAll('.sezzle-payment-amount')].map(e => e.textContent.trim()),
     hidden: document.querySelectorAll('.sezzle-shopify-info-button.sezzle-hidden').length})
   ```

   `widgets` must equal the number of config groups whose `urlMatch` matches the current
   page (all of them only if no group sets `urlMatch`), every amount non-empty, `hidden` 0.

2. **If the project has `jsdom` available**, check saved markup without a round trip — save
   the rendered page including the config `<script>` (for an SPA: DevTools → copy the root's
   outerHTML, not the raw `index.html`) and run from the repo root:

   ```bash
   node <skill-dir>/scripts/verify-render.mjs <saved-page.html> [expected-group-count]
   ```

   Pass the number of config groups whose `urlMatch` matches the page you saved — it
   defaults to the full `configGroups` length, which is only correct when no group sets
   `urlMatch`. It uses the script cached by the ID check and reports widget count vs config groups, a
   parsed amount per widget, and no `.sezzle-hidden`. Exit 2 means it could not run —
   fall through, never treat it as a pass.

Either way, these need a human in a browser — list them as such in the report:

- The message sits sensibly next to the price.
- Changing a variant updates the amount (`observeElements` wired correctly).
- Clicking the widget opens the Sezzle modal, and its X closes it again.
- On an SPA, the widget reappears after navigating between pages.

### Step 6: Report — required shape

Write it before you call the install done. Put every check from Steps 2–5 in exactly one
bucket, and keep the last line's count accurate.

```
Verified
  ✓ <one line per OK from check-merchant-id / check-page / verify-render or console result>

Needs you in a browser
  · <one line per manual item from Step 5, phrased as an instruction: "Open a product page and …">

Nobody has checked these
  · <anything the skill lists that was neither verified nor handed off — e.g. the maxPrice cutoff>

N of <total automated + manual> checks verified automatically; P items still need a browser.
```

N counts only what a command confirmed — browser checks go in the second bucket, not in N.
Never call the widget installed on automated checks alone.

## Alternatives

**Static widget** — you supply the price instead of the widget scraping it. Choose it
when the price is not reliably readable from the DOM, or when you want no runtime
dependency on Sezzle's servers. It still computes and displays installment amounts, from
a required `amount` option, but you own price updates: wire `alterPrice(newPrice)` into
your own variant-change handler.
`npm install @sezzle/sezzle-static-widget`, or `@sezzle/sezzle-react-widget` for
React — two packages with different APIs and different price units, so read
`<skill-dir>/references/static-widget.md` before installing either.

**Checkout installments widget** — a separate component showing a sample payment
schedule on the checkout page. Different package and config; see
`/docs/guides/widgets/checkout-installments.mdx`.

## Pitfalls

- **Config before script.** The widget reads `document.sezzleConfig` once, at load.
  A config that appears after the script tag is never seen.
- **No config, no widget — silently.** With no local config and no Sezzle-saved config, the
  script loads, logs an internal error, and renders nothing. There is no console error
  you would notice and no visible failure.
- **Stored configs shadow yours.** A local config shallow-merges on top of the served
  one — local keys win, but omitting `configGroups` inherits the served array. Ask Merchant Support to remove the saved
  override
  (`/docs/guides/widgets/sdk.mdx`), or use the static widget.
- **Use the production Merchant ID, including on a test store.** There is no supported
  sandbox widget mode. The widget moves no money, so pointing a staging storefront at
  the production ID is the intended path.
- **Merchant ID ≠ API key.** See Step 1.
- **Prices are cents in config, dollars on the page.** `sezzleConfig`'s `minPrice` /
  `maxPrice` are cents, while the price the shopper sees is dollars — convert at the
  call site. The static widget packages differ from each other here; see
  `<skill-dir>/references/static-widget.md`.
- **Unknown config keys are ignored, not rejected.** A typo or an invented option
  fails silently. Stick to documented options.

## Source docs

`/docs/guides/widgets/sdk` • `/docs/guides/widgets/static` •
`/docs/guides/widgets/checkout-installments` • `/docs/plugins`
