Skip to main content
ZeroSettle acts as your Merchant of Record, which means you never need to set up your own Stripe webhook endpoints or handle raw payment events. ZeroSettle receives all payment lifecycle events from Stripe (and Apple), processes them automatically, and keeps your Transactions and Entitlements up to date. Your app interacts with this system in two ways:
  1. Client-side — the SDK receives real-time updates via delegate callbacks
  2. Server-side — your backend queries the Entitlements API to verify purchase status

How It Works

1

User completes checkout

The user pays via Apple Pay or card through the ZeroSettle checkout sheet. The SDK creates a PaymentIntent on ZeroSettle’s backend.
2

Stripe sends a webhook to ZeroSettle

Stripe fires payment_intent.succeeded (or checkout.session.completed for session-based flows). ZeroSettle receives this at its webhook endpoint with signature verification.
3

ZeroSettle creates Transaction + Entitlement

The webhook handler creates an immutable Transaction record (the payment ledger entry) and an Entitlement (the user’s access grant). For subscriptions, it also creates the Stripe subscription and tracks renewal state.
4

SDK updates client-side

The SDK polls for transaction completion and updates the local entitlement cache. Your delegate callback fires with the updated entitlements.
5

Your backend verifies via API

At any point, your backend can query GET /v1/iap/entitlements to verify a user’s purchase status server-side.
Because ZeroSettle is the Merchant of Record, all webhook signature verification, idempotency handling, and retry logic is managed for you. Every webhook event is logged with an idempotency key to prevent duplicate processing.

Server-Side Verification

For server-side purchase verification (e.g., unlocking content from your API, validating access in a game server, or gating API features), query the Entitlements API.

Query by User ID

curl --request GET \
  --url 'https://api.zerosettle.io/v1/iap/entitlements?user_id=your_user_id' \
  --header 'X-ZeroSettle-Key: zs_pk_live_...'

Query by Email

For anonymous users who purchased without a user account:
curl --request GET \
  --url 'https://api.zerosettle.io/v1/iap/entitlements?email=user@example.com' \
  --header 'X-ZeroSettle-Key: zs_pk_live_...'

Response Format

{
  "entitlements": [
    {
      "id": "txn_abc123",
      "product_id": "premium_monthly",
      "product_type": "auto_renewable",
      "source": "web_checkout",
      "status": "active",
      "is_active": true,
      "expires_at": null,
      "purchased_at": "2025-03-14T12:00:00Z",
      "will_renew": true,
      "is_trial": false,
      "trial_ends_at": null,
      "cancelled_at": null
    }
  ]
}

Server-Side Verification Examples

const response = await fetch(
  `https://api.zerosettle.io/v1/iap/entitlements?user_id=${userId}`,
  {
    headers: {
      'X-ZeroSettle-Key': process.env.ZEROSETTLE_KEY,
    },
  }
);

const { entitlements } = await response.json();
const hasPremium = entitlements.some(
  (e) => e.product_id === 'premium_monthly' && e.is_active
);

if (!hasPremium) {
  return res.status(403).json({ error: 'Premium subscription required' });
}
All API endpoints authenticate with your publishable key (zs_pk_live_... or zs_pk_test_...) via the X-ZeroSettle-Key header — the same key you use in the SDK. Use zs_pk_test_... for sandbox queries and zs_pk_live_... for production.

Transaction History

For a full history of all transactions (including failed, refunded, and expired), use the transaction history endpoint:
curl --request GET \
  --url 'https://api.zerosettle.io/v1/iap/transaction-history?user_id=your_user_id&limit=50' \
  --header 'X-ZeroSettle-Key: zs_pk_live_...'
This returns all transactions regardless of status, including consumed consumables, expired subscriptions, and refunds. The limit parameter defaults to 50 (max 100).

Client-Side Updates

The SDK provides real-time delegate callbacks when entitlements change. These fire after purchases, restores, refunds, and subscription renewals.

Delegate Callbacks

class PurchaseManager: ZeroSettleDelegate {
    init() {
        ZeroSettle.shared.delegate = self
    }

    func zeroSettleEntitlementsDidUpdate(_ entitlements: [Entitlement]) {
        // Fires after any entitlement change:
        // - Purchase completed
        // - Subscription renewed
        // - Refund processed
        // - Restore completed
        for entitlement in entitlements {
            if entitlement.isActive {
                unlockFeature(entitlement.productId)
            } else {
                lockFeature(entitlement.productId)
            }
        }
    }
}
Entitlement updates are triggered by SDK operations (purchases, restores). They are not pushed from the backend in real-time via WebSocket or push notification. For server-authoritative access control, use the Entitlements API on your backend.

Reactive UI Observation

You can also observe entitlements reactively in your UI framework:
struct ContentView: View {
    @ObservedObject var iap = ZeroSettle.shared

    var body: some View {
        if iap.entitlements.contains(where: { $0.isActive }) {
            PremiumContent()
        } else {
            UpgradePrompt()
        }
    }
}

Stripe Webhook Events

