{ "openapi": "3.0.3", "info": { "title": "ZeroSettle IAP API", "description": "The ZeroSettle IAP API powers the iOS, Android, and Flutter SDKs. It enables product catalog fetching, web checkout, entitlement management, subscription lifecycle operations, and StoreKit transaction syncing. All endpoints are authenticated via the `X-ZeroSettle-Key` header.", "version": "1.0.0", "contact": { "name": "ZeroSettle Support", "email": "support@zerosettle.io", "url": "https://zerosettle.io" } }, "servers": [ { "url": "https://api.zerosettle.io/v1", "description": "Production" } ], "security": [ { "ApiKeyAuth": [] } ], "tags": [ { "name": "Products", "description": "Fetch the product catalog for your app" }, { "name": "Checkout", "description": "Create checkout sessions and payment intents for web purchases" }, { "name": "Transactions", "description": "Query transaction status and history" }, { "name": "Entitlements", "description": "Query user entitlements (active purchases and subscriptions)" }, { "name": "StoreKit", "description": "Sync and query StoreKit transactions" }, { "name": "Subscriptions", "description": "Manage subscription lifecycle (cancel, pause, resume)" }, { "name": "Cancel Flow", "description": "Configurable cancel flow with retention offers" }, { "name": "Upgrade Offers", "description": "Subscription upgrade/downgrade offers" }, { "name": "User Offer", "description": "Unified offer + subscription-state resolver (SDK 1.2+)" }, { "name": "Migration", "description": "Switch & Save migration tracking" }, { "name": "Events", "description": "Track SDK analytics events" }, { "name": "Restore", "description": "Restore purchases for a user" } ], "paths": { "/iap/products": { "get": { "operationId": "getProducts", "summary": "Get Products", "description": "Fetches the product catalog for your app, including web and StoreKit prices, checkout configuration, trial eligibility, and any active migration campaign. Pass `user_id` to get per-user trial eligibility.", "tags": [ "Products" ], "parameters": [ { "name": "user_id", "in": "query", "required": false, "description": "The user's external ID. When provided, the response includes per-user `is_trial_eligible` on each product.", "schema": { "type": "string" } }, { "name": "demo", "in": "query", "required": false, "description": "**Debug-only.** Surfaces a dashboard-configured campaign as if the requesting user were eligible for it, regardless of their real subscription state. Lets developers preview their tip UI without crafting a specific test account.\n\n- `migration` returns the configured `MigrationCampaign` (Switch & Save) on `config.migration` (and `config.offer` mirror with `flow_type=migration`).\n- `upgrade` returns the configured `UpgradeOfferConfig` (Upgrade & Save) on `config.offer` with `flow_type=upgrade`. Uses the first `upgrade_paths` entry as the preview source/target. `savings_percent` is `0` for demo previews — accurate proration requires a real billing cycle.\n- Honored **only** on test-mode publishable keys (`zs_pk_test_*`). Live keys silently ignore the flag. Impressions are NOT counted on demo requests. Any other value (including the legacy `true`) drops the bypass and the normal user-state resolver runs.", "schema": { "type": "string", "enum": ["migration", "upgrade"] } } ], "responses": { "200": { "description": "Product catalog with checkout configuration", "content": { "application/json": { "schema": { "type": "object", "required": [ "products", "checkout_config" ], "properties": { "products": { "type": "array", "items": { "$ref": "#/components/schemas/Product" } }, "checkout_config": { "$ref": "#/components/schemas/CheckoutConfig" }, "migration_campaign": { "description": "Active Switch & Save migration campaign, or null if none is configured.", "allOf": [ { "$ref": "#/components/schemas/MigrationCampaign" } ], "nullable": true }, "offer": { "description": "Unified offer for the current user. Null if the user is not eligible for any offer. New SDKs prefer this over migration_campaign.", "nullable": true, "oneOf": [ { "$ref": "#/components/schemas/Offer" }, { "type": "null" } ] } } } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "403": { "$ref": "#/components/responses/Forbidden" } } } }, "/iap/payment-intents": { "post": { "operationId": "createPaymentIntent", "summary": "Create Payment Intent", "description": "Creates a Stripe PaymentIntent for native checkout (Apple Pay / Google Pay payment sheet). Returns a `client_secret` for confirming the payment on the client side and a `checkout_url` for fallback browser checkout.", "tags": [ "Checkout" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "product_id" ], "properties": { "product_id": { "type": "string", "description": "The product's `reference_id` (e.g., `com.app.premium`)." }, "external_user_id": { "type": "string", "description": "The user's external ID. Either this or `user_id` is required." }, "user_id": { "type": "string", "description": "Alias for `external_user_id`. Either this or `external_user_id` is required." }, "platform": { "type": "string", "enum": [ "ios", "android", "web" ], "description": "The platform the purchase is being made from." }, "stripe_customer_id": { "type": "string", "description": "Existing Stripe customer ID to attach the payment to." }, "customer_name": { "type": "string", "description": "Optional customer display name to attach to the Stripe customer for this checkout. Also accepts the camelCase alias `customerName`." }, "customer_email": { "type": "string", "description": "Optional customer email to attach to the Stripe customer for this checkout. Also accepts the camelCase alias `customerEmail`." }, "checkout_domain": { "type": "string", "description": "Verified custom subdomain for the app's checkout (e.g., `checkout.example.com`). Configure via the dashboard — see the Custom Checkout Domains guide." }, "checkout_mode": { "type": "string", "enum": [ "native", "browser" ], "description": "Preferred checkout mode. `native` uses the in-app payment sheet; `browser` opens a web checkout page." }, "storekit_subscription_end": { "type": "string", "format": "date-time", "description": "ISO 8601 datetime of the current StoreKit subscription's expiry. Used for Switch & Save migrations to align the trial period." }, "storekit_original_transaction_id": { "type": "string", "description": "The StoreKit original transaction ID for the subscription being migrated." }, "external_purchase_token": { "type": "string", "description": "Apple externalPurchaseToken received from StoreKit at the disclosure-sheet confirmation step. When provided, ZeroSettle uses this token for automatic external-purchase reporting in eligible jurisdictions (EU/EEA under the DMA alternative terms, EEA music streaming, and Japan MSCA on iOS 26.4+). Optional — omit for non-Apple platforms or regions where reporting is not required." }, "ios_version": { "type": "string", "description": "iOS version of the checkout client (e.g., '26.4.1'). Used for JP MSCA regime gating, which requires iOS 26+. Optional." } } } } } }, "responses": { "200": { "description": "Payment intent created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PaymentIntentResponse" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "502": { "$ref": "#/components/responses/UpstreamError" } } } }, "/iap/payment-intents/batch": { "post": { "operationId": "createPaymentIntentBatch", "summary": "Create Payment Intent Batch", "description": "Creates Stripe PaymentIntents for multiple products in a single request. Reduces N sequential requests to 1, with shared Stripe customer/account resolution. Products that fail validation return an error entry instead of failing the entire batch. Maximum 25 products per request.", "tags": [ "Checkout" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "products" ], "properties": { "products": { "type": "array", "description": "Array of products to create payment intents for. Maximum 25 items.", "maxItems": 25, "items": { "type": "object", "required": [ "product_id" ], "properties": { "product_id": { "type": "string", "description": "The product's `reference_id` (e.g., `com.app.premium`)." }, "storekit_subscription_end": { "type": "string", "format": "date-time", "description": "ISO 8601 datetime of the current StoreKit subscription's expiry. Used for Switch & Save migrations." }, "storekit_original_transaction_id": { "type": "string", "description": "The StoreKit original transaction ID for the subscription being migrated." } } } }, "user_id": { "type": "string", "description": "The user's external ID." }, "external_user_id": { "type": "string", "description": "Alias for `user_id`." }, "platform": { "type": "string", "enum": [ "ios", "android", "web" ], "description": "The platform the purchase is being made from." }, "checkout_mode": { "type": "string", "enum": [ "native", "browser" ], "description": "Preferred checkout mode." }, "stripe_customer_id": { "type": "string", "description": "Existing Stripe customer ID to attach payments to." }, "customer_name": { "type": "string", "description": "Optional customer display name to attach to the Stripe customer for this batch. Also accepts the camelCase alias `customerName`." }, "customer_email": { "type": "string", "description": "Optional customer email to attach to the Stripe customer for this batch. Also accepts the camelCase alias `customerEmail`." } } } } } }, "responses": { "200": { "description": "Batch results with per-product success or error", "content": { "application/json": { "schema": { "type": "object", "required": [ "results" ], "properties": { "results": { "type": "array", "description": "Array of results, one per requested product. Each entry is either a successful PaymentIntent response or an error object.", "items": { "type": "object", "properties": { "product_id": { "type": "string", "description": "The product's `reference_id`." }, "client_secret": { "type": "string", "description": "Stripe PaymentIntent client secret. Present on success." }, "transaction_id": { "type": "string", "description": "ZeroSettle transaction ID. Present on success." }, "amount": { "type": "integer", "description": "Charge amount in cents. Present on success." }, "currency": { "type": "string", "description": "ISO 4217 currency code. Present on success." }, "product_name": { "type": "string", "description": "Display name of the product. Present on success." }, "original_amount": { "type": "integer", "description": "Full price before discounts/trials. Present on success." }, "checkout_url": { "type": "string", "description": "Fallback URL for browser-based checkout. Present on success." }, "publishable_key": { "type": "string", "description": "Stripe publishable key for confirming the payment. Present on success." }, "stripe_account": { "type": "string", "nullable": true, "description": "Connected Stripe account ID for BYOS. Null for managed mode." }, "merchant_country": { "type": "string", "nullable": true, "description": "ISO 3166-1 alpha-2 country code for Apple Pay." }, "is_subscription": { "type": "boolean", "description": "Whether this product is a subscription. Present on success." }, "subscription_interval": { "type": "string", "description": "Subscription billing interval (e.g., `month`, `week`). Present for subscriptions." }, "trial_type": { "type": "string", "enum": [ "free_trial", "migration" ], "nullable": true, "description": "Type of trial applied. Omitted when no trial." }, "trial_end": { "type": "integer", "nullable": true, "description": "Unix timestamp when trial ends. Only present with active trial." }, "pending_amount": { "type": "integer", "nullable": true, "description": "Amount billed when trial ends. Only present with active trial." }, "error": { "type": "string", "description": "Error message. Present when this product failed validation." } } } } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "502": { "$ref": "#/components/responses/UpstreamError" } } } }, "/iap/checkout-sessions": { "post": { "operationId": "createCheckoutSession", "summary": "Create Checkout Session", "description": "Creates a Stripe Checkout Session for browser-based checkout. Returns a URL to redirect the user to Stripe's hosted checkout page.", "tags": [ "Checkout" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "product_id" ], "properties": { "product_id": { "type": "string", "description": "The product's `reference_id` (e.g., `com.app.premium`)." }, "external_user_id": { "type": "string", "description": "The user's external ID. Either this or `user_id` is required." }, "user_id": { "type": "string", "description": "Alias for `external_user_id`. Either this or `external_user_id` is required." }, "platform": { "type": "string", "enum": [ "ios", "android", "web" ], "description": "The platform the purchase is being made from." }, "stripe_customer_id": { "type": "string", "description": "Existing Stripe customer ID to attach the payment to." }, "rc_app_user_id": { "type": "string", "description": "RevenueCat app user ID, if using RevenueCat integration." } } } } } }, "responses": { "200": { "description": "Checkout session created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CheckoutSessionResponse" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "502": { "$ref": "#/components/responses/UpstreamError" } } } }, "/iap/transactions/{transaction_id}": { "get": { "operationId": "getTransaction", "summary": "Get Transaction", "description": "Retrieves the status and details of a specific transaction by its ID.", "tags": [ "Transactions" ], "parameters": [ { "name": "transaction_id", "in": "path", "required": true, "description": "The transaction ID (UUID returned from checkout).", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Transaction details", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Transaction" } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/iap/transactions/{transaction_id}/storekit-status": { "patch": { "operationId": "updateStorekitStatus", "summary": "Update StoreKit Status", "description": "Reports a StoreKit subscription state change for a migration transaction. As of 2026-04-27, this is a *trigger to verify* endpoint: when the SDK reports `storekit_status=2` (anything other than active), the backend queries Apple's App Store Server API itself and persists what Apple reports onto the transaction's `storekit_apple_status` and `storekit_subscription_end` fields. Billing-relevant data is never sourced from the client.", "tags": [ "Transactions" ], "parameters": [ { "name": "transaction_id", "in": "path", "required": true, "description": "The transaction ID.", "schema": { "type": "string" } } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "storekit_status" ], "properties": { "storekit_status": { "type": "integer", "enum": [ 1, 2 ], "description": "`1` = active (finished successfully, no further action). `2` = anything-not-active (cancelled, expired, billing retry, grace period, revoked). When `2` is reported, the backend triggers a server-side Apple App Store Server API query to resolve the canonical state." }, "storekit_original_transaction_id": { "type": "string", "description": "Apple `originalTransactionId` for the subscription. Used by the backend to query Apple's API for the canonical subscription state when `storekit_status=2`. Optional — the backend will fall back to the value stored at transaction creation time when omitted." }, "storekit_subscription_end": { "type": "string", "format": "date-time", "deprecated": true, "description": "Deprecated as of 2026-04-27. Accepted for backwards compatibility but **ignored** — the backend now resolves the actual subscription expiry from Apple's App Store Server API. Existing SDKs may continue sending this field; new SDKs should omit it." } } } } } }, "responses": { "200": { "description": "Status updated", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "string", "description": "The transaction ID." }, "storekit_status": { "type": "integer", "description": "The updated StoreKit status." } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/iap/transaction-history": { "get": { "operationId": "getTransactionHistory", "summary": "Get Transaction History", "description": "Returns a paginated list of transactions for a given user, ordered by most recent first.", "tags": [ "Transactions" ], "parameters": [ { "name": "user_id", "in": "query", "required": true, "description": "The user's external ID.", "schema": { "type": "string" } }, { "name": "limit", "in": "query", "required": false, "description": "Maximum number of transactions to return. Default: 50, Max: 100.", "schema": { "type": "integer", "default": 50, "maximum": 100 } } ], "responses": { "200": { "description": "Transaction history", "content": { "application/json": { "schema": { "type": "object", "required": [ "transactions" ], "properties": { "transactions": { "type": "array", "items": { "$ref": "#/components/schemas/TransactionHistoryItem" } } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" } } } }, "/iap/entitlements": { "get": { "operationId": "getEntitlements", "summary": "Get Entitlements", "description": "Returns all entitlements for a user, including active subscriptions, one-time purchases, and expired items. Use this to determine what the user has access to.", "tags": [ "Entitlements" ], "parameters": [ { "name": "user_id", "in": "query", "required": false, "description": "The user's external ID. Required unless `email` is provided.", "schema": { "type": "string" } }, { "name": "email", "in": "query", "required": false, "deprecated": true, "description": "The user's email address. Deprecated -- use `user_id` instead.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "User entitlements", "content": { "application/json": { "schema": { "type": "object", "required": [ "entitlements" ], "properties": { "entitlements": { "type": "array", "items": { "$ref": "#/components/schemas/Entitlement" } } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" } } } }, "/iap/claim-entitlement/": { "post": { "operationId": "claimEntitlement", "summary": "Claim Entitlement", "description": "Transfers an existing StoreKit entitlement to the calling user. Used when an Apple ID's StoreKit subscription is owned by a different ZeroSettle account and needs to be moved to the current account (account migration, support flows, sandbox testing). The backend verifies the supplied JWS, revokes the entitlement from the previous owner, and grants it to `user_id` in a single atomic transaction.", "tags": [ "Entitlements" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "jws_representation", "user_id" ], "properties": { "jws_representation": { "type": "string", "description": "The StoreKit 2 transaction's JWS representation string for the entitlement being claimed." }, "user_id": { "type": "string", "description": "The external user ID that should receive the entitlement." } } } } } }, "responses": { "200": { "description": "Claim processed", "content": { "application/json": { "schema": { "type": "object", "required": [ "status", "claimed" ], "properties": { "status": { "type": "string", "example": "ok" }, "claimed": { "type": "boolean", "description": "True if the entitlement was transferred to the current user. False when the entitlement already belonged to the current user (no-op)." }, "product_id": { "type": "string", "description": "Reference ID of the claimed product. Present when `claimed` is true." }, "original_transaction_id": { "type": "string", "description": "StoreKit `originalTransactionId` of the claimed entitlement. Present when `claimed` is true." } } } } } }, "400": { "description": "Bad request — missing required field, or the entitlement is a consumable and cannot be claimed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "401": { "description": "JWS verification failed against Apple's root certificate chain, or API key invalid.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "404": { "description": "No entitlement to claim — the JWS does not correspond to a known ZeroSettle entitlement.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }, "/iap/storekit-transactions": { "post": { "operationId": "syncStorekitTransaction", "summary": "Sync StoreKit Transaction", "description": "Forwards a StoreKit 2 JWS (JSON Web Signature) transaction to ZeroSettle for verification and recording. The backend verifies the JWS against Apple's root certificate chain, creates a `Transaction` and `Entitlement`, and returns success. The SDK should call `transaction.finish()` only after receiving a success response.\n\nSDK 1.3+ additionally forwards `will_auto_renew` (pulled from `Product.SubscriptionInfo.RenewalInfo.willAutoRenew`) on every sync. The backend diffs this against the stored entitlement state and emits `CANCELLED` / `REACTIVATED` events when the flag flips, so auto-renew state stays accurate even in deployments without App Store Server Notifications (ASSN) v2 configured.", "tags": [ "StoreKit" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "jws_representation", "user_id" ], "properties": { "jws_representation": { "type": "string", "description": "The StoreKit 2 transaction's JWS representation string." }, "user_id": { "type": "string", "description": "The user's external ID." }, "will_auto_renew": { "type": "boolean", "description": "Optional (SDK 1.3+). The current `willAutoRenew` value from StoreKit's `Product.SubscriptionInfo.RenewalInfo` for the subscription this JWS belongs to. When provided, the backend compares it against the stored entitlement and emits `CANCELLED` (true→false) or `REACTIVATED` (false→true) events. Omit the field for non-subscription syncs or older SDKs; behaviour is unchanged when missing." }, "renewal_state": { "type": "string", "description": "Optional (SDK 1.3+, reserved). Mirror of StoreKit's `Product.SubscriptionInfo.RenewalState` for future use. Accepted but not currently acted on. Typical values: `subscribed`, `expired`, `in_billing_retry_period`, `in_grace_period`, `revoked`.", "enum": [ "subscribed", "expired", "in_billing_retry_period", "in_grace_period", "revoked" ] } } } } } }, "responses": { "200": { "description": "Transaction synced successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "ok" } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" } } } }, "/iap/storekit-subscription-status": { "get": { "operationId": "getStorekitSubscriptionStatus", "summary": "Get StoreKit Subscription Status", "description": "Returns the current status of a StoreKit subscription by its original transaction ID. Useful for checking renewal state and expiry. The response includes normalized `is_active` and `auto_renew_enabled` booleans (SDK 1.2+); prefer these over the raw `status`/`auto_renew_status` fields for canonical access decisions.", "tags": [ "StoreKit" ], "parameters": [ { "name": "original_transaction_id", "in": "query", "required": true, "description": "The StoreKit original transaction ID for the subscription.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Subscription status", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/StorekitSubscriptionStatus" } } } }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/iap/user-offer": { "get": { "operationId": "getUserOffer", "summary": "Get User Offer", "description": "Canonical offer + subscription-state resolver (SDK 1.2+). Returns the user's current subscription variant plus exactly one offer decision (`no_action`, `migrate_storekit_to_web`, `upgrade_storekit_to_web`, or `upgrade_web_to_web`). Replaces the overlapping `/iap/products/` offer config, `/iap/upgrade-offer/`, and ad-hoc status parsing with a single typed contract. Legacy endpoints remain supported for older SDKs.", "tags": [ "User Offer" ], "parameters": [ { "name": "user_id", "in": "query", "required": true, "description": "The user's external ID.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Canonical user offer + subscription state", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserOfferResponse" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/iap/migration-converted": { "post": { "operationId": "migrationConverted", "summary": "Report Migration Conversion", "description": "Reports that a Switch & Save migration was shown or converted. Used to track migration campaign analytics.", "tags": [ "Migration" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "properties": { "variant_id": { "type": "integer", "description": "The experiment variant ID, if the migration campaign is part of an A/B test." } } } } } }, "responses": { "200": { "description": "Conversion recorded", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "ok" }, "conversions": { "type": "integer", "description": "Total number of conversions for this campaign." } } } } } }, "401": { "$ref": "#/components/responses/Unauthorized" } } } }, "/iap/subscriptions/cancel": { "post": { "operationId": "cancelSubscription", "summary": "Cancel Subscription", "description": "Cancels a user's active web checkout subscription. By default, access continues until the end of the current billing period (`immediate: false`). Set `immediate: true` to revoke access immediately.", "tags": [ "Subscriptions" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "user_id" ], "properties": { "user_id": { "type": "string", "description": "The user's external ID." }, "product_id": { "type": "string", "description": "Specific product to cancel. If omitted, cancels the user's active subscription." }, "immediate": { "type": "boolean", "default": false, "description": "If `true`, cancels immediately and revokes access. If `false`, cancels at end of billing period." } } } } } }, "responses": { "200": { "description": "Subscription cancelled", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CancelResponse" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/iap/subscriptions/pause": { "post": { "operationId": "pauseSubscription", "summary": "Pause Subscription", "description": "Pauses a user's active web checkout subscription. The subscription will automatically resume after the pause duration expires.", "tags": [ "Subscriptions" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "user_id" ], "properties": { "user_id": { "type": "string", "description": "The user's external ID." }, "product_id": { "type": "string", "description": "Specific product to pause. If omitted, pauses the user's active subscription." }, "pause_duration_days": { "type": "integer", "description": "Number of days to pause the subscription. If omitted, uses the default configured in the cancel flow." } } } } } }, "responses": { "200": { "description": "Subscription paused", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PauseResponse" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/iap/subscriptions/resume": { "post": { "operationId": "resumeSubscription", "summary": "Resume Subscription", "description": "Resumes a previously paused web checkout subscription.", "tags": [ "Subscriptions" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "user_id" ], "properties": { "user_id": { "type": "string", "description": "The user's external ID." }, "product_id": { "type": "string", "description": "Specific product to resume. If omitted, resumes the user's paused subscription." } } } } } }, "responses": { "200": { "description": "Subscription resumed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResumeResponse" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/iap/customer-portal-sessions": { "post": { "operationId": "createCustomerPortalSession", "summary": "Create Customer Portal Session", "description": "Creates a Stripe Customer Portal session URL. The portal lets users manage their subscriptions, update payment methods, and view billing history.", "tags": [ "Subscriptions" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "user_id" ], "properties": { "user_id": { "type": "string", "description": "The user's external ID." }, "return_url": { "type": "string", "description": "URL to redirect the user to after they leave the portal." } } } } } }, "responses": { "200": { "description": "Portal session created", "content": { "application/json": { "schema": { "type": "object", "properties": { "portal_url": { "type": "string", "description": "URL to redirect the user to the Stripe Customer Portal." } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/iap/cancel-flow": { "get": { "operationId": "getCancelFlow", "summary": "Get Cancel Flow", "description": "Returns the configured cancel flow for the app, including survey questions, retention offers, and pause options. The SDK uses this to render a multi-step cancellation experience.", "tags": [ "Cancel Flow" ], "parameters": [ { "name": "user_id", "in": "query", "required": false, "description": "The user's external ID. Used to personalize offers.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Cancel flow configuration", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CancelFlow" } } } }, "401": { "$ref": "#/components/responses/Unauthorized" } } } }, "/iap/cancel-flow/respond": { "post": { "operationId": "submitCancelFlowResponse", "summary": "Submit Cancel Flow Response", "description": "Submits the user's responses from the cancel flow, including survey answers and the final outcome (cancelled, retained, dismissed, or paused). Used for cancel flow analytics.", "tags": [ "Cancel Flow" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "user_id", "outcome" ], "properties": { "user_id": { "type": "string", "description": "The user's external ID." }, "product_id": { "type": "string", "description": "The product being cancelled." }, "outcome": { "type": "string", "enum": [ "cancelled", "retained", "dismissed", "paused" ], "description": "The final outcome of the cancel flow." }, "offer_shown": { "type": "boolean", "description": "Whether a retention offer was shown to the user." }, "offer_accepted": { "type": "boolean", "description": "Whether the user accepted the retention offer." }, "pause_shown": { "type": "boolean", "description": "Whether a pause option was shown." }, "pause_accepted": { "type": "boolean", "description": "Whether the user chose to pause." }, "pause_duration_days": { "type": "integer", "description": "Number of days the user chose to pause, if applicable." }, "last_step_seen": { "type": "string", "description": "The last step of the cancel flow the user saw." }, "variant_id": { "type": "integer", "description": "Experiment variant ID, if the cancel flow is part of an A/B test." }, "answers": { "type": "array", "items": { "type": "object", "properties": { "question_id": { "type": "string" }, "answer": { "type": "string" } } }, "description": "Array of survey question answers." } } } } } }, "responses": { "200": { "description": "Response recorded", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "ok" } } } } } }, "401": { "$ref": "#/components/responses/Unauthorized" } } } }, "/iap/cancel-flow/accept-offer": { "post": { "operationId": "acceptCancelFlowOffer", "summary": "Accept Cancel Flow Offer", "description": "Accepts the retention offer shown during the cancel flow, applying a discount to the user's subscription.", "tags": [ "Cancel Flow" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "user_id", "product_id" ], "properties": { "user_id": { "type": "string", "description": "The user's external ID." }, "product_id": { "type": "string", "description": "The product the offer applies to." } } } } } }, "responses": { "200": { "description": "Offer accepted", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "ok" }, "discount_applied": { "type": "boolean", "description": "Whether the discount was successfully applied." } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/iap/upgrade-offer": { "get": { "operationId": "getUpgradeOffer", "summary": "Get Upgrade Offer", "description": "Checks if a user is eligible for a subscription upgrade and returns available upgrade options.", "tags": [ "Upgrade Offers" ], "parameters": [ { "name": "user_id", "in": "query", "required": true, "description": "The user's external ID.", "schema": { "type": "string" } }, { "name": "product_id", "in": "query", "required": false, "description": "Specific product to check upgrade eligibility for.", "schema": { "type": "string" } } ], "responses": { "200": { "description": "Upgrade offer details", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpgradeOffer" } } } }, "401": { "$ref": "#/components/responses/Unauthorized" } } } }, "/iap/upgrade-offer/execute": { "post": { "operationId": "executeUpgradeOffer", "summary": "Execute Upgrade Offer", "description": "Executes a subscription upgrade, creating a new payment intent or checkout session for the target product. The old subscription is cancelled with proration when the new payment succeeds.", "tags": [ "Upgrade Offers" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "user_id", "current_product_id", "target_product_id" ], "properties": { "user_id": { "type": "string", "description": "The user's external ID." }, "current_product_id": { "type": "string", "description": "The reference ID of the user's current subscription product." }, "target_product_id": { "type": "string", "description": "The reference ID of the product to upgrade to." } } } } } }, "responses": { "200": { "description": "Upgrade initiated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpgradeExecuteResponse" } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" } } } }, "/iap/upgrade-offer/respond": { "post": { "operationId": "respondUpgradeOffer", "summary": "Respond to Upgrade Offer", "description": "Records the user's response to an upgrade offer for analytics purposes.", "tags": [ "Upgrade Offers" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "user_id" ], "properties": { "user_id": { "type": "string", "description": "The user's external ID." }, "accepted": { "type": "boolean", "description": "Whether the user accepted the upgrade offer." }, "variant_id": { "type": "integer", "description": "Experiment variant ID, if the upgrade offer is part of an A/B test." } } } } } }, "responses": { "200": { "description": "Response recorded", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "ok" } } } } } }, "401": { "$ref": "#/components/responses/Unauthorized" } } } }, "/iap/events": { "post": { "operationId": "trackEvent", "summary": "Track Event", "description": "Tracks an analytics event from the SDK. Used for funnel analysis, screen views, and custom events.", "tags": [ "Events" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "event_type", "user_id" ], "properties": { "event_type": { "type": "string", "description": "The event type identifier (e.g., `paywall_shown`, `purchase_started`, `checkout_completed`)." }, "user_id": { "type": "string", "description": "The user's external ID." }, "product_id": { "type": "string", "description": "The product related to this event, if applicable." }, "screen_name": { "type": "string", "description": "The screen or view where the event occurred." }, "metadata": { "type": "object", "additionalProperties": true, "description": "Additional key-value metadata for the event." } } } } } }, "responses": { "201": { "description": "Event recorded", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "ok" } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" } } } }, "/iap/restore-purchases": { "post": { "operationId": "restorePurchases", "summary": "Restore Purchases", "description": "Restores purchases for a user by linking entitlements from a previous identity (matched by email) to the current user ID. Useful when a user reinstalls the app or signs in on a new device.", "tags": [ "Restore" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "email", "user_id" ], "properties": { "email": { "type": "string", "description": "The user's email address to match against previous purchases." }, "user_id": { "type": "string", "description": "The user's current external ID to restore purchases to." } } } } } }, "responses": { "200": { "description": "Purchases restored", "content": { "application/json": { "schema": { "type": "object", "properties": { "status": { "type": "string", "example": "ok" }, "restored_count": { "type": "integer", "description": "Number of entitlements restored." } } } } } }, "400": { "$ref": "#/components/responses/BadRequest" }, "401": { "$ref": "#/components/responses/Unauthorized" } } } } }, "components": { "securitySchemes": { "ApiKeyAuth": { "type": "apiKey", "in": "header", "name": "X-ZeroSettle-Key", "description": "Your publishable API key. Use `zs_pk_test_*` for sandbox or `zs_pk_live_*` for production." } }, "schemas": { "Product": { "type": "object", "description": "A product in the catalog.", "properties": { "id": { "type": "string", "description": "The product's reference ID (e.g., `com.app.premium.monthly`)." }, "display_name": { "type": "string", "description": "Human-readable product name." }, "product_description": { "type": "string", "description": "Product description text." }, "type": { "type": "string", "enum": [ "consumable", "non_consumable", "non_renewing_subscription", "auto_renewable_subscription" ], "description": "The product type." }, "web_price": { "description": "The web checkout price, or null if not available for web purchase.", "allOf": [ { "$ref": "#/components/schemas/Price" } ], "nullable": true }, "storekit_price": { "description": "The StoreKit price from App Store Connect, or null if not synced.", "allOf": [ { "$ref": "#/components/schemas/Price" } ], "nullable": true }, "subscription_group_id": { "description": "The subscription group this product belongs to, or null for non-subscription products.", "type": "integer", "nullable": true }, "free_trial_duration": { "description": "ISO 8601 duration for the free trial period (e.g., `P7D` for 7 days), or null if no trial is configured.", "type": "string", "nullable": true }, "is_trial_eligible": { "description": "Whether the current user is eligible for a free trial on this product. Null if `user_id` was not provided in the request.", "type": "boolean", "nullable": true }, "trial": { "description": "Trial mode details for this product. Present only for `auto_renewable_subscription` products that have a free trial configured AND whose trial eligibility was resolved (i.e., `user_id` was included in the request and the user is trial-eligible). Absent for non-subscription products, products without a trial, and anonymous requests.", "nullable": true, "type": "object", "properties": { "mode": { "type": "string", "enum": ["free", "paid", "auth_hold"], "description": "How the trial is collected. `free`: no charge during the trial period; the payment method is saved and billed at trial end. `paid`: a real upfront charge of `upfront_amount_cents` is collected immediately and kept; the full subscription price is billed at trial end. `auth_hold`: a temporary authorization hold of `hold_amount_cents` is placed (shown as a pending charge); released when the subscription converts or is cancelled." }, "duration": { "type": "string", "description": "Duration token for the trial period (e.g., `1_week`, `3_days`, `1_month`). This is a ZeroSettle token, not an ISO 8601 duration. The app should map this to its own display copy." }, "upfront_amount_cents": { "type": "integer", "description": "For `paid` mode: the real charge collected at checkout, in cents. Zero for `free` and `auth_hold` modes." }, "hold_amount_cents": { "type": "integer", "description": "For `auth_hold` mode: the authorization hold amount placed at checkout, in cents. Zero for `free` and `paid` modes." }, "validates_card": { "type": "boolean", "description": "`true` for `paid` and `auth_hold` modes (a real or pending charge verifies the card); `false` for `free` mode." } } } } }, "Price": { "type": "object", "description": "A price in micros (1/1,000,000 of the currency unit).", "properties": { "amount_micros": { "type": "integer", "description": "Price in micros. For example, $4.99 = 4990000." }, "currency_code": { "type": "string", "description": "ISO 4217 currency code (e.g., `USD`)." } } }, "CheckoutConfig": { "type": "object", "description": "Configuration for how the SDK should present checkout.", "properties": { "sheet_type": { "type": "string", "enum": [ "webview", "native" ], "description": "Default checkout presentation: `native` for Apple Pay / Google Pay payment sheet, `webview` for embedded web checkout." }, "is_enabled": { "type": "boolean", "description": "Whether web checkout is enabled for this app." }, "jurisdictions": { "type": "object", "description": "Per-jurisdiction checkout overrides.", "properties": { "us": { "$ref": "#/components/schemas/JurisdictionConfig" }, "eu": { "$ref": "#/components/schemas/JurisdictionConfig" }, "other": { "$ref": "#/components/schemas/JurisdictionConfig" } } }, "apple_merchant_id": { "description": "Apple Merchant ID for Apple Pay, if configured.", "type": "string", "nullable": true } } }, "JurisdictionConfig": { "type": "object", "description": "Checkout configuration for a specific jurisdiction.", "properties": { "sheet_type": { "type": "string", "enum": [ "webview", "native" ], "description": "Checkout presentation type for this jurisdiction." }, "is_enabled": { "type": "boolean", "description": "Whether web checkout is enabled in this jurisdiction." } } }, "MigrationCampaign": { "type": "object", "description": "An active Switch & Save migration campaign.", "properties": { "should_show": { "type": "boolean", "description": "Whether the migration offer should be displayed to the user." }, "product_id": { "type": "string", "description": "The product the migration offer applies to." }, "discount_percent": { "type": "integer", "description": "Discount percentage offered (e.g., 20 for 20% off)." }, "min_subscription_days": { "type": "integer", "description": "Minimum number of days the user must have been subscribed to be eligible." }, "max_subscription_days": { "type": "integer", "description": "Maximum number of subscription days for eligibility." }, "free_trial_days": { "type": "integer", "description": "Number of free trial days offered in the migration." }, "title": { "type": "string", "description": "Title text for the migration offer UI." }, "message": { "type": "string", "description": "Body text for the migration offer UI." }, "variant_id": { "description": "Experiment variant ID if the campaign is part of an A/B test.", "type": "integer", "nullable": true } } }, "PaymentIntentResponse": { "type": "object", "description": "Response from creating a payment intent.", "properties": { "client_secret": { "type": "string", "description": "Stripe PaymentIntent client secret. Use this to confirm the payment on the client side." }, "payment_intent_id": { "type": "string", "description": "Stripe PaymentIntent ID." }, "transaction_id": { "type": "string", "description": "ZeroSettle transaction ID. Use this to poll for transaction status." }, "amount_cents": { "type": "integer", "description": "The charge amount in cents." }, "currency": { "type": "string", "description": "ISO 4217 currency code (e.g., `usd`)." }, "product_name": { "type": "string", "description": "Display name of the product being purchased." }, "checkout_url": { "type": "string", "description": "Fallback URL for browser-based checkout." }, "merchant_country": { "type": "string", "description": "ISO 3166-1 alpha-2 country code of the merchant's Stripe account. Required for Apple Pay `paymentRequest()` country parameter." }, "original_amount_cents": { "type": "integer", "description": "The original price before any discounts or prorations." }, "trial_type": { "type": "string", "enum": [ "free_trial", "migration" ], "nullable": true, "description": "The type of trial applied to this checkout. `free_trial` for standard free trials, `migration` for Switch & Save aligned trials. Omitted when no trial is active." }, "trial_end": { "type": "integer", "nullable": true, "description": "Unix timestamp when the trial period ends. Only present when a trial is active." }, "pending_amount": { "type": "integer", "nullable": true, "description": "The charge amount in cents that will be billed when the trial ends. Only present when a trial is active." }, "intent_type": { "type": "string", "enum": ["setup", "payment"], "description": "Discriminator for which Stripe confirm method the client must call. `setup`: confirms a SetupIntent (free trial — card saved, no immediate charge); `payment`: confirms a PaymentIntent (paid/auth_hold trial upfront charge, or any normal charge). Also returned by `POST /iap/payment-intents/{transaction_id}/finalize/` with the same semantics." } } }, "CheckoutSessionResponse": { "type": "object", "description": "Response from creating a checkout session.", "properties": { "checkout_session_id": { "type": "string", "description": "Stripe Checkout Session ID." }, "url": { "type": "string", "description": "URL to redirect the user to for checkout." }, "client_secret": { "type": "string", "description": "Client secret for embedded checkout." }, "transaction_id": { "type": "string", "description": "ZeroSettle transaction ID." } } }, "Transaction": { "type": "object", "description": "A purchase transaction.", "properties": { "id": { "type": "string", "description": "Transaction ID (UUID)." }, "product_id": { "type": "string", "description": "The product's reference ID." }, "status": { "type": "string", "enum": [ "pending", "processing", "succeeded", "failed", "refunded", "disputed" ], "description": "Current transaction status." }, "source": { "type": "string", "enum": [ "web_checkout", "store_kit" ], "description": "Where the transaction originated." }, "purchased_at": { "type": "string", "format": "date-time", "description": "ISO 8601 timestamp of when the purchase was made." }, "storekit_status": { "description": "StoreKit finish status: `1` = active (finished), `2` = anything-not-active (cancelled, expired, billing retry, grace period, revoked). Null for web checkout transactions. This is a coarse SDK-reported flag; consult `storekit_apple_status` for Apple's full state.", "type": "integer", "nullable": true }, "storekit_apple_status": { "description": "Apple's full subscription status code from the App Store Server API. `1` = Active, `2` = Expired, `3` = BillingRetry, `4` = GracePeriod, `5` = Revoked. Null when no Apple-verified state has been recorded (e.g., before the SDK has reported a state change, or when ASC credentials are not configured for the tenant).", "type": "integer", "nullable": true, "enum": [ 1, 2, 3, 4, 5 ] }, "expires_at": { "description": "ISO 8601 expiry timestamp for subscription transactions. Null for one-time purchases.", "type": "string", "format": "date-time", "nullable": true } } }, "TransactionHistoryItem": { "type": "object", "description": "A transaction in the user's history.", "properties": { "id": { "type": "string", "description": "Transaction ID." }, "product_id": { "type": "string", "description": "The product's reference ID." }, "status": { "type": "string", "enum": [ "pending", "processing", "succeeded", "failed", "refunded", "disputed" ], "description": "Transaction status." }, "source": { "type": "string", "enum": [ "web_checkout", "store_kit" ], "description": "Transaction source." }, "purchased_at": { "type": "string", "format": "date-time", "description": "Purchase timestamp." }, "amount_cents": { "type": "integer", "description": "Transaction amount in cents." }, "currency": { "type": "string", "description": "ISO 4217 currency code." }, "product_name": { "type": "string", "description": "Display name of the product." }, "expires_at": { "description": "Subscription expiry timestamp, or null.", "type": "string", "format": "date-time", "nullable": true } } }, "Entitlement": { "type": "object", "description": "A user's entitlement to a product.", "properties": { "id": { "type": "string", "description": "Stable transaction-derived identifier in the form `txn_`. Older entitlements may still return the legacy `txn_` format." }, "product_id": { "type": "string", "description": "The product's reference ID." }, "product_type": { "type": "string", "enum": [ "consumable", "non_consumable", "non_renewing_subscription", "auto_renewable_subscription" ], "description": "The type of product." }, "source": { "type": "string", "enum": [ "web_checkout", "store_kit", "play_store" ], "description": "Where the entitlement originated." }, "status": { "type": "string", "enum": [ "active", "expired", "cancelled", "paused", "revoked", "billing_retry", "billing_grace_period" ], "description": "Current entitlement status." }, "is_active": { "type": "boolean", "description": "Whether the user currently has access to this product." }, "expires_at": { "description": "When the entitlement expires, or null for lifetime purchases.", "type": "string", "format": "date-time", "nullable": true }, "purchased_at": { "type": "string", "format": "date-time", "description": "When the entitlement was originally purchased." }, "will_renew": { "type": "boolean", "description": "Whether the subscription will auto-renew at the end of the current period." }, "is_trial": { "type": "boolean", "description": "Whether the entitlement is currently in a free trial period." }, "trial_ends_at": { "description": "When the trial period ends, or null if not on trial.", "type": "string", "format": "date-time", "nullable": true }, "cancelled_at": { "description": "When the subscription was cancelled, or null if not cancelled.", "type": "string", "format": "date-time", "nullable": true } } }, "StorekitSubscriptionStatus": { "type": "object", "description": "Status of a StoreKit subscription. Prefer the normalized `is_active` and `auto_renew_enabled` booleans for canonical access decisions; `status` and `auto_renew_status` are diagnostic fields that mirror the raw Apple payload.", "required": [ "is_active", "auto_renew_enabled" ], "properties": { "status": { "type": "integer", "description": "Diagnostic only — SDK-collapsed Apple status: 1 (active) or 2 (not active). Prefer `is_active` for access decisions." }, "auto_renew_status": { "type": "integer", "description": "Diagnostic only — raw Apple `autoRenewStatus` integer (0 = off, 1 = on). Prefer `auto_renew_enabled` for canonical use." }, "expires_at": { "type": "string", "format": "date-time", "nullable": true, "description": "When the subscription expires or renews." }, "is_active": { "type": "boolean", "description": "Canonical active-entitlement flag. `true` when the Apple status code represents active access (active, grace period, or billing retry with access). Derived server-side from `apple_status` — use this instead of interpreting `status` yourself." }, "auto_renew_enabled": { "type": "boolean", "description": "Canonical auto-renew flag. Equivalent to `auto_renew_status == 1`, exposed as a boolean for consistency with the rest of the API." } } }, "CancelResponse": { "type": "object", "description": "Response from cancelling a subscription.", "properties": { "status": { "type": "string", "description": "Result status." }, "cancelled_at": { "type": "string", "format": "date-time", "description": "When the cancellation was processed." }, "access_until": { "type": "string", "format": "date-time", "description": "When the user will lose access (end of billing period for non-immediate cancellations)." } } }, "PauseResponse": { "type": "object", "description": "Response from pausing a subscription.", "properties": { "status": { "type": "string", "description": "Result status." }, "paused_at": { "type": "string", "format": "date-time", "description": "When the subscription was paused." }, "resumes_at": { "type": "string", "format": "date-time", "description": "When the subscription will automatically resume." } } }, "ResumeResponse": { "type": "object", "description": "Response from resuming a subscription.", "properties": { "status": { "type": "string", "description": "Result status." }, "resumed_at": { "type": "string", "format": "date-time", "description": "When the subscription was resumed." } } }, "CancelFlow": { "type": "object", "description": "Cancel flow configuration.", "properties": { "enabled": { "type": "boolean", "description": "Whether the cancel flow is enabled for this app." }, "questions": { "type": "array", "description": "Survey questions to show during the cancel flow.", "items": { "$ref": "#/components/schemas/CancelFlowQuestion" } }, "offer": { "description": "Retention offer to show, or null if none is configured.", "allOf": [ { "$ref": "#/components/schemas/CancelFlowOffer" } ], "nullable": true }, "pause": { "description": "Pause option configuration, or null if pause is not enabled.", "allOf": [ { "$ref": "#/components/schemas/CancelFlowPause" } ], "nullable": true }, "variant_id": { "description": "Experiment variant ID if the cancel flow is part of an A/B test.", "type": "integer", "nullable": true } } }, "CancelFlowQuestion": { "type": "object", "description": "A survey question in the cancel flow.", "properties": { "id": { "type": "string", "description": "Question identifier." }, "text": { "type": "string", "description": "Question text to display." }, "type": { "type": "string", "enum": [ "single_choice", "multiple_choice", "free_text" ], "description": "Question type." }, "options": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string", "description": "Option identifier." }, "text": { "type": "string", "description": "Option text." } } }, "description": "Available answer options (for choice questions)." } } }, "CancelFlowOffer": { "type": "object", "description": "A retention offer in the cancel flow.", "properties": { "type": { "type": "string", "enum": [ "discount", "extension" ], "description": "Type of retention offer." }, "discount_percent": { "type": "integer", "description": "Discount percentage (for discount-type offers)." }, "duration_months": { "type": "integer", "description": "How many billing cycles the discount lasts." }, "title": { "type": "string", "description": "Offer title text." }, "message": { "type": "string", "description": "Offer body text." } } }, "CancelFlowPause": { "type": "object", "description": "Pause option in the cancel flow.", "properties": { "enabled": { "type": "boolean", "description": "Whether the pause option is available." }, "duration_days": { "type": "integer", "description": "Default pause duration in days." }, "max_duration_days": { "type": "integer", "description": "Maximum allowed pause duration." }, "title": { "type": "string", "description": "Pause option title text." }, "message": { "type": "string", "description": "Pause option body text." } } }, "UpgradeOffer": { "type": "object", "description": "Upgrade offer eligibility and options.", "properties": { "eligible": { "type": "boolean", "description": "Whether the user is eligible for an upgrade." }, "current_product_id": { "type": "string", "description": "The user's current subscription product ID." }, "eligible_upgrades": { "type": "array", "description": "Products the user can upgrade to.", "items": { "type": "object", "properties": { "product_id": { "type": "string", "description": "The upgrade target product's reference ID." }, "display_name": { "type": "string", "description": "Product display name." }, "price": { "$ref": "#/components/schemas/Price" }, "savings_percent": { "type": "integer", "description": "Percentage savings compared to the current plan (annualized)." } } } }, "variant_id": { "description": "Experiment variant ID if the upgrade offer is part of an A/B test.", "type": "integer", "nullable": true } } }, "UpgradeExecuteResponse": { "type": "object", "description": "Response from executing an upgrade.", "properties": { "type": { "type": "string", "enum": [ "web_to_web", "storekit_to_web" ], "description": "The type of upgrade being performed." }, "client_secret": { "type": "string", "description": "Stripe PaymentIntent client secret for confirming the upgrade payment." }, "checkout_url": { "type": "string", "description": "Fallback checkout URL." }, "transaction_id": { "type": "string", "description": "ZeroSettle transaction ID for the upgrade." } } }, "Offer": { "type": "object", "description": "Unified offer payload for migration and upgrade flows.", "required": [ "flow_type", "product_id", "eligible_product_ids", "savings_percent", "display", "free_trial_days", "min_subscription_days", "max_subscription_days", "rollout_percent" ], "properties": { "flow_type": { "type": "string", "enum": [ "migration", "upgrade" ], "description": "The type of offer flow." }, "product_id": { "type": "string", "description": "Target product reference ID." }, "eligible_product_ids": { "type": "array", "items": { "type": "string" }, "description": "Product IDs the user might currently have." }, "savings_percent": { "type": "integer", "description": "Annualized savings percentage (0 if not computable)." }, "display": { "$ref": "#/components/schemas/OfferDisplay" }, "free_trial_days": { "type": "integer", "description": "Free trial days (computed at checkout time from product config)." }, "min_subscription_days": { "type": "integer", "description": "Minimum days subscribed before eligibility." }, "max_subscription_days": { "type": "integer", "nullable": true, "description": "Maximum days subscribed for eligibility." }, "rollout_percent": { "type": "integer", "description": "Percentage of eligible users who see this offer (0-100)." }, "upgrade_type": { "type": "string", "enum": [ "storekit_to_web", "web_to_web" ], "nullable": true, "description": "Upgrade subtype. Only present when flow_type is upgrade." }, "from_product_id": { "type": "string", "nullable": true, "description": "User's current product (upgrades only)." }, "to_product_id": { "type": "string", "nullable": true, "description": "Upgrade target product (upgrades only)." }, "variant_id": { "type": "integer", "nullable": true, "description": "Experiment variant ID (when A/B testing)." }, "per_product_prompts": { "type": "object", "nullable": true, "description": "Per-product display overrides keyed by product ID." }, "checkout_presentation": { "type": "string", "enum": [ "inline", "sheet" ], "nullable": true, "description": "How the SDK presents checkout: inline within tip card or overlay sheet." } } }, "OfferDisplay": { "type": "object", "description": "Server-configurable copy for every tip card state. Empty strings mean the SDK should use its hardcoded defaults.", "required": [ "offer_title", "offer_message", "offer_cta", "accepted_title", "accepted_message", "accepted_cta", "completed_title", "completed_message" ], "properties": { "offer_title": { "type": "string", "description": "Title for the offer card (e.g., 'Thanks for being with us!')." }, "offer_message": { "type": "string", "description": "Main offer body text." }, "offer_cta": { "type": "string", "description": "CTA button text (e.g., 'Save 20% Forever')." }, "accepted_title": { "type": "string", "description": "Post-checkout title (e.g., 'Thanks for switching!')." }, "accepted_message": { "type": "string", "description": "Post-checkout message with Apple cancel instructions." }, "accepted_cta": { "type": "string", "description": "Post-checkout CTA (e.g., 'Cancel Apple Billing')." }, "completed_title": { "type": "string", "description": "Success title (e.g., 'Congratulations!')." }, "completed_message": { "type": "string", "description": "Success message (e.g., 'You are now saving 20% forever.')." } } }, "UserOfferResponse": { "type": "object", "description": "Canonical response from `GET /iap/user-offer/`. Wraps the user's current subscription state (as a tagged union keyed by `subscription.type`) together with exactly one offer decision.", "required": [ "user_id", "app_id", "is_sandbox", "subscription", "offer", "server_time" ], "properties": { "user_id": { "type": "string", "description": "Echoes the `user_id` query parameter." }, "app_id": { "type": "integer", "description": "Internal ZeroSettle app ID that owns the publishable key." }, "is_sandbox": { "type": "boolean", "description": "Resolved environment — `true` when the request was authenticated with a sandbox publishable key." }, "subscription": { "$ref": "#/components/schemas/UserOfferSubscription" }, "offer": { "$ref": "#/components/schemas/OfferEligibility" }, "server_time": { "type": "string", "format": "date-time", "description": "Server-side timestamp at which the response was resolved (ISO 8601). Useful for client-side cache invalidation." } } }, "UserOfferSubscription": { "description": "Tagged-union describing the user's current subscription state. Discriminated by the `type` field. Exactly one variant is returned per request.", "oneOf": [ { "$ref": "#/components/schemas/SubscriptionStateNone" }, { "$ref": "#/components/schemas/SubscriptionStateActiveWeb" }, { "$ref": "#/components/schemas/SubscriptionStateActiveStorekit" }, { "$ref": "#/components/schemas/SubscriptionStateMigrationTrial" }, { "$ref": "#/components/schemas/SubscriptionStateCancelledActive" } ], "discriminator": { "propertyName": "type", "mapping": { "none": "#/components/schemas/SubscriptionStateNone", "active_web": "#/components/schemas/SubscriptionStateActiveWeb", "active_storekit": "#/components/schemas/SubscriptionStateActiveStorekit", "migration_trial": "#/components/schemas/SubscriptionStateMigrationTrial", "cancelled_active": "#/components/schemas/SubscriptionStateCancelledActive" } } }, "SubscriptionStateNone": { "type": "object", "description": "User has no active subscription entitlement from any source.", "required": ["type"], "properties": { "type": { "type": "string", "enum": ["none"] } } }, "SubscriptionStateActiveWeb": { "type": "object", "description": "User has an active web-sourced subscription (may be in grace period, past due, or paused).", "required": [ "type", "product_id", "status", "is_trial", "will_renew", "stripe_subscription_id", "entitlement_id" ], "properties": { "type": { "type": "string", "enum": ["active_web"] }, "product_id": { "type": "string", "description": "Reference ID of the active product." }, "subscription_group_id": { "type": "integer", "nullable": true, "description": "ZeroSettle subscription group ID, if any." }, "status": { "type": "string", "description": "EntitlementStatus value (e.g., `active`, `grace_period`, `past_due`, `paused`, `cancelled`)." }, "expires_at": { "type": "string", "format": "date-time", "nullable": true, "description": "When the current period ends / next renewal is due." }, "trial_ends_at": { "type": "string", "format": "date-time", "nullable": true, "description": "Trial end timestamp when `is_trial` is `true`." }, "is_trial": { "type": "boolean" }, "will_renew": { "type": "boolean", "description": "Whether the subscription is set to auto-renew at period end." }, "stripe_subscription_id": { "type": "string" }, "entitlement_id": { "type": "integer" } } }, "SubscriptionStateActiveStorekit": { "type": "object", "description": "User has an active StoreKit-sourced subscription.", "required": [ "type", "product_id", "status", "expires_at", "is_trial", "will_renew", "storekit_original_transaction_id", "entitlement_id" ], "properties": { "type": { "type": "string", "enum": ["active_storekit"] }, "product_id": { "type": "string" }, "subscription_group_id": { "type": "integer", "nullable": true }, "status": { "type": "string", "description": "EntitlementStatus value." }, "expires_at": { "type": "string", "format": "date-time" }, "is_trial": { "type": "boolean" }, "trial_ends_at": { "type": "string", "format": "date-time", "nullable": true }, "will_renew": { "type": "boolean" }, "storekit_original_transaction_id": { "type": "string" }, "entitlement_id": { "type": "integer" } } }, "SubscriptionStateMigrationTrial": { "type": "object", "description": "Dual-active migration trial window: the user has a web subscription inside its free trial plus an overlapping StoreKit entitlement that has not yet expired.", "required": [ "type", "web_product_id", "storekit_product_id", "trial_ends_at", "storekit_expires_at", "stripe_subscription_id", "storekit_original_transaction_id", "entitlement_id" ], "properties": { "type": { "type": "string", "enum": ["migration_trial"] }, "web_product_id": { "type": "string" }, "storekit_product_id": { "type": "string" }, "subscription_group_id": { "type": "integer", "nullable": true }, "trial_ends_at": { "type": "string", "format": "date-time" }, "storekit_expires_at": { "type": "string", "format": "date-time" }, "stripe_subscription_id": { "type": "string" }, "storekit_original_transaction_id": { "type": "string" }, "entitlement_id": { "type": "integer" } } }, "SubscriptionStateCancelledActive": { "type": "object", "description": "Subscription is cancelled but access remains until `expires_at`.", "required": [ "type", "source", "product_id", "expires_at", "entitlement_id" ], "properties": { "type": { "type": "string", "enum": ["cancelled_active"] }, "source": { "type": "string", "description": "EntitlementSource (e.g., `web_checkout`, `store_kit`)." }, "product_id": { "type": "string" }, "subscription_group_id": { "type": "integer", "nullable": true }, "expires_at": { "type": "string", "format": "date-time" }, "stripe_subscription_id": { "type": "string", "nullable": true }, "storekit_original_transaction_id": { "type": "string", "nullable": true }, "entitlement_id": { "type": "integer" } } }, "OfferEligibility": { "type": "object", "description": "The offer decision. Exactly one `action_type` is returned per user per request. `is_eligible` is `false` iff `action_type` is `no_action`.", "required": [ "action_type", "is_eligible", "checkout_product_id", "savings_percent", "free_trial_days", "min_subscription_days", "requires_apple_cancel" ], "properties": { "action_type": { "type": "string", "enum": [ "no_action", "migrate_storekit_to_web", "upgrade_storekit_to_web", "upgrade_web_to_web" ], "description": "Which offer branch fired. `no_action` = nothing to show." }, "is_eligible": { "type": "boolean", "description": "Convenience boolean; equivalent to `action_type != 'no_action'`." }, "checkout_product_id": { "type": "string", "description": "Product ID the SDK should drive checkout toward. Empty string when `action_type` is `no_action`." }, "from_product_id": { "type": "string", "nullable": true, "description": "The product the user is currently on (relevant for upgrade/migrate flows)." }, "savings_percent": { "type": "integer", "description": "Annualized savings percent vs. the user's current plan, 0 when not applicable." }, "free_trial_days": { "type": "integer", "description": "Days of free trial the SDK should honor on the new subscription." }, "min_subscription_days": { "type": "integer", "description": "Minimum subscribed days before the offer becomes eligible (for rollout/lock-in rules)." }, "display": { "$ref": "#/components/schemas/OfferDisplayV2", "nullable": true }, "proration": { "$ref": "#/components/schemas/OfferProration", "nullable": true }, "requires_apple_cancel": { "type": "boolean", "description": "Whether the user must cancel their StoreKit subscription after the new checkout completes (true for `migrate_*` / `upgrade_storekit_to_web`)." }, "apple_subscription": { "$ref": "#/components/schemas/AppleSubscriptionSummary", "nullable": true }, "checkout_presentation": { "type": "string", "enum": ["webview", "native_pay", "safari_vc", "safari"], "nullable": true, "description": "How the SDK should present checkout for this offer." }, "experiment_variant_id": { "type": "integer", "nullable": true, "description": "A/B experiment variant ID when this offer is part of an experiment." } } }, "OfferDisplayV2": { "type": "object", "description": "Presentation copy for the user-offer card. Distinct from the legacy `OfferDisplay` schema used by `/iap/products/` offer config — field names are simplified and the copy is localized + interpolated server-side.", "required": [ "title", "body", "cta_text", "dismiss_text", "accepted_title", "accepted_body", "completed_title", "completed_body" ], "properties": { "title": { "type": "string" }, "body": { "type": "string" }, "cta_text": { "type": "string" }, "dismiss_text": { "type": "string" }, "accepted_title": { "type": "string" }, "accepted_body": { "type": "string" }, "completed_title": { "type": "string" }, "completed_body": { "type": "string" }, "apple_cancel_instructions": { "type": "string", "description": "Localized instructions shown in the accepted-state card when `requires_apple_cancel` is `true`. May be empty." } } }, "OfferProration": { "type": "object", "description": "Stripe proration preview for web-to-web upgrades. Present only when `action_type` is `upgrade_web_to_web`.", "required": [ "amount_cents", "currency" ], "properties": { "amount_cents": { "type": "integer", "description": "Credit applied from the old subscription, in minor currency units. Negative values represent money owed back to the user." }, "currency": { "type": "string", "description": "ISO-4217 currency code for the proration amount." }, "next_billing_date": { "type": "string", "format": "date-time", "nullable": true, "description": "When the customer will next be billed on the upgraded plan." } } }, "AppleSubscriptionSummary": { "type": "object", "description": "Normalized view of the user's Apple (StoreKit) subscription. Present only when the user currently has — or recently had — a StoreKit entitlement. Clients should rely on `is_active` rather than deriving activeness from `status_code`.", "required": [ "is_active", "status_code", "auto_renew_enabled" ], "properties": { "is_active": { "type": "boolean", "description": "Canonical active-entitlement flag, normalized server-side from the Apple status code." }, "expires_at": { "type": "string", "format": "date-time", "nullable": true }, "status_code": { "type": "integer", "description": "Raw Apple status code (1=active, 2=expired, 3=billing_retry, 4=grace_period, 5=revoked). Diagnostic only — prefer `is_active`." }, "auto_renew_enabled": { "type": "boolean" } } }, "Error": { "type": "object", "description": "Error response.", "properties": { "error": { "type": "string", "description": "Human-readable error message." }, "code": { "type": "string", "description": "Machine-readable error code (not always present)." } }, "required": [ "error" ] } }, "responses": { "BadRequest": { "description": "Invalid request -- missing or malformed parameters.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "Unauthorized": { "description": "Invalid or missing API key.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "Forbidden": { "description": "Account disabled or insufficient permissions.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "NotFound": { "description": "Resource not found.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }, "UpstreamError": { "description": "Upstream service error (Stripe, Apple).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } } } } }