---
name: sezzle-virtual-card
description: >
  Accept Sezzle as a credit card transaction through your existing payment processor
  (Stripe, Cybersource, Braintree) using Sezzle Virtual Card — no changes to your
  payment stack. Covers the Virtual Card SDK, manual postMessage integration, card
  sessions, and the tokenized card-data flow. Use when: "accept Sezzle through
  Stripe/Braintree", "Sezzle virtual card integration", "create a card session",
  "get virtual card data by token". US-only today. For a standard
  Sezzle-hosted checkout use sezzle-checkout instead.
metadata:
  author: Sezzle
  version: "1.0.0"
---

# Integrate Sezzle Virtual Card

## How it works

Sezzle issues the shopper a single-use virtual card funded by their Sezzle plan; your existing
card form and processor charge it like any other card. Your systems touch real card data — the
integration choice below decides how much of it, and therefore your PCI scope.

## Rules

Follow these exactly. They cover the failure modes that are expensive or irreversible.

- **Never retry `GET /v2/session/card/token/{token}`.** The token is single-use: the first
  successful read consumes it, and a second call fails permanently. If the read succeeds but
  something downstream fails, you cannot re-fetch the card. Do not wrap this call in a retry
  loop, a generic HTTP-retry client, or a resilience decorator. Read once, hold the response in
  memory, charge, discard.
- **Never log, print, or persist card data.** No `console.log(response.data)`, no debug dumps,
  no writing PAN/CVV to storage or analytics. This applies to the SDK's `onComplete` payload
  and to the manual flow's `postMessage` payload alike, including while debugging.
- **Do not invent fields.** If a value you need is not listed below, stop and ask rather than
  assuming a plausible name. In particular, do not expect a session identifier in `onComplete`.
- **Do not work around a missing origin allowlist.** If checkout creation fails because the
  origin is not allowlisted, that is a Sezzle-side provisioning step — tell the user to request
  it, do not switch modes or change the origin to something that happens to pass.

## Before starting

Prerequisites, in order:

1. **An approved Sezzle account.** Sandbox and production are separate; credentials are not
   transferable. If the user has no account yet, stop and use the `sezzle-prototype-sandbox`
   skill — it covers sign-up, the manual application approval request, and key generation.
2. **API keys** (`sz_pub_…`, `sz_pr_…`) from the dashboard matching the environment you target.
3. **Your origin allowlisted by Sezzle.** Virtual Card checkout creation fails until Sezzle
   allowlists the exact origin of the page hosting the checkout, per environment. This is not
   self-service: the user must ask their Account Manager, supplying their Merchant UUID and the
   domains for sandbox and production. Treat a failure here as a blocker to surface, not to
   route around.
4. **US only.** Virtual Card is available in the United States today; Canada is listed as
   coming soon.
5. For `iframe` mode, `*.sezzle.com` in the host page's Content Security Policy, and the
   domain enabled for iframe by Sezzle (same request as step 3).

For the full reference — every SDK button attribute, the complete schemas — read
`/docs/guides/virtual/introduction.mdx`, `/docs/guides/virtual/automated.mdx`,
`/docs/guides/virtual/manual.mdx` and
`/docs/api/core/sessions/virtual/postv2sessioncard.mdx` with the `sezzle-docs` MCP tool
`query_docs_filesystem_sezzle` (install it with
`npx add-mcp https://docs.sezzle.com/mcp --name sezzle-docs`). Not required: everything needed
to build a working integration is below.

## Choose an integration

Decide by rule, not preference:

- **Default to the Virtual Card SDK with `card_response_format: "token"`.** The SDK hosts the
  checkout and returns a token; your server exchanges it for card data. PAN and CVV never touch
  the browser, which keeps the merchant's frontend out of PCI scope.
- **Use the manual integration only** when the merchant has stated a constraint that rules the
  SDK out (e.g. they cannot load third-party script, or they must control the checkout window
  themselves). Unless you request a token, the manual flow delivers the full PAN and CVV to frontend JavaScript, putting
  their frontend in PCI DSS scope. Confirm the merchant accepts that before building it.

Both flows are documented below. Build one.

## Money units

`amount_in_cents` and every `price.amount_in_cents` are **cents**. Convert at the call site.

---

## Virtual Card SDK (default)

### Step 1: Load the SDK

```html
<script
  type="text/javascript"
  src="https://checkout-sdk.sezzle.com/checkout.min.js"
></script>
```

