---
name: sezzle-instore-pos
description: >
  Integrate Sezzle at point of sale — send a Sezzle checkout link to the customer
  by text message or email from the POS, and receive real-time payment updates when
  they complete checkout on their phone. Use when: "add Sezzle to my POS", "send a
  Sezzle checkout link by SMS", "in-store Sezzle payments", "text the customer a
  payment link", "real-time POS payment status with Sezzle". For ecommerce checkout
  use sezzle-checkout instead.
metadata:
  author: Sezzle
  version: "1.0.0"
---

# Integrate Sezzle POS Checkout

The POS creates a Sezzle session and Sezzle sends the checkout link to the customer by SMS or
email. The customer completes checkout on their own phone — signs in with phone + OTP, adds a
payment method, picks an installment plan. The POS learns the outcome from a webhook and
finalizes the sale.

## Before Starting: Load API Context

If the `sezzle-docs` MCP server is available, read these pages via `query_docs_filesystem_sezzle`:

1. `cat /docs/guides/in-store.mdx` — POS flow overview
2. `cat /docs/api/core/sessions/postv2session.mdx` — Create Session (full schema)
3. `cat /docs/api/core/webhooks/infov2webhooks.mdx` — signature, payloads, retry behavior

If not installed, ask the user to run: `npx add-mcp https://docs.sezzle.com/mcp --name sezzle-docs`.
Otherwise the workflow below is self-contained.

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

API keys are environment-specific and not transferable. Dashboard → Settings → API Keys.

## Prerequisites

1. **An approved Sezzle merchant account** in the environment you target. Sandbox and
   production require separate sign-up and separate merchant applications.
2. **API keys** (`sz_pub_…`, `sz_pr_…`) from Dashboard → Settings → API Keys. In production the account must
   first link a credit or debit card and a bank account; sandbox does not require either.
3. **A webhook receiver on a public HTTPS URL.** The POS learns the outcome from a webhook,
   so a localhost-only receiver cannot complete this flow. See the `sezzle-webhooks` skill.
4. **A test phone number or email for the shopper leg**, and a US or CA IP address on the
   device that completes checkout.

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

## Workflow

### Step 1: Authenticate

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

Use `Authorization: Bearer {token}` on every other call. Tokens last **120 minutes** — a
register that runs all day must refresh, not authenticate once at boot.

### Step 2: Choose the intent — this determines which webhook you get

Sezzle emits **one** completion event per order, and which one depends on the intent. Wire the
handler that matches your choice; the other event never arrives.

| | `AUTH` | `CAPTURE` |
|---|---|---|
| Event when the customer finishes on their phone | `order.authorized` | `order.captured` |
| Funds settle | when you call capture | immediately on completion |
| Confirmation that the sale is final | the **200 from your capture call** | the `order.captured` webhook |
| Must capture before authorization expires | yes | n/a |

**`AUTH` does not produce an `order.captured` webhook when you capture through the API.** The
capture call returns 200 with a capture UUID — that response *is* your confirmation. Do not
block the register waiting for a webhook that will not come.

Use `AUTH` when the register needs to confirm approval before releasing goods, then capture at
hand-off. Use `CAPTURE` for immediate settlement. If `intent` is omitted it defaults to `CAPTURE`.

Authorization expiry is set in the Merchant Dashboard (Settings → Payment Captures), from
30 minutes to 7 days. It is **not** a request parameter (unlike the link's `checkout_expiration`,
Step 4). If the authorization expires uncaptured, the funds are released and capture fails.

### Step 3: Subscribe to webhooks (one-time setup)

```
POST /v2/webhooks
{
  "url": "https://pos-backend.example/webhooks/sezzle",
  "events": ["order.authorized", "order.refunded"]
}
```

- Verify the `Sezzle-Signature` header on every delivery: HMAC-SHA256 of the raw request body,
  keyed with your merchant private key. See the sezzle-webhooks skill.
- A POS terminal cannot receive webhooks; the endpoint must be public HTTPS on your backend,
  which then pushes to the register.
