# Xizor — Full documentation > Xizor is the AI-native subscription platform for mobile apps: a native StoreKit 2 payment SDK (Swift + Expo/React Native), server-hosted paywalls authored as web code that go live on save, complete subscription analytics, and a built-in MCP server so AI agents can read every stat and edit every paywall. This file concatenates every page of https://xizor.dev/docs for LLM consumption. --- # Overview URL: https://xizor.dev/docs Xizor is the AI-native subscription platform for mobile apps. It gives you four things that normally require four different products: - **Native StoreKit 2 payments** — XizorKit (Swift) and @xizor/expo (React Native) handle purchases, restores, and server-side receipt verification. Entitlements are always computed on the server from cryptographically verified transactions. - **Server-hosted paywalls, authored as web code** — paywalls are self-contained web documents edited in the dashboard (or by an AI agent) and served to the SDKs as a page rendered in a WebView, with StoreKit and analytics staying native via a bridge. Publishing a change makes it live at the very next presentation, without an app update. - **Complete subscription analytics** — MRR, revenue, churn, trial conversion, LTV, ARPU, cohort retention, and per-paywall funnels, computed from App Store Server Notifications and verified transactions. - **A built-in MCP server** — connect Claude, ChatGPT, or Gemini with OAuth 2.1 and your agent can read every stat and _write_: draft, localize, and publish paywalls from a chat prompt. ## How it fits together ```text ┌────────────────────────┐ ┌───────────────────────────────┐ │ Your app │ │ App Store │ │ XizorKit (Swift) │ │ Server Notifications V2 │ │ @xizor/expo (RN) │ └──────────────┬────────────────┘ └──────────┬─────────────┘ │ signed JWS │ identify / transactions / │ │ entitlements / paywall / events │ ▼ ▼ ┌──────────────────────────────────────────────────────────────┐ │ api.xizor.dev — verification, entitlements, paywall delivery │ └──────────┬───────────────────────────────────┬───────────────┘ │ │ webhooks (HMAC-signed) ▼ ▼ ┌─────────────────────────┐ ┌────────────────────────┐ │ Xizor platform │ │ Your server (optional) │ │ dashboard.xizor.dev │ └────────────────────────┘ │ mcp.xizor.dev (agents) │ └─────────────────────────┘ ``` Your app talks to `api.xizor.dev` with a publishable API key. Apple talks to the same API via App Store Server Notifications V2, so renewals, refunds, and billing issues keep your data current even while the app is closed. The dashboard and the MCP server sit on top of the same data — what your agent sees is exactly what you see. ## Where to go next | Guide | What you get | | ----------------------------------------------------------------------- | -------------------------------------------------------------------------- | | [iOS quickstart](https://xizor.dev/docs/quickstart-ios) | XizorKit installed, purchases and paywalls working in SwiftUI | | [Expo quickstart](https://xizor.dev/docs/quickstart-expo) | @xizor/expo with provider, hooks, and the WebView paywall renderer | | [Migrate from Superwall](https://xizor.dev/docs/migrate-from-superwall) | Concept mapping, automatic subscriber migration, and the cutover checklist | | [Apple setup](https://xizor.dev/docs/apple-setup) | App Store Connect key + Server Notifications wired to Xizor | | [Paywalls](https://xizor.dev/docs/paywalls) | Placements, versions, save-to-live, and the full document schema | | [Analytics](https://xizor.dev/docs/analytics) | Exact definitions behind every metric | | [MCP server](https://xizor.dev/docs/mcp) | Your subscription business inside Claude, ChatGPT, or Gemini | | [REST API](https://xizor.dev/docs/api) | The full api.xizor.dev reference and webhook verification | | [For coding agents](https://xizor.dev/docs/for-agents) | A copy-paste prompt that integrates Xizor into an app | ## The five-minute version 1. Sign up at [dashboard.xizor.dev](https://dashboard.xizor.dev) and create an app — you get a publishable API key (`xz_pub_…`). 2. Complete the [Apple setup](https://xizor.dev/docs/apple-setup): In-App Purchase key + Server Notifications URL. 3. Install a SDK and call `configure` with your key ([iOS](https://xizor.dev/docs/quickstart-ios) / [Expo](https://xizor.dev/docs/quickstart-expo)). 4. Create a paywall in the dashboard, assign it to a placement (e.g. `onboarding`), and present it from the SDK. 5. Optionally [connect your AI agent](https://xizor.dev/docs/mcp) — and let it run your paywall experiments. Machine-readable indexes of these docs live at [/llms.txt](https://xizor.dev/llms.txt) and [/llms-full.txt](https://xizor.dev/llms-full.txt). --- # iOS quickstart (XizorKit) URL: https://xizor.dev/docs/quickstart-ios XizorKit is the native Swift SDK: StoreKit 2 purchases, server-verified entitlements, and server-hosted paywalls — each paywall is served as a self-contained web page that the SDK renders in a `WKWebView` (presented from SwiftUI or UIKit), while purchases, restore, and analytics stay native via a bridge. It supports iOS 17+, macOS 14+, tvOS 17+, watchOS 10+, and visionOS 1+, and is built with strict concurrency. ## Install XizorKit currently lives inside the Xizor monorepo at [github.com/valewnrt/xizor](https://github.com/valewnrt/xizor), in the `sdks/swift` directory (a dedicated `xizor-swift` repository with tagged releases is planned — this page will be updated when it ships). Because Swift Package Manager resolves URL dependencies from a repository root, add it as a **local package** for now: 1. Clone the repository: `git clone https://github.com/valewnrt/xizor.git` 2. In Xcode: **File → Add Package Dependencies… → Add Local…** and select the `sdks/swift` folder (it contains `Package.swift` for the `XizorKit` library). 3. Add the `XizorKit` product to your app target. Alternatively, reference it from another `Package.swift` via `.package(path: "../xizor/sdks/swift")`. ## Configure Call `Xizor.configure(apiKey:)` once, as early as possible — your publishable key (`xz_pub_…`) is in the dashboard under your app's settings and is safe to embed in the app: ```swift import SwiftUI import XizorKit @main struct MyApp: App { init() { Xizor.configure(apiKey: "xz_pub_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX") } var body: some Scene { WindowGroup { ContentView() } } } ``` `configure` accepts optional parameters: | Parameter | Default | Purpose | | ----------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------ | | `apiBaseURL` | `https://api.xizor.dev` | Override for testing | | `logLevel` | `.warn` | `XizorLogLevel` verbosity | | `salvageDelay` | `2500 ms` | Wait before double-checking a reported purchase cancel (StoreKit sometimes mis-reports) | | `legacyAppAccountToken` | `nil` | Async provider of an existing appAccountToken when [migrating](https://xizor.dev/docs/migrate-from-superwall) from another SDK | On configure, the SDK starts observing StoreKit transaction updates (renewals, refunds, Ask to Buy) and performs an initial entitlement sync. The published `currentEntitlements` property makes the whole SDK usable as a SwiftUI `ObservableObject`. ## Identify users and the appAccountToken Xizor identifies each install with an **appAccountToken** — a UUID generated on-device, persisted across launches, and attached to every StoreKit purchase as `Transaction.appAccountToken`. Because Apple embeds it in the signed transaction, it cryptographically ties App Store purchases (and every later server notification: renewals, refunds, billing retry…) back to the right subscriber, even across reinstalls and restores. The SDK registers it with the backend on launch (`identify()`); you never manage it yourself. Anonymous users work out of the box. To attach your own user id after login, hand the token to your backend and call `POST /v1/subscribers/alias` there with your **secret** key — existing subscriber histories merge automatically ([details](https://xizor.dev/docs/api)): ```swift // After login — bind this install to "user_42" via your backend: let token = await Xizor.shared.appAccountToken try await myAPI.linkSubscriber(token: token, userID: "user_42") // backend: POST https://api.xizor.dev/v1/subscribers/alias (secret key) ``` ## Purchases and entitlements ```swift // Purchase — verification and entitlement computation happen server-side. let result = try await Xizor.shared.purchase(productID: "com.example.pro.yearly") switch result { case .success(let entitlements): print("unlocked:", entitlements) case .cancelled: break // user backed out — not an error case .pending: break // e.g. Ask to Buy — entitlement arrives later } // Gate features: if Xizor.shared.hasEntitlement("pro") { unlockPro() } // Fetch fresh entitlements from the backend: let entitlements = try await Xizor.shared.entitlements() // Restore: try await Xizor.shared.restorePurchases() ``` In SwiftUI, observe `currentEntitlements` directly: ```swift struct ProGate: View { @ObservedObject var xizor = Xizor.shared var body: some View { if xizor.currentEntitlements.contains(where: { $0.identifier == "pro" && $0.isActive }) { ProContent() } else { LockedContent() } } } ``` Each `XizorEntitlement` carries `identifier`, `isActive`, `productId`, `expiresAt`, `willRenew`, `isTrial`, and `state` (`subscribed`, `expired`, `in_grace_period`, `in_billing_retry`, `revoked`, `paused`). Only `subscribed` and `in_grace_period` are active. The SDK sends the signed StoreKit 2 transaction (`jwsRepresentation`) to `POST /v1/transactions`, where the signature chain is verified against Apple's root certificates — entitlements are never computed on-device. ## Present a paywall Paywalls are fetched by **placement** (e.g. `"onboarding"`) — the dashboard decides which paywall a placement shows. Two ways to present: **Imperative** (UIKit presentation, awaits the outcome): ```swift let result = await Xizor.shared.presentPaywall(placement: "onboarding") if case .purchased = result { unlockPro() } ``` **SwiftUI** with `XizorPaywallView` — a self-loading paywall view for `fullScreenCover` or `sheet`: ```swift .fullScreenCover(isPresented: $showPaywall) { XizorPaywallView(placement: "onboarding") { result in if case .purchased = result { unlockPro() } showPaywall = false } } ``` Both return/report a `XizorPaywallResult`: `.purchased(entitlements:)`, `.restored(entitlements:)`, or `.dismissed`. Purchase buttons, restore, dismiss, product selection, and funnel events (`paywall_view`, `purchase_started`, `purchase_completed`, `paywall_dismiss`…) are all handled by the renderer automatically. Paywall documents can trigger `custom` actions (e.g. "continue with limited version"). Handle them via the closure or a delegate: ```swift XizorPaywallView(placement: "onboarding", onCustomAction: { name, payload in if name == "skip" { showPaywall = false } }) // …or globally: Xizor.shared.paywallDelegate = self // XizorPaywallDelegate ``` You can also pass `customVariables:` to fill `{{custom.*}}` placeholders in paywall text. Product variables (`{{product.price}}`, `{{trial.days}}`, …) are filled from StoreKit automatically. Paywall fetches are ETag-cached: the SDK revalidates on every presentation, so an edit published in the dashboard (or by your AI agent) is live at the very next presentation — no app update, no waiting. ## Track custom events ```swift Xizor.shared.track("onboarding_finished", properties: ["step": .int(4)]) ``` Events appear in the dashboard and feed the [analytics](https://xizor.dev/docs/analytics) and paywall funnels. ## Next steps - [Apple setup](https://xizor.dev/docs/apple-setup) — required for server notifications and sandbox testing. - [Paywalls](https://xizor.dev/docs/paywalls) — placements, versions, and the document schema. - [MCP server](https://xizor.dev/docs/mcp) — let your agent edit the paywalls you just wired up. --- # Expo quickstart (@xizor/expo) URL: https://xizor.dev/docs/quickstart-expo `@xizor/expo` is the Xizor SDK for React Native / Expo: StoreKit 2 purchases via [expo-iap](https://github.com/hyodotdev/expo-iap) and a server-driven paywall renderer — the server-hosted paywall is served as a self-contained web page that the SDK loads into a WebView (react-native-webview), while StoreKit purchases and analytics stay native through a bridge. Requirements: Expo SDK 57+, React Native 0.86+, React 19 (New Architecture). iOS first — Android purchase paths throw a clear `platform_not_supported` error until Play Billing lands. ## Install ```sh npx expo install expo-iap react-native-webview expo-web-browser \ expo-secure-store expo-haptics pnpm add @xizor/expo # or npm/yarn ``` All of the above are **peer dependencies** — `npx expo install` picks the versions matching your Expo SDK. `expo-iap` (StoreKit 2), `react-native-webview` (the paywall renderer), and `expo-web-browser` (in-app browser for paywall links) are required; `expo-secure-store` (or `@react-native-async-storage/async-storage`) persists the subscriber token, and `expo-haptics` powers paywall haptic feedback — both optional but recommended. ## You need a development build (no Expo Go) In-app purchases require native StoreKit code that is not part of Expo Go. Build a development client instead: ```sh npx expo run:ios # local dev build # or with EAS: eas build --profile development --platform ios ``` Everything else about your workflow stays the same — fast refresh, Expo Router, etc. Only the container changes. ## Provider and hooks Wrap your app in `XizorProvider` with your publishable API key (`xz_pub_…`, from the dashboard, safe to embed): ```tsx import { XizorProvider, useEntitlements, usePaywall } from "@xizor/expo"; export default function App() { return (
); } function Main() { const { isPro, loading } = useEntitlements(); // isPro = hasEntitlement("pro") const paywall = usePaywall("onboarding"); if (loading) return ; if (!isPro) { return (