### Step 2: Configure the checkout

```javascript
const checkoutSdk = new Checkout({
  mode: "popup",          // popup | iframe — see note below
  publicKey: "sz_pub_...",
  apiMode: "sandbox",     // sandbox | live
  isVirtualCard: true,    // required
});
```

`mode` is **`popup` or `iframe` only**. Redirect is not usable for Virtual Card: card data comes
back through window messaging, which needs a popup or iframe to message. `popup` is the default
choice; use `iframe` when popups are blocked (webviews, embedded browsers), which requires the
CSP entry and Sezzle-side enablement from the prerequisites.

### Step 3: Render the Sezzle button

```html
<div id="sezzle-smart-button-container" style="text-align: center"></div>
```

```javascript
checkoutSdk.renderSezzleButton("sezzle-smart-button-container");
```

The container element supports presentation attributes (`templateText` with a `%%logo%%`
placeholder, `borderType`, `customClass`, padding/width/height). Add them only if asked.

### Step 4: Initialize and start the checkout

The SDK creates the card session itself when the shopper clicks the button. **Do not create a
session server-side for this flow** — there is no way to hand a pre-created session to the SDK.

```javascript
checkoutSdk.init({
  onClick: function (event) {
    event.preventDefault();
    checkoutSdk.startCheckout({
      checkout_payload: {
        amount_in_cents: 10000,
        currency: "USD",
        merchant_reference_id: "ord-12345",   // alphanumeric, dashes, underscores
        card_response_format: "token",        // keeps card data out of the browser
        items: [
          {
            name: "Blue tee",
            sku: "sku123456",
            quantity: 1,
            price: { amount_in_cents: 10000, currency: "USD" },
          },
        ],
      },
    });
  },
  onComplete: function (response) {
    // response.data.card.token and response.data.holder — send both to your server, do not log them
  },
  onCancel: function () {
    // shopper exited before completing
  },
  onFailure: function () {
    // checkout failed to load or errored
  },
});
```

Notes that matter:

- `origin` and `mode` are supplied by the SDK from its own config. Do not put them in
  `checkout_payload`.
- Shopper prefill is **not applied on the SDK path**. Do not add a `customer` object expecting
  the checkout to be pre-filled with it.
- The session is short-lived and its lifetime is configured by Sezzle. Start the checkout from
  the shopper's click; never store the checkout URL to reuse later.

With `card_response_format: "token"`, `onComplete` receives:

```json
{
  "card": { "token": "abc12345" },
  "holder": {
    "email": "...", "phone": "...", "firstName": "...", "lastName": "...",
    "address1": "...", "address2": "...", "city": "...", "state": "MN",
    "country": "US", "postalCode": "..."
  }
}
```

Send `holder` to your server with the token and use it as the billing address for the processor
charge (AVS). The token exchange below returns no address.

Without it, `onComplete` receives raw card data instead — see the manual flow's payload shape.
Prefer the token.

### Step 5: Exchange the token server-side

```
GET /v2/session/card/token/{token}
Authorization: Bearer {auth_token}
```

The bearer token comes from `POST /v2/authentication` with the merchant's public and private
keys (server-side only — see the sezzle-checkout skill).

Response:

| Field | Notes |
|---|---|
| `pan` | 16-digit card number |
| `cvv_number` | security code |
| `expiration_date` | 4-character `MMYY` string, e.g. `0228`. Split it (month `02`, year `28`) for your processor. |
| `first_name` | name on the card |
| `last_name` | name on the card |

Card data is available for 24 hours, requires authentication, and the token is consumed by the
first successful read. Re-read the "Rules" section before writing this call.

### Step 6: Charge through your processor

Run the card through Stripe / Cybersource / Braintree exactly as you would a shopper-entered
card. Nothing Sezzle-specific here.

---

## Manual integration

Only when the SDK is ruled out. Without a token, card data arrives in frontend JavaScript — full PCI scope.

### Step 1: Create the card session (server-side)

```
POST /v2/session/card
Authorization: Bearer {auth_token}

{
  "amount_in_cents": 10000,
  "currency": "USD",
  "origin": "https://merchant.example",   // window origin of the host page; must be allowlisted
  "mode": "popup",                        // popup | iframe
  "merchant_reference_id": "ord-12345",
  "items": [ ... ],
  "customer": { ... }                     // optional, prefills shopper info
}
```