ZeroSettle processes the following Stripe webhook events automatically. You do not need to handle these yourself.
EventWhat ZeroSettle Does
payment_intent.succeededMarks transaction as succeeded, creates Identity + Entitlement. For subscriptions, creates the Stripe subscription and handles plan upgrades (cancels old subscription with proration).
payment_intent.processingMoves transaction from pending to processing, freeing the deduplication slot so the user can retry if needed.
payment_intent.payment_failedMarks the pending transaction as failed.
payment_intent.canceledMarks the pending transaction as failed to free the dedup slot.
EventWhat ZeroSettle Does
checkout.session.completedRoutes to IAP checkout handler if payment_status=paid. Creates Transaction + Entitlement.
checkout.session.expiredMarks pending transaction as failed when the checkout session expires without payment.
EventWhat ZeroSettle Does
customer.subscription.updatedHandles cancellation (cancel_at_period_end), pause/resume, and status changes. Entitlement stays active until period end on cancellation.
customer.subscription.deletedDeactivates the entitlement when the subscription is fully terminated.
customer.subscription.pausedMarks entitlement as paused. Access is suspended until resumed.
customer.subscription.resumedReactivates a paused entitlement.
customer.subscription.trial_will_endLogs upcoming trial expiration (~3 days before). Used for Switch & Save migrations with aligned trial periods.
EventWhat ZeroSettle Does
invoice.paidCreates Transaction records for subscription renewals and proration charges. Extends entitlement expiry on renewal. Skips initial subscription_create invoices (handled by checkout flow).
invoice.payment_failedLogs renewal failure context. Entitlement status is handled by customer.subscription.updated (moves to past_due).
invoice.voidedHandles voided invoices for paused subscriptions.
EventWhat ZeroSettle Does
charge.refundedMarks transaction as refunded, deactivates all linked entitlements.
charge.dispute.createdMarks transaction as disputed, revokes all linked entitlements. ZeroSettle handles evidence submission as MoR.
charge.dispute.updatedLogs dispute status changes.
charge.dispute.closedRecords dispute outcome (won/lost).
charge.dispute.funds_withdrawnLogs fund withdrawal.
charge.dispute.funds_reinstatedRe-activates entitlements if dispute is won and funds are returned.
EventWhat ZeroSettle Does
setup_intent.succeededConfirms trial subscription card setup. Finalizes the trial transaction.
setup_intent.setup_failedMarks trial transaction as failed if card setup fails.
account.application.deauthorizedClears stale Stripe references when a connected account revokes platform access.

BYOS (Bring Your Own Stripe)

If you use BYOS mode with your own Stripe account connected via OAuth, ZeroSettle still receives and processes all webhook events through the same endpoint. Stripe routes Connect webhooks to the platform automatically. You do not need to configure a separate webhook endpoint on your connected account.
Do not configure your own webhook endpoint for the same events on your connected Stripe account. Duplicate processing can cause entitlement conflicts.

App Store Server Notifications

ZeroSettle also handles Apple’s App Store Server Notifications V2 (ASSN v2) for StoreKit subscription lifecycle events. If your app uses StoreKit purchases alongside web checkout, these notifications keep entitlements in sync automatically.

Setting Up ASSN v2

Configure the webhook URL in App Store Connect:
1

Open App Store Connect

Navigate to your app in App Store Connect.
2

Go to App Information

Select App Information from the sidebar under General.
3

Configure the webhook URL

Under App Store Server Notifications, set:Production URL:
https://api.zerosettle.io/v1/webhooks/appstore-notifications/
Sandbox URL:
https://api.zerosettle.io/v1/webhooks/appstore-notifications/?sandbox=true
Select Version 2 for the notification version.

Handled Apple Notifications

ZeroSettle processes the following ASSN v2 notification types:
NotificationWhat ZeroSettle Does
REFUNDDeactivates entitlement, marks transaction as refunded
REVOKEDeactivates entitlement (family sharing revocation)
DID_RENEWExtends entitlement expiry to new period end. Applies pending product changes from plan switches.
EXPIREDMarks entitlement as expired and deactivates access
DID_FAIL_TO_RENEWMarks entitlement as grace period or past due depending on subtype
DID_CHANGE_RENEWAL_STATUSHandles auto-renew toggle (marks cancelled or reactivated)
DID_CHANGE_RENEWAL_PREFRecords pending product change for next renewal
GRACE_PERIOD_EXPIREDDeactivates access after billing grace period ends
RENEWAL_EXTENDEDExtends entitlement expiry when Apple grants an extension
SUBSCRIBEDCreates transaction and entitlement for new subscriptions
ONE_TIME_CHARGECreates transaction and entitlement for one-time purchases
OFFER_REDEEMEDRecords offer metadata on the entitlement
REFUND_REVERSEDRe-activates entitlement after a refund reversal
REFUND_DECLINEDLogged only (refund request was declined by Apple)
All Apple notifications are verified using JWS signature verification against Apple’s Root CA certificate chain before processing. Duplicate notifications are automatically skipped via idempotency checks on the notificationUUID.

Notification Forwarding

If your app needs to receive ASSN v2 notifications on your own server (e.g., for analytics or custom logic), you can configure notification forwarding in the ZeroSettle dashboard. When enabled, ZeroSettle processes the notification first, then forwards the original signed payload to your URL.

Best Practices

For content gating on your backend (API access, downloadable content, server-side features), always verify entitlements via the REST API rather than trusting client-side state. The SDK cache is convenient for UI but should not be the source of truth for authorization decisions.
The SDK’s entitlement cache is in-memory only and is cleared when the app process terminates. Call restoreEntitlements() on every app launch to rehydrate the user’s entitlements from the server.
Entitlements will be empty if no purchases have been made. Always handle this case gracefully in your UI and server-side logic.
ZeroSettle handles all Stripe webhook processing, including signature verification, idempotency, and retry logic. Building a parallel webhook handler will cause duplicate entitlement creation and state conflicts.