---
name: sezzle-mobile-sdk
description: >
  Integrate Sezzle checkout and promotional messaging into a native iOS or Android app
  with the Sezzle Mobile SDK — install, server-driven or SDK-created sessions, launching
  checkout in the system browser or an in-app WebView, handling complete/cancel/failure
  callbacks, callback URL schemes, and capturing the order from your backend. Use when:
  "add Sezzle to my iOS app", "integrate the Sezzle Android SDK", "SezzleSDK.startCheckout",
  "SezzlePromotionalView", "pay with Sezzle in our mobile app", "Sezzle in-app checkout".
  For web checkouts use sezzle-checkout instead.
metadata:
  author: Sezzle
  version: "1.0.0"
---

# Integrate the Sezzle Mobile SDK

Every path shares one spine: create a session → the SDK presents checkout → the app gets a
result → **your backend** confirms and settles the order. The SDK never captures, refunds or
holds a private key; settling is the same REST API web merchants use.

## Rules

- **Never put the private key (`sz_pr_…`) or a bearer token in the app.** Session creation
  with a bearer token, capture, refund and release all run on the merchant's backend.
- **Sessions the SDK creates default `order.intent` to `AUTH`**, the opposite of the REST API's
  `CAPTURE` default. An `AUTH` order must be captured before the authorization expires (default 30
  minutes), or the amount is released.
- **Use `https`, `http` or `sezzle-sdk` for callback URLs.** Sezzle allows only those and a
  short list of partner app schemes, and can reject a session that uses any other scheme with a
  400. Use the URLs in § "Callback URLs".
- **Do not invent API names.** Everything below matches SDK 1.3.0 (iOS and Android). If a
  call you need isn't here, read the guide pages under "Before starting" rather than guessing.

## Pick the flow first

Use the interactive question tool. Default to server-driven when the merchant has a backend
that already owns orders, which is the usual case.

| | Server-driven (default) | SDK creates the session |
|---|---|---|
| Who calls `POST /v2/session` | Your backend, with a bearer token | The SDK, with the public key |
| Key on the device | None, `configure` is not called | Public key only |
| Who holds `order.uuid` | Your backend, from the 201 | The app, from the completion callback |
| App entry point | `startCheckout(checkoutURL:completeURL:cancelURL:…)` | `startCheckout(_ checkout: SezzleCheckout, …)` |

Both flows need a backend to capture, so neither is backend-free.

**Only a public key?** `POST /v2/authentication` returns 400 `Missing: private_key`, so
server-driven sessions, order verification and capture can't run. You can still build the
SDK-created flow and test launch and callbacks, but any order a shopper approves stays an
uncaptured `AUTH` and is released when the authorization expires. Say so; don't stub capture.

## Before starting

1. **An approved Sezzle account and API keys for the target environment.** If the user has no
   account, stop and use the `sezzle-prototype-sandbox` skill. Only an Admin dashboard user can
   generate keys, so ask rather than guess. Keep keys in backend config (`.env`, gitignored).
2. **Backend context:** this skill reuses `sezzle-checkout` for authentication, the session
   schema, intent, capture and order management, and `sezzle-webhooks` for async confirmation.
   Follow them for anything backend-side not spelled out here.
3. **Optional reference:** with the `sezzle-docs` MCP tool `query_docs_filesystem_sezzle`
   (install with `npx add-mcp https://docs.sezzle.com/mcp --name sezzle-docs`), read
   `/docs/guides/mobile/ios.mdx`, `/docs/guides/mobile/android.mdx` and
   `/docs/api/core/sessions/postv2session.mdx`. Not required to build a working integration.

| | Sandbox | Production |
|---|---|---|
| API base (backend) | `https://sandbox.gateway.sezzle.com/v2` | `https://gateway.sezzle.com/v2` |
| SDK environment | `.sandbox` / `SezzleEnvironment.SANDBOX` | `.production` / `SezzleEnvironment.PRODUCTION` (the default) |

## Step 1: Install

**iOS** (iOS 15+, Xcode 16+). Swift Package Manager: add
`https://github.com/sezzle/sezzle-merchant-sdk-ios.git`, "Up to Next Major Version" from
`1.3.0`, product `SezzleMerchantSDK`. Or CocoaPods: `pod 'SezzleMerchantSDK', '~> 1.3'`.
No Info.plist URL type or extra permission is needed.