- **The subscription deletes itself if delivery keeps failing.** Retries span several days; after
  the final failure Sezzle drops every subscription on that URL and you must recreate it.
  Sezzle emails the merchant before and at deletion, but alert on delivery failures on your side
  too; to the register, a dropped subscription looks like a quiet sales day.
- Events are not ordered. A retried older event can land after a newer one; dedupe on the
  top-level `uuid` and treat handlers as idempotent.
- `POST /v2/webhooks/test` sends a sample payload of any event to the `url` you pass. Use it
  to build the handler before running a real checkout.

### Step 4: Create the session with `send_checkout_url`

Collect the customer's mobile number (or email) at the register, then:

```
POST /v2/session
{
  "cancel_url":   { "href": "https://pos-backend.example/sezzle/cancel" },
  "complete_url": { "href": "https://pos-backend.example/sezzle/complete" },
  "order": {
    "intent": "AUTH",
    "reference_id": "pos-txn-789",
    "description": "In-store purchase #789",
    "order_amount": { "amount_in_cents": 15000, "currency": "USD" },
    "send_checkout_url": {
      "to_sms_phone": "+16125551234",
      "language": "en"
    }
  }
}
```

Response gives you `order.uuid` and `order.checkout_url`. **Store both.** The UUID keys the POS
transaction; the URL is your fallback when the message does not arrive (below).

Field notes:

- Required: `order.reference_id`, `order.description`, `cancel_url.href`, `complete_url.href`.
  Keep the URLs even for SMS; the session is rejected (400) without them.
- `to_sms_phone` texts the link, `to_email` emails it. At least one is required; supplying both
  sends both. Omitting `send_checkout_url` entirely creates an ordinary checkout with no message.
- **Phone numbers are validated.** Normalize to E.164 (`+16125551234`) at the register before
  sending. An invalid number can fail the whole `POST /v2/session` (no order UUID) or silently
  drop the SMS, so validate at the register.
  Never invent a number for testing; ask the user for a real one or use `to_email`.
- `language` accepts `en` and `fr-CA`; omitting it gives English.
- `reference_id` allows alphanumerics, dashes and underscores only. `order_amount` minimum is
  100 cents (your merchant configuration may set a higher floor).
- For multi-location merchants, identify the store in `reference_id` or your own `metadata` keys;
  both come back on `GET /v2/order/{uuid}`. Sezzle also adds `checkout_url_*` keys holding the
  customer's phone or email, so pick out your keys; don't export `metadata` wholesale.
- The link expires 30 minutes after creation unless you set `order.checkout_expiration`
  (ISO 8601, capped at 7 days out). `GET /v2/order/{uuid}` returns it; drive the register's
  countdown and timeout off it.

### Step 5: Track completion — webhook first, polling as the safety net

On the matching webhook from Step 2, mark the sale approved and (for `AUTH`) capture.

**Sezzle sends no webhook when the customer abandons or is declined.** There is no
`order.declined` event and no timeout event. Silence is the normal outcome for a customer who
walks away, so a webhook listener alone leaves the register hanging forever. Poll
`GET /v2/order/{order_uuid}` every few seconds, stopping once the webhook arrives or the table
below says to stop waiting, and drive the register off `checkout_status` (treat any other value as
not paid):

| `checkout_status` | Meaning | Register does |
|---|---|---|
| `active` | not yet completed (a cancel or walk-away looks the same until expired) | keep waiting |
| `completed` | customer finished checkout | proceed (capture if `AUTH`) |
| `denied` | Sezzle declined an attempt; the customer can still retry | kill the link (Step 7), then offer another payment method |
| `deleted` | link expired before completion | offer another payment method |
| `unknown` | Sezzle could not read the checkout | keep polling; treat as not paid |

Do not poll `authorization.approved` as your primary signal: the `authorization` object is
**absent from the response entirely** until the order is authorized, so it is missing in exactly
the states you are polling for. Once `checkout_status` is `completed`, `authorization.approved`
is available and meaningful.

