---
name: sezzle-checkout
description: >
  Implement a full Sezzle BNPL checkout integration from scratch — authentication,
  session creation, shopper redirect, and order management (capture, refund, release,
  reauthorize, upcharge). Covers direct REST API and the JavaScript Checkout SDK
  (popup, iframe, redirect), plus express checkout. Use when: "add Sezzle to my
  checkout", "integrate Sezzle payments", "create a Sezzle checkout session",
  "capture or refund a Sezzle order", "implement auth and capture with Sezzle",
  "add the Sezzle JS SDK", "add Sezzle express checkout". Not for
  Shopify/WooCommerce/BigCommerce/Magento/Wix — those have platform plugins.
metadata:
  author: Sezzle
  version: "1.1.0"
---

# Integrate Sezzle Checkout

Every path shares one spine: authenticate → create session → shopper approves → settle.

## Pick the path first

Use the interactive question tool:

| The merchant wants | Read |
|---|---|
| Server-side REST with an off-site redirect (the default) | Steps 1–6 below |
| In-context checkout (popup or iframe) via the JS SDK | `references/javascript-sdk.md` |
| Sezzle collects the shipping address, you quote shipping/tax mid-checkout | `references/express-checkout.md` |
| Refund, release, reauthorize, partial capture, upcharge, or reading amounts off an order | `references/order-management.md` |
| Webhooks (subscribing, signature verification, event payloads) | the `sezzle-webhooks` skill |

Read a reference file when you reach the step that needs it, not before.

## Before starting: load API context

With the `sezzle-docs` MCP server, read via `query_docs_filesystem_sezzle`:

1. `cat /docs/api/core/sessions/postv2session.mdx` for the full Create Session schema. Always.
2. `cat /docs/api/core/orders/getv2order.mdx` for the Get Order response, including the `authorization` child attributes. Always. **Caveat:** the event shapes documented there for `captures`, `refunds` and `releases` are wrong — before parsing those arrays, read `references/order-management.md` § "Reading amounts off an order" for the actual live shape.
3. `cat /docs/guides/auth-and-capture.mdx` for AUTH vs CAPTURE semantics, when choosing intent in Step 2.
4. `cat /docs/guides/direct/integration.mdx` for the JS SDK path only.
5. `cat /docs/guides/express/express-checkout.mdx` for the express path only.

If it isn't installed, ask the user to run `npx add-mcp https://docs.sezzle.com/mcp --name
sezzle-docs`. Before writing code that parses any Sezzle response, read that endpoint's own
response schema. Shapes differ between endpoints, and a request body doesn't predict a response.

## Environments

| | Sandbox | Production |
|---|---|---|
| API base | `https://sandbox.gateway.sezzle.com/v2` | `https://gateway.sezzle.com/v2` |
| Dashboard | `https://sandbox.dashboard.sezzle.com/merchant` | `https://dashboard.sezzle.com/merchant` |

Keys are environment-specific and not transferable. Production additionally requires a linked
card and bank account on the merchant account; sandbox does not. Testing refunds in sandbox needs
a payment method on the account, or the `Delayed Settlement` payout option.

**If the user has no Sezzle account yet, stop and use the `sezzle-prototype-sandbox` skill.** It
covers sign-up, the manual application approval, key generation and shopper test data. Do not
guess at a signup or key-generation flow.

## Step 1: Authenticate

Check the codebase and `.env` for existing keys. Grep `SEZZLE`, since names are often
environment-suffixed (`SEZZLE_SANDBOX_PUBLIC_KEY`), and select the API base URL from the same
switch. **Human handoff if absent:** only an Admin dashboard user can generate keys, so stop and
ask. Save to `.env` and confirm `.gitignore` covers it.

```
POST /v2/authentication
{ "public_key": "...", "private_key": "..." }
```

Response: `{ "token": "...", "expiration_date": "...", "merchant_uuid": "..." }`

