Using your own backend

If you handle subscription logic and receipt validation on your own backend, follow this guide to keep Superwall in sync.

Superwall works out of the box without any backend integration. You only need this guide if your own server is involved in subscription logic.

Common reasons to integrate your own backend include:

  • You validate receipts server-side (via the App Store Server API or the Google Play Developer API) instead of trusting the device.
  • Your app's entitlements are served from your own API rather than derived from the local receipt.
  • You share subscription state across platforms (for example, a subscription purchased on the web that unlocks features in your app).

You can integrate your own backend with Superwall in one of two ways:

  1. Letting Superwall handle purchases (recommended): Keep Superwall's default purchasing behavior and forward completed transactions to your backend for validation and record-keeping.
  2. Using a purchase controller: Take end-to-end control of the purchasing pipeline: you make the purchase, validate it on your server, and tell Superwall the user's subscription status.
You only need a PurchaseController if your backend must be the source of truth for the subscription status Superwall uses on-device. If you just need your server to know about purchases, the first option is much simpler.

Letting Superwall handle purchases

By default, Superwall handles purchasing, restoring, and subscription status for you — there's nothing to configure. To keep your backend informed, listen for completed transactions and send them to your server for validation.

1. Forward transactions to your backend

Use the Superwall delegate to respond to completed transactions and send their identifiers to your backend. Your backend can then verify the purchase directly with Apple or Google:

class SWDelegate: SuperwallDelegate {
  func handleSuperwallEvent(withInfo eventInfo: SuperwallEventInfo) {
    switch eventInfo.event {
    case .transactionComplete(let transaction, let product, _, _):
      Task {
        // Send the transaction to your backend. Your backend can verify it
        // with the App Store Server API using the transaction identifier.
        await sendTransactionToBackend(
          transactionId: transaction?.storeTransactionId,
          originalTransactionId: transaction?.originalTransactionIdentifier,
          productId: product.productIdentifier
        )
      }
    case .transactionRestore(let restoreType, let paywallInfo):
      print("transactionRestore restoreType \(restoreType) \(paywallInfo)")
    default:
      break
    }
  }
}

2. Keep your backend in sync with webhooks

Renewals, cancellations, refunds, and billing issues happen off-device, so listening in the app isn't enough. Superwall can send webhooks to your backend for subscription and payment events — use them to keep your server's subscription records up to date without polling store APIs yourself.

When Superwall handles purchases, don't set subscriptionStatus yourself — the SDK updates it automatically after purchases, restores, and receipt checks. If your backend needs to control the status Superwall uses on-device, use a purchase controller instead.

Using a purchase controller

With a PurchaseController, you take over purchasing and restoring, and you become responsible for setting Superwall.shared.subscriptionStatus from your backend's entitlement state. See Purchases and Subscription Status for a full explanation of this mode.

1. Create a PurchaseController

Create a new file called MyBackendPurchaseController, then copy and paste the following. Replace fetchSubscriptionStatusFromBackend() and sendTransactionToBackend(...) with calls to your own API — they're placeholders for whatever your backend exposes:

import SuperwallKit
import StoreKit

enum PurchasingError: LocalizedError {
  case sk2ProductNotFound
  case unverifiedTransaction

  var errorDescription: String? {
    switch self {
    case .sk2ProductNotFound:
      return "Superwall didn't pass a StoreKit 2 product to purchase. Are you sure you're not "
        + "configuring Superwall with a SuperwallOption to use StoreKit 1?"
    case .unverifiedTransaction:
      return "The transaction couldn't be verified."
    }
  }
}

final class MyBackendPurchaseController: PurchaseController {
  // MARK: Sync Subscription Status
  /// Fetches the user's entitlements from your backend and makes sure
  /// Superwall knows about them by setting `Superwall.shared.subscriptionStatus`.
  func syncSubscriptionStatus() async {
    do {
      // Ask your backend which entitlements this user has, e.g. ["pro"].
      let entitlementIds = try await fetchSubscriptionStatusFromBackend()
      let entitlements = Set(entitlementIds.map { Entitlement(id: $0) })
      await MainActor.run {
        if entitlements.isEmpty {
          Superwall.shared.subscriptionStatus = .inactive
        } else {
          Superwall.shared.subscriptionStatus = .active(entitlements)
        }
      }
    } catch {
      // Keep the previous cached status and retry later,
      // e.g. next time the app comes to the foreground.
    }
  }

  // MARK: Handle Purchases
  /// Purchases the product with StoreKit 2, sends the signed transaction to
  /// your backend for validation, and returns the result. This gets called
  /// when someone tries to purchase a product on one of your paywalls.
  func purchase(product: SuperwallKit.StoreProduct) async -> PurchaseResult {
    guard let sk2Product = product.sk2Product else {
      return .failed(PurchasingError.sk2ProductNotFound)
    }
    do {
      let result = try await sk2Product.purchase()
      switch result {
      case .success(let verificationResult):
        guard case .verified(let transaction) = verificationResult else {
          return .failed(PurchasingError.unverifiedTransaction)
        }
        // Send the signed transaction to your backend. Your backend validates
        // it (e.g. with the App Store Server API) and updates the user's
        // entitlements before responding.
        try await sendTransactionToBackend(jws: verificationResult.jwsRepresentation)
        await transaction.finish()
        await syncSubscriptionStatus()
        return .purchased
      case .userCancelled:
        return .cancelled
      case .pending:
        return .pending
      @unknown default:
        return .cancelled
      }
    } catch {
      return .failed(error)
    }
  }

  // MARK: Handle Restores
  /// Restores purchases and returns `.restored`, unless an error is thrown.
  /// This gets called when someone tries to restore purchases on one of your paywalls.
  func restorePurchases() async -> RestorationResult {
    do {
      try await AppStore.sync()
      // Re-fetch entitlements from your backend after the restore.
      await syncSubscriptionStatus()
      return .restored
    } catch {
      return .failed(error)
    }
  }
}

As discussed in Purchases and Subscription Status, this PurchaseController is responsible for handling the subscription-related logic. Take a few moments to look through the code to understand how it does this.

2. Configure Superwall

Initialize an instance of MyBackendPurchaseController and pass it in to Superwall.configure(apiKey:purchaseController:):

let purchaseController = MyBackendPurchaseController()

Superwall.configure(
  apiKey: "MY_API_KEY",
  purchaseController: purchaseController
)

// Make sure we sync the subscription status on first load
Task {
  await purchaseController.syncSubscriptionStatus()
}

3. Sync the subscription status

Until you set the subscription status, it's .unknown — and paywall presentation is automatically delayed until it changes. Call syncSubscriptionStatus() when the app launches and whenever your backend's entitlement state changes, so Superwall always knows whether to show a paywall.

Keeping the status in sync across launches and devices

Your backend is the source of truth, so treat the on-device status as a cache of it:

  • Identify users consistently. Call identify with the same user ID your backend uses so entitlements follow the user across devices. See User Management.
  • Fetch entitlements on launch. Call your backend's entitlement endpoint when the app launches (and when it returns to the foreground) and update the subscription status with the result. subscriptionStatus is cached between app launches, so the previous value is used until your fetch completes.
  • Make server notifications your source of truth. Renewals, cancellations, refunds, and billing issues happen off-device. Subscribe your backend to App Store Server Notifications and Google Play's Real-time Developer Notifications so its entitlement records stay correct — the app then picks up changes the next time it syncs.

How is this guide?

On this page