**Android** (minSdk 23, compileSdk 35+, Kotlin 2.1+ or Java), from Maven Central:

```kotlin
dependencies { implementation("com.sezzle:sezzle-merchant-sdk:1.3.0") }
```

No intent-filter is needed for the default `sezzle-sdk` callbacks. Packages: `com.sezzle.sdk`
(`SezzleSDK`, `SezzleCheckoutListener`, `SezzleCheckoutResult`), `com.sezzle.sdk.models`
(`SezzleCheckout`, `SezzleOrder`, `SezzleEnvironment`, `SezzleCheckoutMode`, `SezzleError`, …),
`com.sezzle.sdk.checkout` (`SezzleCheckoutContract`), `com.sezzle.sdk.promotional`
(`SezzlePromotionalView`, …).

## Step 2: Backend endpoints

Authenticate as in `sezzle-checkout` Step 1 (`POST /v2/authentication`, bearer token, 120-minute
expiry). Then build what your flow needs:

**Server-driven: create the session** when the shopper taps "Pay with Sezzle", not on screen
load. `checkout_url` expires about 30 minutes after creation and the amount is frozen.

```
POST /v2/session
Authorization: Bearer {token}
{
  "complete_url": { "href": "sezzle-sdk://checkout/confirmed?ref=ord-12345" },
  "cancel_url":   { "href": "sezzle-sdk://checkout/cancelled?ref=ord-12345" },
  "order": {
    "intent": "AUTH",
    "reference_id": "ord-12345",
    "description": "Order ord-12345",
    "order_amount": { "amount_in_cents": 4999, "currency": "USD" },
    "checkout_mode": "redirect"
  }
}
```

Store `order.uuid` against `ord-12345` and return `order.checkout_url`, `complete_url.href` and
`cancel_url.href` to the app. Field rules (required `description`, cents, `reference_id`
characters, `customer`/`items`, the 400 `location` body) are in `sezzle-checkout` Step 3; keep
`checkout_mode` as `redirect`, the only mode the SDK uses.

**Both flows: confirm the order** in an endpoint the app calls on completion.

- Server-driven: the app sends your `reference_id`; look up the stored `order.uuid`.
- SDK-created: the app sends `orderUUID` and your `reference_id`. **The UUID came from a
  device, so verify it first:** `GET /v2/order/{order_uuid}` must return your `reference_id`, the
  expected `order_amount`, and an `authorization` with `approved: true`.

Then `POST /v2/order/{order_uuid}/capture` with
`{ "capture_amount": { "amount_in_cents": 4999, "currency": "USD" } }`. 200 is captured. 422
`already_completed` means it can't be captured any more: usually because it already was, but
also when the authorization expired or was released, so `GET` the order and confirm
`authorization.captures` covers the amount before treating it as paid. Any other 422 (`invalid`,
"Order not authorized") is a real failure. Refund, release and reauthorize are `sezzle-checkout`
Step 5.

For a backup signal, subscribe to `order.authorized` for `AUTH` (`order.captured` for `CAPTURE`)
with `sezzle-webhooks`. The payload identifies the order by `data.uuid` only, with no
`reference_id`; in the SDK-created flow, `GET` the order to read it if the app died before
calling you.

## Step 3: Configure (SDK-created flow only)

Once at startup, before any checkout. Skip this entirely for server-driven.

```swift
// iOS: AppDelegate didFinishLaunching, or the SwiftUI App's init()
SezzleSDK.shared.configure(publicKey: "sz_pub_...", environment: .sandbox)
```
```kotlin
// Android: Application.onCreate(). Register the class with android:name in the manifest.
SezzleSDK.configure(publicKey = "sz_pub_...", environment = SezzleEnvironment.SANDBOX)
```

Read the key from build config (an untracked `Secrets.swift`, `local.properties` →
`BuildConfig`), not a string literal in source control. A sandbox key with the production
environment fails with `apiError` 401.

## Step 4: Launch checkout

`mode` defaults to the system browser (`ASWebAuthenticationSession` / Chrome Custom Tab), which
is recommended: it shares the shopper's existing Sezzle login. `.webView` / `WEB_VIEW` keeps the
shopper in-app. Awaiting your backend before calling `startCheckout` is fine on mobile; there is
no popup blocker.

