---
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 3.

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

## Workflow

Each check reports `OK`, `FAIL`, `BLOCKED` or `UNVERIFIED` lines; a `BLOCKED` result spans
several lines. Quote the Step 2 script's lines verbatim in the Step 6 report, and write the
other checks' results in the same form. `UNVERIFIED` means the check could not run — never
read it as a pass.

### 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

Save this to `${TMPDIR:-/tmp}/check-merchant-id.sh` and run `sh` on it with `$MERCHANT_ID` as
the argument:

```sh
#!/bin/sh
set -u
export LC_ALL=C   # scan bytes; a non-UTF-8 byte must not silence the BLOCKED check

ID="${1:-}"
OUT="${TMPDIR:-/tmp}/sezzle-widget-$(id -u).js"
URL="https://widget.sezzle.com/v1/javascript/price-widget?uuid=$ID"

printf '%s\n' "$ID" | grep -Eq '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' \
  || { echo "FAIL  not a 36-char lowercase UUID: $ID"; exit 1; }

TMP=$(mktemp); trap 'rm -f "$TMP"' EXIT
code=$(curl -s --compressed --max-time 30 -o "$TMP" -w '%{http_code}' "$URL") || code=000

case "$code" in
  200) mv "$TMP" "$OUT" || { echo "FAIL  could not cache the served script at $OUT"; exit 1; }
       [ -s "$OUT" ] || { echo "FAIL  HTTP 200 but the served script is empty"; exit 1; } ;;
  400) echo "FAIL  HTTP 400: UUID is well-formed but not active for widgets (sandbox IDs land here). Not fixable in code; merchant must contact merchantsupport@sezzle.com"; exit 1 ;;
  000) echo "FAIL  no response from widget.sezzle.com (network?)"; exit 1 ;;
  *)   echo "FAIL  HTTP $code from widget.sezzle.com"; exit 1 ;;
esac

# Errors come back as HTTP 200 with only a /* comment */ (bad UUID, deactivated merchant,
# unsupported country).
body=$(tr -d '\n' < "$OUT")
if printf '%s' "$body" | grep -qx '/\*.*\*/'; then
  case "$body" in
    *"not supported in your country"*)
      echo "UNVERIFIED  $body: Sezzle blocks the country this check runs from, not the merchant"
      exit 1 ;;
  esac
  echo "FAIL  HTTP 200 but the body is only a comment, not the widget script: $body"
  exit 1
fi
echo "OK    merchant ID is live (HTTP 200, script served)"

# Must run after the checks above: an empty or error body passes this grep. A Sezzle-saved
# config is an unconditional `document.sezzleConfig = {` in the prelude before the bundle.
if awk '/^\(function\(\)\{try\{/{exit} {print}' "$OUT" | grep -q '^document\.sezzleConfig *= *{'; then
  echo "BLOCKED  Sezzle has a widget config saved on its own servers for this merchant, and"
  echo "         the script it serves replaces any config your page sets. The widget will"
  echo "         only appear where the selectors below match this site — check each one"
  echo "         against the page before concluding it will render anywhere:"
  awk '/^\(function\(\)\{try\{/{exit} {print}' "$OUT" \
    | grep -oE '"(targetXPath|urlMatch)": *"[^"]*"' \
    | sed 's/^/           /'
  echo "         To fix: ask Sezzle Support to remove the saved config, or use the static widget."
  exit 1
fi
echo "OK    no Sezzle-saved config: a local document.sezzleConfig is required"
echo "      served script cached at $OUT"
exit 0
```

Exit 0 means proceed. Exit 1 means stop, except a `BLOCKED` whose saved selectors match
this site (see Step 3); 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.
- `UNVERIFIED` not supported in your country — this says nothing about the merchant. Ask
  the user to open the widget URL from Step 4 in their browser and paste the start of the
  response, or to run the check from the merchant's country.
- `BLOCKED` Sezzle-saved config — Sezzle serves its own `document.sezzleConfig` for this
  merchant and it discards yours entirely. 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 (§ "Static widget"), which ignores Sezzle-saved
  configs.

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

Step 2 already told you whether a Sezzle-saved config exists. If it did, do not write the
config below — it will be discarded. 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
: "${MERCHANT_ID:?set MERCHANT_ID to the merchant UUID first}"
P="${TMPDIR:-/tmp}/sezzle-page.html"
curl -sL --compressed --max-time 30 -o "$P" -w '%{http_code}\n' <url>
LC_ALL=C tr -d '\n\r' < "$P" | LC_ALL=C awk -v id="$MERCHANT_ID" \
  '{print "config:", index($0, "document.sezzleConfig"), "tag:", index($0, "price-widget?uuid=" id)}'
```

Anything but `200` means you did not get the rendered page (check the URL or bot
protection). Both positions must be non-zero and `config` lower than `tag`: the config is
read once at load, so one that comes after the script is ignored. `tag: 0` while the page
has a `price-widget?uuid=` tag means it uses a different Merchant ID. For a saved HTML file,
skip the `curl` and point `P` at it.

### 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 already has `jsdom`** (never install it), 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`). In jsdom with
   `runScripts: "outside-only"` and a URL the groups' `urlMatch` accepts, stub `fetch`, `eval`
   each inline script that sets `document.sezzleConfig`, then `eval` the served script Step 2
   cached. Poll up to 8 s for `.sezzle-shopify-info-button`. Pass: the count equals the config
   groups whose `urlMatch` matches that URL, each widget has a non-empty
   `.sezzle-payment-amount`, and none has `.sezzle-hidden`. If it cannot run, report
   `UNVERIFIED` and 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 Steps 2, 4 and 5 (console or jsdom 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 fewer runtime calls
to Sezzle (only the logo loads, from `media.sezzle.com`). It still computes and displays
installment amounts from the price you pass. With the vanilla package you own price updates
(`alterPrice`); the React package re-renders on `price` changes.
`@sezzle/sezzle-static-widget` and, for React, `@sezzle/sezzle-react-widget` are two packages
with different APIs and different price units, so read § "Static widget" 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 discard yours outright.** The served script assigns
  `document.sezzleConfig = {…}` unconditionally, clobbering whatever the page set
  earlier — so a Sezzle-saved config beats a local one no matter how correctly you ordered
  config before script. The documented remedy is the only one: ask Merchant Support to
  remove the saved override (https://docs.sezzle.com/docs/guides/widgets/sdk.md), 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
  § "Static widget".
- **Unknown config keys fail silently** — typos included. Stick to documented options.

## Static widget

A stored server-side config has no effect on static widgets. They are not fully offline,
though: the logo still loads from `media.sezzle.com`. https://docs.sezzle.com/docs/guides/widgets/static.md
(or `/docs/guides/widgets/static.mdx` via the `sezzle-docs` MCP server) is the source for the
shared behaviour and the vanilla package (the React package's is its README); where it and
this section disagree, the doc wins.

### Which package

Two packages implement this, with different APIs. Follow one package's section and ignore the
other — an option belonging to the other package is ignored silently, with no warning.

- **React app** → `@sezzle/sezzle-react-widget`, which re-renders on price changes for
  you.
- **Everything else** → `@sezzle/sezzle-static-widget`, where you wire price updates
  yourself. This is also the better pick in a React app that would rather not take on
  another peer dependency.

### Verify

Static widgets have no `sezzleConfig`, so use this snippet instead of Step 5's; the expected
count is the number of placements you added. Have the user run this on a page with a price
and paste the result back:

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

`widgets` must equal the placements you added, every amount non-empty.

**A non-zero `widgets` count is not on its own a pass.** Above `maxPrice` the vanilla
package renders nothing at all (no container, no error). The React package still renders
the container (`.sezzle-shopify-info-button`) but leaves the amount empty. In both cases,
the amounts array is the check that matters — empty or missing amounts mean the price is
out of range.

### Pitfalls for both packages

- **The price option is a string with a currency symbol**, in both packages. A number
  gives no error and no installment amount.
- **`numberOfPayments` is adaptive by default.** Left alone, the widget picks 4 or 5
  installments from the order amount. Pinning `4` means 5 never appears, in the widget
  or the modal — so an expected-text assertion written against one cart value fails on
  another.
- **Above `maxPrice`, no installment amount renders and nothing complains.** The default
  cutoff is low enough to hit real carts. Check the merchant's price range against it rather
  than assuming — and in the units your package uses, which differ between the two.
- **Unmounting a widget mid-modal strands the modal.** It is appended to `<html>` with
  no cleanup, so the overlay survives and its X stops working. Avoid placing a widget
  inside a clickable ancestor — a whole-card listing link navigates on the bubbled
  click — or stop the click at the widget's wrapper.
- **CSP still matters.** `img-src`, `style-src` and `font-src` need the sources listed in
  Step 4.

### `@sezzle/sezzle-static-widget`

`npm install @sezzle/sezzle-static-widget@latest`, or copy the minified
[bundle](https://github.com/sezzle/static-widgets/blob/production/dist/bundle.js) into
your own asset file and include it — Shopify themes usually take the second route, as a
theme asset.

Place a container where the widget should appear, then construct it below:

```html
<div id="sezzle-widget"></div>
<script src="../node_modules/@sezzle/sezzle-static-widget/dist/bundle.js"></script>
<script>
  var renderSezzle = new AwesomeSezzle({ amount: `${yourPriceVariableHere}` });
  renderSezzle.init();
</script>
```

`amount` is the only required option, e.g. `"$29.99"`; on Shopify, the output of the
`| money` filter.

**`minPrice` and `maxPrice` are in cents here.**

Options whose behaviour is not self-evident:

| Option | Notes |
|---|---|
| `amount` | **Required.** String with currency symbol |
| `renderElement` | Container id, or an array of ids. Defaults to `sezzle-widget` |
| `numberOfPayments` | `4` or `5`. Adaptive unless you pin it |
| `widgetType` | `product-page`, `product-preview`, `cart` — the page category |
| `language` | Defaults to `<html lang>`; an unsupported value falls back to English |
| `parseMode` | Switch for foreign-currency number formatting |
| `merchantLocale` | Selects which program's details the modal shows (North America vs Europe) |
| `ineligibleWidgetTemplate` | Copy shown when the amount is out of range; `%%logo%%` / `%%info%%` placeholders |

The rest are cosmetic — theme, alignment and its responsive switch, fonts, colors,
margins, width, logo scale and style. They do not affect whether the widget works; read
their current names and defaults from https://docs.sezzle.com/docs/guides/widgets/static.md,
which carries the full option set.

Nothing updates automatically. Call `alterPrice(newPrice)` from your own variant, swatch or
quantity handler, or the widget keeps showing the first price it was given.

```js
document.onchange = function () {
  renderSezzle.alterPrice(`${yourPriceVariableHere}`);
};
```

Three other methods exist: `renderModalByfunction()` opens the modal from an element
other than the info icon, `isMobileBrowser()` returns true on mobile so you can show
different placements, and `getElementToRender()` hands back the widget element for
manual insertion.

### `@sezzle/sezzle-react-widget`

`npm install @sezzle/sezzle-react-widget`. It declares React, react-dom and
`prop-types` as peer dependencies — check the versions it wants against the project's.

```jsx
import SezzleWidget from "@sezzle/sezzle-react-widget";

<SezzleWidget price={`$${price.toFixed(2)}`} merchantId={MERCHANT_ID} />;
```

**`minPrice` and `maxPrice` are in dollars here** — whole currency units, not cents.

**The package ships no TypeScript declarations** (its `package.json` has no `types`
field), so a TS project needs an ambient `declare module` block. Transcribe the props
from the package README rather than guessing.

| Prop | Notes |
|---|---|
| `price` | **Required.** String with currency symbol |
| `merchantId` | **Required.** 36-character UUID |
| `numberOfPayments` | `4` or `5`. Adaptive unless you pin it |
| `includeAPModal` | Appends Afterpay's logo alongside Sezzle |

Cosmetic props — theme, alignment, fonts, colors, logo scale — do not affect whether the
widget works; take their current names and defaults from the package README.

Long-term lending props (`LTgroup`, `minPriceLT`, `maxPriceLT`, `minAPR`, `medianAPR`,
`maxAPR`, `termsToShow`) are reserved for merchants enrolled in that program. Do not set
them without confirming the merchant's package with their Sezzle contact; `LTgroup` is
the switch that enables the rest, and its preset shifts every other default.

Price updates are automatic. The component re-renders when the `price` prop changes, so a
variant or quantity change that updates your own state is enough.

Whether this package validates `merchantId` against Sezzle at all is unconfirmed — it
renders in jsdom with no network call. So it is not established that Step 2's `FAIL`
outcome is meaningful here. Do not tell a merchant their install is blocked by that check
without evidence.

The package README is the source: npmjs.com returns 403 to automated fetches, so read
`https://registry.npmjs.org/@sezzle%2Fsezzle-react-widget`. Where it and this section
disagree, the README wins. The prop list here is deliberately partial; the README carries
the full set with defaults.

## Source docs

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