Use `Authorization: Bearer {token}` on all subsequent calls. **Tokens expire after 120 minutes.**
Refresh proactively; you may hold several. A 401 on any call means re-authenticate and retry once.

## Step 2: Decide intent (AUTH vs CAPTURE)

- `CAPTURE` (default): captured automatically after a successful checkout. Right for most merchants.
- `AUTH`: authorization only. Pick it when your backend gates fulfilment. You must capture before
  the authorization expires (default 30 min; Dashboard Settings allows 30 min to 7 days). **On
  expiry capture is denied and the uncaptured amount is released.** The order stays readable, with
  the released amount under `authorization.releases` and `authorization.expiration` in the past,
  so don't treat a missing order as the expiry signal. Recovery is `reauthorize`, not a new session.

Capture-first (Step 4) is correct under either intent.

## Step 3: Create a session

```
POST /v2/session
{
  "cancel_url":   { "href": "https://merchant.example/cancel" },
  "complete_url": { "href": "https://merchant.example/complete" },
  "customer": {
    "first_name": "Jane", "last_name": "Doe",
    "email": "jane@example.com", "phone": "5555045294",
    "shipping_address": { "name": "Jane Doe", "street": "123 W Lake St", "city": "Minneapolis", "state": "MN", "postal_code": "55408", "country_code": "US" }
  },
  "order": {
    "intent": "CAPTURE",
    "reference_id": "ord-12345",
    "description": "Order ord-12345",
    "order_amount": { "amount_in_cents": 10000, "currency": "USD" },
    "items": [ { "name": "Widget", "sku": "sku-1", "quantity": 1, "price": { "amount_in_cents": 10000, "currency": "USD" } } ],
    "checkout_mode": "redirect"
  }
}
```

Response (201) includes `order.uuid` and `order.checkout_url`. Redirect the shopper to
`checkout_url`.

- Required in `order`: `intent`, `reference_id`, `description`, `order_amount`. `description` is
  easy to miss and a 400 if omitted. `items` and `customer` are optional but boost conversion.
- All amounts are **in cents** (10000 = $100.00). Range 100 to 5,000,000; merchant config may set
  a higher minimum.
- `reference_id`: alphanumeric, dashes, underscores only.
- `cancel_url`/`complete_url` are objects (`{ "href": ... }`, not strings), GET only. `cancel_url`
  is required for `redirect` mode. `complete_url` is required for `redirect` mode **or** any
  session with `customer.tokenize: true`, including `iframe`/`popup`, where it's easy to assume
  neither is needed.
- `checkout_mode` lives on `order`: `redirect` (default), `iframe`, `popup`. An unrecognized value
  is silently treated as `redirect`, not rejected. For `iframe`/`popup` see
  `references/javascript-sdk.md`.
- Long Term Lending merchants MUST send a complete `shipping_address` (no PO boxes). The address
  phone key is `phone`.
- Sports & Hobbies / Hunting & Fishing merchants must send `category_path` per item (delimiter
  `>`). The API won't tell you the account is in these categories, so ask the user.
- Express checkout adds a top-level `express_checkout_type` and `order.requires_shipping_info`;
  see `references/express-checkout.md`.

**Validation failures return 400** with a body of `[{ "code", "message", "location" }]`, where
`location` names the offending field (e.g. `complete_url.href`). Read `location` and fix that
field rather than reshaping the request.

## Step 4: Handle completion

Persist `order.uuid` from the 201. The redirect back to `complete_url` carries **no** order or
session identifier (only `customer-uuid`, and only when tokenization succeeded), so the stored
UUID is your only correlation handle.