Give the register a hard timeout tied to `checkout_expiration`.

### Step 6: Capture (`AUTH` only)

```
POST /v2/order/{order_uuid}/capture
Sezzle-Request-Id: pos-txn-789-capture
{ "capture_amount": { "amount_in_cents": 15000, "currency": "USD" } }
```

200 with a capture UUID means the sale is final — print the receipt on this response. Send a
stable `Sezzle-Request-Id` derived from the POS transaction ID: it makes the call idempotent,
and a cashier double-tapping *Retry* is exactly how double captures happen.

Partial capture is supported for split fulfillment; release the remainder with
`POST /v2/order/{order_uuid}/release` and `{ "amount_in_cents": 5000, "currency": "USD" }`.
Give every operation its own stable `Sezzle-Request-Id` (max 36 chars), e.g.
`pos-txn-789-cap-1`, `pos-txn-789-cap-2`, `pos-txn-789-rel`. Reusing one ID across
partial captures or refunds makes the later call a no-op replay.

### Step 7: Cancel and return

- **Customer walks away before completing:** `DELETE /v2/order/{order_uuid}/checkout` kills the
  link so it cannot be completed later. Do this whenever the register voids the transaction;
  `checkout_status` stays `active` until the original expiry. A checkout completed just before
  the DELETE still stands: check the order once more and release (`AUTH`) or refund (`CAPTURE`)
  it. Unwind a late completion webhook the same way.
- **Authorized but the sale is voided before capture:** `POST /v2/order/{order_uuid}/release`.
- **Return after capture:** `POST /v2/order/{order_uuid}/refund` with
  `{ "amount_in_cents", "currency" }` — note this body is flat, unlike capture's nested
  `capture_amount`. Partial refunds supported. Send `Sezzle-Request-Id`.

## The message can silently not arrive

A 201 from `POST /v2/session` does **not** guarantee the customer got a text or email. Shoppers
can opt out of these notifications, and for an opted-out recipient Sezzle creates the checkout
and skips the message without reporting an error.

Design for it: the session response contains `checkout_url`. Show it at the register as a QR
code, or offer to read it out, whenever the customer says nothing arrived. This also covers
carrier delays and wrong-number entry.

## Testing in sandbox

Point at the sandbox base URL with sandbox API keys, then:

- Prefer `to_email` — you can read the inbox. For SMS, ask the user for a real mobile number
  they control; do not fabricate one.
- Regardless of channel, the response's `checkout_url` is directly openable, so the full flow
  can be exercised without waiting for a message.
- Shopper OTP is always `123123`. Test card `4242 4242 4242 4242`, any 3-digit CVV, any future
  expiry.
- Keep test amounts small and pick Pay in Full so spending power does not block the checkout.

Verify the whole loop: session created → checkout completed on the phone → correct webhook
received for your intent → capture returns 200 → order visible in the Sandbox Dashboard.

## Pitfalls

- **Don't release goods on session creation.** Wait for the completion webhook or a
  `checkout_status` of `completed`.
- **Don't wait for `order.captured` after an API capture** — see Step 2.
- **Don't treat a 201 as "message delivered"** — see above.
- **Don't poll `authorization.approved`** for a checkout that may not have completed — poll
  `checkout_status`.
- **Don't let the webhook subscription die quietly.** Alert on delivery failures.
- **Idempotency:** `Sezzle-Request-Id` on capture, release and refund.
- **Money units:** every amount is in cents.

## After sandbox works

Tell the user what remains before real money: production account + keys, re-subscribing
webhooks in production, and a safe first transaction. Use the `sezzle-go-live` skill when
they are ready.

## Source docs

- /docs/guides/in-store
- /docs/api/core/sessions/postv2session
- /docs/api/core/orders (capture, refund, release, delete checkout, get order)
- /docs/api/core/webhooks/infov2webhooks
- /docs/api/test-cards • /docs/api/environments