**iOS.** `import SezzleMerchantSDK`. `startCheckout` needs a presenting `UIViewController` and a
delegate object (`SezzleCheckoutDelegate` is `@MainActor` and class-bound).

```swift
final class CheckoutCoordinator: NSObject, SezzleCheckoutDelegate {
    func checkoutDidComplete(result: SezzleCheckoutResult) { /* call your confirm endpoint */ }
    func checkoutDidCancel() { /* back to cart */ }
    func checkoutDidFail(error: SezzleError) { /* .browserDismissed → treat as cancel */ }
}

// Server-driven: values returned by your backend in Step 2
SezzleSDK.shared.startCheckout(
    checkoutURL: URL(string: session.checkoutURL)!,
    completeURL: URL(string: session.completeURL)!,
    cancelURL: URL(string: session.cancelURL)!,
    from: viewController, delegate: coordinator, mode: .systemBrowser)

// SDK-created
let checkout = SezzleCheckout(
    customer: SezzleCustomer(email: "jane@example.com", firstName: "Jane", lastName: "Doe"),
    order: SezzleOrder(
        referenceId: "ord-12345",
        description: "Order ord-12345",
        amount: SezzleAmount(amountInCents: 4999, currency: "USD"),
        intent: .auth))
SezzleSDK.shared.startCheckout(checkout, from: viewController, delegate: coordinator, mode: .systemBrowser)
```

In SwiftUI, keep the coordinator in `@State` and present from the top view
controller:

```swift
@MainActor func topViewController() -> UIViewController? {
    let scene = UIApplication.shared.connectedScenes
        .compactMap { $0 as? UIWindowScene }.first { $0.activationState == .foregroundActive }
    var top = scene?.keyWindow?.rootViewController
    while let presented = top?.presentedViewController { top = presented }
    return top
}
```

**Android.** `activity` must be a `ComponentActivity` (`AppCompatActivity` and Compose's
`ComponentActivity` both qualify).

```kotlin
private val checkoutListener = object : SezzleCheckoutListener {
    override fun onCheckoutComplete(result: SezzleCheckoutResult) { /* call your confirm endpoint */ }
    override fun onCheckoutCancel() { /* back to cart */ }
    override fun onCheckoutError(error: SezzleError) { /* BrowserDismissed → treat as cancel */ }
}

// Server-driven, system browser (listener)
SezzleSDK.startCheckout(
    checkoutUrl = session.checkoutUrl,                 // String, as returned
    completeUrl = Uri.parse(session.completeUrl),
    cancelUrl = Uri.parse(session.cancelUrl),
    activity = this, listener = checkoutListener,
    mode = SezzleCheckoutMode.SYSTEM_BROWSER)

// SDK-created: same call shape with a SezzleCheckout
val checkout = SezzleCheckout(
    customer = SezzleCustomer(email = "jane@example.com"),
    order = SezzleOrder(referenceId = "ord-12345", description = "Order ord-12345",
        amount = SezzleAmount(amountInCents = 4999, currency = "USD")))
SezzleSDK.startCheckout(checkout = checkout, activity = this, listener = checkoutListener)
```

For `WEB_VIEW` on Android use the lifecycle-safe launcher instead: the listener can lose the
result if the host activity is recreated mid-checkout. The launcher is WebView-only.

```kotlin
// A field, or rememberLauncherForActivityResult in Compose: register before STARTED
private val sezzleLauncher = registerForActivityResult(SezzleCheckoutContract()) { result ->
    when (result) {
        is SezzleCheckoutContract.Output.Complete -> confirm(result.orderUuid, result.callbackUrl)
        SezzleCheckoutContract.Output.Cancel -> showCart()
        is SezzleCheckoutContract.Output.Error -> showError(result.code, result.message)
    }
}

SezzleSDK.startCheckoutForResult(launcher = sezzleLauncher,
    checkoutUrl = session.checkoutUrl, completeUrl = Uri.parse(session.completeUrl),
    cancelUrl = Uri.parse(session.cancelUrl), onError = { showError(it) })
// SDK-created: startCheckoutForResult(launcher = sezzleLauncher, checkout = checkout, onError = { … })
```