`origin`, `mode`, `merchant_reference_id`, `amount_in_cents` and `currency` are required.
`items` uses the same line-item shape as `POST /v2/session`. `customer` takes
`first_name`, `last_name`, `email`, `phone` and a nested `billing_address`.

Add `"card_response_format": "token"` to receive `card.token` instead of card data, then
exchange it server-side as in the SDK flow's Step 5.

Response (201):

```json
{
  "uuid": "fadbc642-05a4-4e38-9e74-80e325623af9",
  "dashboard_url": "https://checkout.sezzle.com?id=..."
}
```

### Step 2: Open `dashboard_url` in the mode you requested

Popup or iframe, matching the `mode` you sent.

### Step 3: Listen for the card payload

Derive the expected origin from the `dashboard_url` you received rather than hardcoding a
hostname: the host differs between sandbox and production, and an origin that doesn't match
discards every message with nothing logged.

In popup mode, two things are on you. When the shopper clicks Complete, the checkout pings its
opener and waits for `{ ack: true }`; without the reply the shopper gets an error and the
order does not complete. After the card payload arrives, the popup stays on its processing
screen until you close it. In iframe mode, remove the iframe instead: `close()` on its window
does nothing.

```javascript
// sezzleOrigin = new URL(dashboardUrl).origin, from the session you created
// mode = the mode you sent when creating the session
// sezzleWindow = the popup returned by window.open(dashboardUrl, ...), or sezzleIframe.contentWindow
window.addEventListener("message", function (event) {
  if (event.origin !== sezzleOrigin) return;

  const data = event.data || {};
  if (data.event_data && data.event_data.complete_initiated) {
    sezzleWindow.postMessage({ ack: true }, sezzleOrigin);
    return;
  }
  if (data.szl_source !== "v_card") return;

  if (!data.card || !data.holder) {
    // shopper did not complete the virtual card session
    return;
  }

  if (mode === "popup") {
    sezzleWindow.close();
  } else {
    sezzleIframe.remove();
  }

  if (data.card.token) {
    // token mode: your server exchanges it once via GET /v2/session/card/token/{token}
    sendTokenToServer(data.card.token, data.holder);
    return;
  }

  // Raw card mode (frontend in PCI scope). Send straight to your processor. Do not log, do not store.
  submitToPaymentGateway({
    card: {
      number: data.card.pan,
      cvv: data.card.cvv,
      expiryMonth: data.card.expiryMonth,   // "02"
      expiryYear: data.card.expiryYear,     // "28"
      firstName: data.card.firstName,
      lastName: data.card.lastName,
    },
    billingAddress: {
      email: data.holder.email,
      phone: data.holder.phone,
      firstName: data.holder.firstName,
      lastName: data.holder.lastName,
      address1: data.holder.address1,
      address2: data.holder.address2,
      city: data.holder.city,
      state: data.holder.state,
      country: data.holder.country,
      postalCode: data.holder.postalCode,
    },
  });
});
```

### Step 4: Charge through your processor

Same as the SDK flow.

---

## Testing

- Sandbox gateway: `https://sandbox.gateway.sezzle.com`, with sandbox dashboard API keys.
- Shopper OTP in sandbox is always `123123`. Test values: `/docs/api/test-cards`.
- **Sandbox cannot exercise your payment processor.** Sezzle's sandbox will issue a test virtual
  card, but charging it is between you and your processor's own test environment. Plan the
  end-to-end verification accordingly.

## Pitfalls

- **Single-use token.** Covered in "Rules" — the most common way to lose an order.
- **24-hour window** on card data availability after checkout completes.
- **`expiration_date` is `MMYY`** — parse it as a 4-character string, not as a date.
- **`origin` must match exactly** and must be allowlisted by Sezzle, per environment.
- **Redirect mode does not work** for Virtual Card.
- **US only** today.
- **Don't confuse products:** `POST /v2/session/card` is Virtual Card only — not the direct
  integration flow (`POST /v2/session`), which the sezzle-checkout skill covers.

## After sandbox works

Remind the user: production needs its own keys AND its own domain allowlisting request —
sandbox allowlisting does not carry over. Use the `sezzle-go-live` skill for the full
cutover.

## Wrapping up

Keep the final message short and plain: what was built, what the user must do before trying it
(usually the origin allowlist request), and a few quick steps to try it. Save the longer test
list and go-live notes for when the user asks.

## Source docs
- /docs/guides/virtual/* • /docs/api/core/sessions/virtual/* • /docs/api/test-cards
