---
name: sezzle-troubleshoot
description: >
  Diagnose a Sezzle integration that is failing — read the API error body, inspect
  order state, and match the symptom to its usual cause: 401s, rejected amounts, 404 on
  an order, checkout that never completes, capture/release/refund failures, webhooks
  that never arrive, signature mismatches, duplicate charges. Use when: "Sezzle returns
  401", "what does this Sezzle error mean", "my Sezzle checkout failed", "Sezzle webhook
  not firing", "capture/refund failed", "debug my Sezzle integration". Diagnosis only —
  it names the cause and hands the fix to the Sezzle skill for the affected feature.
metadata:
  author: Sezzle
  version: "1.0.0"
---

# Troubleshoot a Sezzle Integration

Find the cause, name it, then hand off to the skill that builds the fix. Don't rewrite the
integration from here.

## Optional: Load API Context

For field-level detail, read these with the `sezzle-docs` MCP tool
`query_docs_filesystem_sezzle` (install: `npx add-mcp https://docs.sezzle.com/mcp --name sezzle-docs`):

1. `cat /docs/api/errors.mdx` — error body
2. `cat /docs/api/core/orders/getv2order.mdx` — order state fields
3. `cat /docs/api/environments.mdx` — sandbox vs production behavior

## Workflow

### Step 1: Collect the facts

Ask for, or find in the code and logs:

- **Environment**: which base URL the failing call hit — `sandbox.gateway.sezzle.com` or
  `gateway.sezzle.com`. Most "mystery" failures are a sandbox/production mix.
- **The call**: method, path, HTTP status, and the **full error body**.
- **Order UUID** (`order.uuid` from the session response) and the `intent` used.
- **When it happened**, so you can compare against token, checkout, and authorization
  expirations.

Never ask the user to paste key values. Check presence (`grep -q '^SEZZLE_.*_KEY=.' .env`),
not contents.

### Step 2: Read the error body

Every failed call returns an **array** of error objects, not a single object:

```json
[
  {
    "code": "invalid",
    "location": "order.amount.amount_in_cents",
    "message": "Order amount must be greater than $35",
    "debug_uuid": "919f40d0-874b-4d98-810d-ed2246a8ad77"
  }
]
```

- `location` is the request field at fault — go straight there.
- `message` is human-readable and often states the exact threshold or rule.
- `debug_uuid` is what Sezzle Support needs to trace the request. Always keep it.

Status codes: `400`/`422` invalid request, `401` any failed auth **including expired bearer
tokens**, `404` resource not found. Code that parses errors as a single object will throw
before it ever shows you the message — check that first if the reported error is a JSON
parse failure.

### Step 3: Inspect order state

```
GET /v2/order/{order_uuid}
Authorization: Bearer {token}
```

| Field | What it tells you |
|---|---|
| `checkout_status` | `active` shopper hasn't finished; `completed` finished; `deleted` checkout expired, was deleted, or funds released; `denied` exceeds the shopper's spending power |
| `checkout_expiration` | Deadline for the shopper to use `checkout_url` |
| `authorization` | **Absent until the shopper completes checkout.** Missing ≠ error |
| `authorization.approved` | The only proof of payment approval |
| `authorization.expiration` | Capture deadline on AUTH orders |
| `authorization.releases[]` / `captures[]` / `refunds[]` | What has already happened to the money. **Sum `entry.amount.amount_in_cents`, not the flat `entry.amount_in_cents` that `getv2order.mdx` documents** — the flat read totals zero and a captured order looks uncaptured |

`checkout_status` says nothing about capture. An order can be `completed` and uncaptured.

The response also carries shopper name, email, phone, and addresses. Don't echo those into
summaries or tickets.

For webhook problems, also run `GET /v2/webhooks` to confirm the subscription still exists
and lists the event you expect.

### Step 4: Match the symptom