`onError` is required and reports only pre-launch failures; everything after launch arrives
through the launcher.

## Step 5: Handle the result

| What happened | iOS delegate | Android listener | Android launcher |
|---|---|---|---|
| Shopper completed | `checkoutDidComplete(result:)` | `onCheckoutComplete(result)` | `Output.Complete` |
| Shopper chose cancel on Sezzle's page | `checkoutDidCancel()` | `onCheckoutCancel()` | `Output.Cancel` |
| Shopper closed the sheet (X, swipe, back) | `checkoutDidFail(.browserDismissed)` | `onCheckoutError(BrowserDismissed)` | `Output.Cancel` |
| Session creation failed (SDK-created) | `.apiError(statusCode:message:)`, `.networkError` | `ApiError`, `NetworkError` | `onError` |
| `configure` missing (SDK-created) | `.notConfigured` | `NotConfigured`, also for a non-`ComponentActivity` | `onError(NotConfigured)` |
| Page failed to load (WebView mode) / unexpected redirect | `.networkError`, `.invalidResponse` | `NetworkError`, `InvalidResponse` | `Output.Error` (`code`) |

- **On complete,** call your confirm endpoint. SDK-created: `result.orderUUID` (launcher:
  `orderUuid`) is set and `callbackURL` is nil. Server-driven: `orderUUID` is nil and
  `callbackURL` is the full URL Sezzle redirected to, so you can read `ref` from its query, or
  just send the `reference_id` the app already holds.
- **Treat a dismissal as a cancel**, not an error. The shopper is back on your cart; create a
  new session for the next attempt rather than reopening the old `checkout_url`.
- **iOS system browser first shows a "'App' Wants to Use 'sezzle.com' to Sign In" alert.** That
  is the OS consent prompt; Cancel on it arrives as `checkoutDidFail(.browserDismissed)`.
- Callbacks arrive on the main thread, at most once per checkout. On Android's listener path a
  recreated activity or a killed process can drop the result.

## Callback URLs and URL schemes

The SDK finishes when checkout navigates to `completeURL` or `cancelURL`, matching scheme +
host + path (the query may differ). In the SDK-created flow it uses its own
`sezzle-sdk://checkout/confirmed` and `…/cancelled`; nothing to set up. Server-driven:

| Mode | Use |
|---|---|
| System browser, iOS and Android | `sezzle-sdk://checkout/<path>` |
| WebView, either platform | `sezzle-sdk://checkout/<path>` or an `https` URL on your domain |

- Give complete and cancel **different paths** on the same scheme and host.
- Put an opaque reference in the query, never PII or tokens; the URL passes through the browser.
- Your own scheme (`yourapp://`) needs Sezzle to allowlist it first, and on Android an
  intent-filter on `com.sezzle.sdk.checkout.SezzleRedirectActivity` for system-browser mode.
  Use `sezzle-sdk` unless the merchant already has that allowlisting.
- **Android system browser: every app with the SDK registers `sezzle-sdk://checkout`.** A shopper
  with two such apps installed gets an "Open with" chooser on return and can pick the wrong one.
  Tell the merchant. `WEB_VIEW` avoids it; in the server-driven flow so does an allowlisted
  scheme of their own (the SDK-created flow always uses `sezzle-sdk`).

## Promotional messaging (optional)

```swift
let promo = SezzlePromotionalView(amountInCents: 4999, presentingFrom: self)  // tap opens the info modal
promo.update(amountInCents: 14999)
```
```kotlin
val promo = SezzlePromotionalView(context = this, amountInCents = 4999)
promo.setPresenter(this)        // without this, taps do nothing
promo.update(amountInCents = 14999)
```

Hidden below $35 and above $2,500 by default; 4 payments below $50, 5 at $50 and up. Android uses
`GONE` (zero height, so the layout shifts); iOS sets `isHidden`, which collapses only inside a
`UIStackView`. Long-term monthly messaging (defaults $100–$40,000) appears only with a widget config:
iOS `SezzlePromotionalView(amountInCents:widgetConfig: SezzleWidgetConfig(longTermConfig: SezzleLongTermConfig()), presentingFrom:)`,
Android `SezzlePromotionalView(context = this, amountInCents = 4999, widgetConfig = SezzleWidgetConfig(longTermConfig = SezzleLongTermConfig()))`. It needs no key and no `configure`. In SwiftUI or Compose wrap it in
`UIViewRepresentable` / `AndroidView`.