The completion signal (the return to `complete_url`, or the SDK's `onComplete`) means the shopper
finished and the order is authorized. **On that signal attempt capture directly**; don't poll
`GET /v2/order` first. That's one round trip instead of two, and it keeps `authorization` parsing
off the checkout-blocking path.

- `POST /v2/order/{order_uuid}/capture` with the full amount. **200** means captured.
- **422 `already_completed`** means it was already captured, either by a prior retry or by
  Sezzle's auto-capture under `CAPTURE` intent. Treat it as success.
- Match on the `code`, not the 422 status: a capture before the shopper authorized is also a 422
  (`invalid` / "Order not authorized"), and that one is a real failure.

For **asynchronous confirmation**, subscribe to the webhook matching your intent (sezzle-webhooks
skill). **The event depends on `intent`:** `CAPTURE` fires `order.captured` and never
`order.authorized`; `AUTH` fires `order.authorized`. Subscribing to the wrong one is a silent
hang, not an error.

For **status reporting** outside the confirm flow, use `GET /v2/order/{order_uuid}`. The
`authorization` object is absent until checkout completes, so handle "absent" separately from
`approved: false`.

Totalling `authorization.captures` / `.refunds` / `.releases`: sum `entry.amount.amount_in_cents`,
**not** the flat `entry.amount_in_cents` the Get Order reference documents. The flat read totals
zero, so a captured order looks merely `authorized` and the shopper is told it failed after the
money moved. `references/order-management.md` has the evidence.

## Step 5: Refund and cancel

| | |
|---|---|
| Refund a captured order | `POST /v2/order/{order_uuid}/refund`. Body is a bare `{ "amount_in_cents", "currency" }`, *not* wrapped like capture's `capture_amount`. |
| Cancel an uncaptured authorization | `POST /v2/order/{order_uuid}/release`, same bare body. |

Send `Sezzle-Request-Id` on every mutating call for idempotency; `POST /v2/session` does not
accept it, and it's capped at 36 characters. Partial captures, reauthorize, upcharge, `PATCH`
and the endpoints to avoid are in `references/order-management.md`.

## Step 6: Test in sandbox

Completing a checkout needs a browser, a shopper login and an OTP, so you cannot verify end to
end alone. Don't report success after a 201. Say which half you verified.

**Verify unaided (do all of it):**
- `POST /v2/authentication` returns a token and a future `expiration_date`.
- `POST /v2/session` returns 201 with `order.uuid` and `order.checkout_url`, and `order.intent`
  matching what you sent.
- `GET /v2/order/{order_uuid}` returns 200 with `checkout_status: active` and **no**
  `authorization`. That's the correct pre-checkout state, and it proves you stored the right UUID.
- Capture that UUID before checkout: a 422 `invalid` proves you distinguish it from
  `already_completed`.
- Your `complete_url` handler resolves the stored `order.uuid` without reading the query string.
- SDK paths only: after loading the payment surface and changing the cart, exactly one Sezzle
  button exists and one session per total, counted in a production build. A framework's dev-mode
  double invoke (React StrictMode) doubles that; any more means a lifetime is tied to a
  re-render.
- Omit `description` from a session and confirm you surface the 400 `location` rather than
  swallowing it.

**Human handoff for the rest.** Ask the user to complete a sandbox checkout: cart total
$20–$2500, US/CA IP (VPN otherwise), shopper OTP `123123`, Visa test card `4242 4242 4242 4242`.
If Sezzle doesn't appear at checkout, check that total range and the IP first. It's a heuristic
rather than an API limit: the floor is $1.00, and the effective minimum is merchant-configured.
Then confirm the shopper landed on `complete_url`, and verify from your side that capture succeeds
(or returns `already_completed`), followed by a refund. `sezzle-prototype-sandbox` covers the
setup in full.

## After sandbox works

Tell the user what remains before real money: production account and keys, re-subscribed
webhooks, re-requested allowlists, and a safe first transaction. Use `sezzle-go-live` when ready.

## Source docs

Beyond the pages listed under "Before starting":

- `/docs/guides/direct/manual` for the direct REST walkthrough, which matches Steps 1–5
- `/docs/api/environments` • `/docs/api/test-cards`
- `/docs/plugins/` for platform plugins (Shopify, WooCommerce, BigCommerce, Magento 2, Wix, …)