| Symptom | Usual cause | Check | Skill to hand off to |
|---|---|---|---|
| `401` on every call | Token past 120 min, or keys from the other environment (keys are not transferable and look identical) | Re-authenticate; confirm dashboard host matches gateway host | sezzle-checkout |
| `401` only after the app ran a while | Token cached past `expiration_date` | Refresh before expiry, not on 401 | sezzle-checkout |
| `400` at `order.amount.amount_in_cents` | Below the merchant's minimum, or dollars sent where cents expected | Read `message` for the threshold; API floor is 100 cents | sezzle-checkout |
| `400` at `reference_id` | Spaces or symbols; only letters, digits, `-`, `_` allowed | Inspect the value | sezzle-checkout |
| `404` on `/v2/order/{uuid}` | Wrong UUID (session or checkout UUID used instead of `order.uuid`), or order lives in the other environment | Compare against the session response | sezzle-checkout |
| Session `201` but no payment | Nothing is wrong yet: `201` is session creation, not approval | `checkout_status`; wait for the intent's webhook | sezzle-checkout |
| Shopper never reaches Sezzle (sandbox testing) | Cart outside ~$20–$2500, or non-US/CA IP | Adjust cart total; test from a US/CA IP or VPN | sezzle-prototype-sandbox |
| Shopper never reaches Sezzle (production) | Cart below the merchant's configured minimum, or shopper outside US/CA | Merchant's minimum in the dashboard; `message` on the 400 states the threshold | sezzle-checkout |
| Checkout link dead | `checkout_expiration` passed; `checkout_status: deleted` | Create a new session | sezzle-checkout |
| `checkout_status: denied` | Shopper's spending power too low for this total | Smaller order or different shopper; in sandbox, release earlier test orders | sezzle-prototype-sandbox |
| Capture rejected | Authorization expired (default 30 min; 7 days on Shopify, and merchant-configurable up to 7 days), amount exceeds what remains, or already captured | `authorization.expiration`, `releases[]`, `captures[]` | sezzle-checkout |
| Approved order later `deleted`, never paid | `AUTH` order never captured; authorization expired | `captures[]` empty, `authorization.expiration` passed | sezzle-checkout |
| Release rejected | Nothing left to release: captures and earlier releases already cover the authorization, or the authorization expired and auto-released the remainder | `captures[]`, `releases[]`, `authorization.expiration` | sezzle-checkout |
| Refund fails in sandbox | No payment method on the sandbox merchant account | Add the test bank or switch payout to Delayed Settlement | sezzle-prototype-sandbox |
| Webhook never arrives | Wrong event for the intent (`CAPTURE` → `order.captured`, `AUTH` → `order.authorized`); subscription made in the other environment; receiver on localhost | `GET /v2/webhooks` | sezzle-webhooks |
| Webhooks stopped after working | Receiver returned only non-2xx HTTP codes for ~5 days of retries, so **every** subscription for that URL was deleted | `GET /v2/webhooks` returns nothing for the URL | sezzle-webhooks |
| Signature mismatch | HMAC over a re-serialized body instead of raw bytes; verifying with the other environment's private key; parsing the header as `t=…,v1=…` | Log raw body length vs parsed | sezzle-webhooks |
| Same order captured or refunded twice | Retries without `Sezzle-Request-Id` | Request logs | sezzle-checkout |
| `500 api_error` from `PATCH /v2/webhooks/{uuid}` | Duplicate URL or unknown uuid, not an outage (docs say 422/404) | `GET /v2/webhooks` and compare | sezzle-webhooks |
| Events out of order | Normal. Deliveries are not ordered; retries reuse the same `uuid` | Dedupe on webhook `uuid` | sezzle-webhooks |
| SDK hits production from a sandbox store | `apiMode` unset — the default is `live` | SDK config | sezzle-checkout |
| Widgets fail on a sandbox store with sandbox ID | Widgets always use the **production** Merchant ID | Widget config | sezzle-widgets |
| Virtual Card: card data fetch fails | Token already used (single-use), or past the 24-hour window, or `origin` not allowlisted for this environment | Request logs | sezzle-virtual-card |
| Tokenized charge fails | Customer token expired or revoked | Charge error `code` is `token_is_expired` | sezzle-tokenization |
| Dashboard keeps logging out | Sandbox and production sessions can't coexist in one browser | Close the other tab | — |

### Step 5: Report and hand off

State the cause in one line, the evidence (status, `location`, order field), and the skill
to hand off to, written as "use the sezzle-checkout skill to fix this", never "fix owner".
If that skill isn't available, describe the fix and cite the docs page it comes from.
If the cause is a bug in the merchant's own code, say where it is and what to change, and
offer to fix it. If nothing in Step 4 matches, escalate to Sezzle Support at
https://dashboard.sezzle.com/merchant/contact with: environment, endpoint, timestamp,
`debug_uuid`, and `order.uuid`. Never include key values or shopper details.

## Rules

- **Diagnose, don't rebuild.** Name the skill in the table's last column as the next step.
- **Verify before claiming.** Before saying what else a bug affects or how to recover, find
  the callers and read the route or code involved. If you haven't traced it, say so.
- **Environment first.** Before any other theory, confirm gateway host, dashboard host, and
  key source all agree.
- **`authorization` missing is not a failure** — the shopper hasn't finished.
- **Keep `debug_uuid`** from every error; it's the only handle Support has.
- **Never print key values or shopper data.**

## Source docs
- /docs/api/errors • /docs/api/core/orders/getv2order • /docs/api/environments • /docs/api/core/webhooks/getv2webhooks