## Logout: clear the WebView session

In `.webView` / `WEB_VIEW` mode the shopper's Sezzle login persists in the app's WebView storage
across checkouts. If the app supports logout or account switching, call
`SezzleSDK.shared.clearWebViewData()` (iOS, optional completion) / `SezzleSDK.clearWebViewData()`
(Android) in the logout flow, or the next user can land in the previous user's Sezzle session.

## Step 6: Test in sandbox

Completing checkout needs a shopper login and an OTP, so don't report success after a 201. Say
which half you verified.

**Verify unaided:**
- Both apps build, and the backend's `POST /v2/authentication` and `POST /v2/session` succeed
  (201 with `order.uuid` and `order.checkout_url`).
- `GET /v2/order/{order_uuid}` before checkout returns `checkout_status: active` and **no**
  `authorization`; a capture then returns 422 `invalid`. That proves the stored UUID and that
  your backend treats `invalid` as a failure.
- Tapping "Pay with Sezzle" opens the Sezzle checkout page in each mode you ship. Android's
  checkout WebView sets `FLAG_SECURE`, so screenshots are black; confirm it with
  `adb shell uiautomator dump` instead.
- Closing it reaches your dismissal handling, and a second tap opens checkout again.
- SDK-created only: `startCheckout` without `configure` reaches `notConfigured` / `NotConfigured`.

**Human handoff for the rest.** Ask the user to complete a checkout on the simulator or
emulator: total $20–$2,500, US/CA IP, any validly formatted US/CA shopper phone (nothing is
sent to it), OTP `123123`, test card `4242 4242 4242 4242`.
Then confirm the completion callback fired and that your backend captured the order (on
`already_completed`, that the order's `GET` shows the capture). `sezzle-prototype-sandbox` covers
the test data in full.

## Common pitfalls

- **A second `startCheckout` while one is open is silently ignored**, with no callback. Disable
  the button until a terminal callback arrives; don't add a retry.
- **Android device with no browser:** system-browser mode needs an installed browser. With none,
  the launch crashes the app in the SDK-created flow, where try/catch can't reach it; in the
  server-driven flow, catching it means retries are ignored. Use `WEB_VIEW`, or check
  `intent.resolveActivity(packageManager)` for an `https` URL before choosing `SYSTEM_BROWSER`
  (on Android 11+ this needs a manifest `<queries>` entry for `VIEW` with scheme `https`, or it
  returns null). On emulators, use a Google APIs or Google Play image.
- **Local http backend in a test app:** Android blocks cleartext traffic by default (the emulator
  reaches the host at `10.0.2.2`) and iOS ATS blocks http. Allow it for debug builds only, or use https.
- **Android system browser returns to a second copy of your screen.** Give the host activity
  `android:launchMode="singleTask"`, or use `WEB_VIEW`. Without it, the shopper lands on a new
  copy while your listener updates the hidden original. `singleTask` also closes activities
  opened on top of it.
- **iOS: complete and cancel URLs on different schemes** — the system browser only intercepts the
  `completeURL` scheme, so a cancel on another scheme never returns. `https` callbacks fail in
  `.systemBrowser` (a debug-build warning is printed); use `.webView` for those.
- **Missed capture window:** the SDK-created flow is `AUTH` unless you pass `intent: .capture` /
  `SezzleIntent.CAPTURE`. Capture on the confirm call, not in a later batch job.
- **Reserved metadata (SDK-created flow):** the SDK adds its own `_`-prefixed keys to
  `order.metadata`; don't use that prefix. An omitted `description` becomes `"Mobile SDK Order"`.
- **Pass `checkout_url` unmodified;** the SDK adds its own parameters.

## After sandbox works

Switch the app to the production environment and key, and the backend to production keys and
webhooks. Use `sezzle-go-live` for the cutover.

## Source docs

- `/docs/guides/mobile/introduction.mdx`, `/docs/guides/mobile/ios.mdx`, `/docs/guides/mobile/android.mdx`
- SDK source and example apps: `github.com/sezzle/sezzle-merchant-sdk-ios`,
  `github.com/sezzle/sezzle-merchant-sdk-android`
