Xee Docs
Test & release

Troubleshooting & errors

View as Markdown

This page covers the problems people hit most often with Xee and lists every error code the iOS SDK can return. Start with the logs: most errors wrap an underlying StoreKit or network error, and verbose logs print it.

Turn on logs

Set the level before you activate, so it applies from the first call:

Swift
Xee.logLevel = .verbose
try await Xee.activate(withAPIKey: "xee_YOUR_SDK_KEY")

You can also change it at any time with Xee.logLevel = .verbose. The levels, from least to most output:

LevelWhat it logs
.errorErrors only.
.warnErrors, plus problems that do not stop the SDK.
.infoWarnings, plus the lifecycle of the SDK's parts. This is the default.
.verboseEverything above, plus each method call and API request. Use it while debugging.
.debugThe most detail. Only for digging into the SDK itself.

Logs go to the system log, so they show in the Xcode console and in Console.app. To also keep them yourself, for example to attach them to a bug report, add a handler. It runs in addition to the default output, not instead of it:

Swift
Xee.setLogHandler { record in
    MyLogFile.append("\(record.date) [\(record.level)] \(record.message)")
}

Switch back to .info or .error before you release.

Handle errors

Every SDK method throws XeeError. Its xeeErrorCode says what kind of failure it was, and originalError holds the StoreKit, network or system error it wraps, when there is one. Errors the SDK raises itself, such as calling a method before activation, have no original error.

Swift
do {
    let paywall = try await Xee.getPaywallVariant(paywallId: "main")
    let products = try await Xee.getPaywallVariantProducts(paywallVariant: paywall)
    // Show the paywall.
} catch let error as XeeError {
    switch error.xeeErrorCode {
    case .networkFailed:
        // Offline or the server is unreachable. Offer a retry.
        break
    case .badRequest:
        // The paywall ID is wrong, or the paywall is not live.
        break
    default:
        print("Xee: \(error), caused by: \(String(describing: error.originalError))")
    }
} catch {
    print("Unexpected: \(error)")
}

XeeError is also an NSError in the XeeErrorDomain domain, and its errorCode is the number in the tables below.

Common problems

No notifications in Events

The Events page lists every notification Apple sent to Xee, and every purchase Xee checked with Apple (VERIFIED_WITH_APPLE). If no notification appears after a purchase:

  • Check that the Notification URL from App settings, iOS SDK is pasted in App Store Connect for both Production and Sandbox, with Version 2 chosen.
  • Check that the Bundle ID in App settings, iOS SDK matches the app. Notifications for another bundle are rejected.
  • If you set the Apple app ID, check it too. Xee rejects a production notification whose app ID does not match.
  • Renewals, expiry and refunds reach Xee only through these notifications, so the Notification URL must be set for both Production and Sandbox, with Version 2.
  • Purchases made with a StoreKit configuration file in Xcode never reach Apple's servers, so no notification is sent. Use a sandbox account, as in Test purchases in the sandbox.

Purchase succeeded but premium is not active

Work through these in order:

  1. The In-App Purchase key. Without it, Xee asks Apple nothing when the SDK reports a purchase, and access waits for Apple's notification. Ask Claude to run list_apps: in_app_purchase_key must be true. Otherwise add the key as in Check subscription status, then call Xee.restorePurchases() to bring in the purchase.
  2. Events. Find the purchase in Events. A VERIFIED_WITH_APPLE event means Xee checked it with Apple; a purchase notification such as SUBSCRIBED means Apple's own message arrived. If neither is there, the check did not run or did not pass.
  3. The check itself. A check that fails never fails the SDK call: the profile comes back as it stands, and Xee's log says why, for example Apple refused the key, the answer was for another bundle ID, or its signature did not verify. Check that the key's Issuer ID and Key ID are right and that the Bundle ID matches your app.
  4. Notifications. If the key is set but the check failed, access still arrives with Apple's notification. See the section above if it does not.
  5. Sandbox or Xcode. A purchase from a StoreKit configuration file in Xcode never unlocks, because Apple has no record of it. Use a sandbox account.
  6. Timing. Refresh the profile with Xee.getProfile() a few seconds after the purchase, or update your UI from didLoadLatestProfile on your XeeDelegate.

Xee gives a subscription to the profile whose app account token the transaction carries, if a profile has it; otherwise to the profile it already belongs to; otherwise to the profile that reported it. A subscription never moves away from a profile that owns it, so a purchase that carried another profile's token shows up there, not on the device that reported it. See Identify users.

notActivated (2002)

The SDK was called before Xee.activate finished, or the server refused the SDK key. Xee answers an unknown key with 401, and the SDK reports any 401 or 403 as notActivated.

  • Await activate (or wait for its completion handler) before any other call, including calls from a splash screen.
  • Copy the SDK key again from App settings, iOS SDK. If someone rotated it, the old key stops working.

badRequest (2003) when getting a paywall

The server could not serve the paywall. Usually the paywall ID is not in this app, the paywall is not live, or it has no version yet. Check the ID on the Paywalls page and that the SDK key belongs to the same app.

noProductIDsFound (1000)

StoreKit returned none of the products the paywall asked for. Check that:

  • The product IDs in Xee match App Store Connect exactly. Run Sync on the Products page.
  • The products have all their metadata in App Store Connect and the Paid Applications agreement is active.
  • The device's storefront sells the products. A sandbox account's country decides this.

cantMakePayments (1003)

In-app purchases are blocked on the device, for example by Screen Time restrictions. You also get it when the SDK runs in observer mode (.with(observerMode: true)): your own code then makes purchases, and makePurchase is not available. Use one or the other.

identify fails

Xee.identify(_:withAppAccountToken:) throws when the server refuses the request for good, and the app keeps the profile it had. The SDK retries only what can pass: a timeout, a 429, a 5xx or no connection. Any other 4xx ends the call with the matching error, as in the tables below. A wrong SDK key gives notActivated; the cases below give badRequest (2003), with a 409 from the server:

  • The token belongs to another profile. Each app account token can be used by one profile. Pass the token generated for this user, not one shared by several accounts or devices.
  • The profile already has a different customer user ID. A profile's customer user ID cannot be changed once set. Call Xee.logout() first, then identify the new user.

An ID that already belongs to another profile is not an error: identify switches the device to that profile. See Identify users.

profileWasChanged (3006)

A call was still running when the user changed, for example a purchase started while Xee.identify was in flight. Await identify (and logout) before anything that depends on the user.

Error codes

StoreKit

These pass on the code StoreKit gave. Read originalError for Apple's full error.

ErrorCodeWhat it means
unknown0Something unexpected failed. Retry, and check the logs.
clientInvalid1The user or device is not allowed to make this request.
paymentCancelled2The user cancelled the payment. Nothing to fix.
paymentInvalid3The App Store did not recognise a payment parameter.
paymentNotAllowed4The user is not allowed to authorize payments on this device.
storeProductNotAvailable5The product is not sold in the user's storefront.
cloudServicePermissionDenied6The user did not allow access to cloud service information.
cloudServiceNetworkConnectionFailed7The device could not connect to the network.
cloudServiceRevoked8The user revoked permission to use the cloud service.
privacyAcknowledgementRequired9The user has not yet accepted Apple's privacy policy.
unauthorizedRequestData10The app used a StoreKit property it lacks the entitlement for.
invalidOfferIdentifier11The offer ID does not exist in App Store Connect, or the offer was revoked.
invalidSignature12The signature on a promotional offer is not valid.
missingOfferParams13A promotional offer is missing parameters.
invalidOfferPrice14The offer's price is no longer valid. An offer must cost less than the product.

Purchases and products

ErrorCodeWhat it means
noProductIDsFound1000None of the requested products could be loaded from the App Store. See above.
productRequestFailed1002Loading products from StoreKit failed. Retry later.
cantMakePayments1003Purchases are not possible on this device, or the SDK is in observer mode. See above.
cantReadReceipt1005No valid receipt on the device. In the sandbox there is none until the first purchase.
productPurchaseFailed1006The purchase failed. originalError holds the StoreKit reason, usually one of codes 0 to 14.
refreshReceiptFailed1010Refreshing the receipt failed.
fetchSubscriptionStatusFailed1020Reading the subscription status from the App Store failed.
unknownTransactionId1030A transaction had no identifier.
paymentPendingError1050The payment is waiting on approval, such as Ask to Buy.

Network and server

ErrorCodeWhat it means
notActivated2002The SDK is not activated yet, or the server refused the SDK key (401 or 403). See above.
badRequest2003The server answered with a 4xx, for example 404 when a paywall is not found or not live, or 409 when identify is refused.
serverError2004The server answered with a 5xx or 429. Retry later.
networkFailed2005The request did not get through: no connection, or a timeout. Check the device's connection and try again.
decodingFailed2006The SDK could not read a response or a stored file.
encodingFailed2009The SDK could not build a request.

General

ErrorCodeWhat it means
analyticsDisabled3000The event was not sent because analytics are turned off for this user.
wrongParam3001A parameter you passed is not valid. The message says which.
activateOnceError3005activate was called more than once. Call it once per launch.
profileWasChanged3006The user changed while the call was running. See above.
unsupportedData3007The data is in a format the SDK does not support.
unidentifiedUserLogout3020logout was called while no user was identified.
fetchTimeoutError3101Loading took longer than the timeout you set.
operationInterrupted9000The system interrupted the operation, for example because its task was cancelled.

Still stuck

  • Reproduce the problem with .verbose logs and keep the full console output.
  • Check the Events page and the user's profile to see what Xee received.
  • Check the Bundle ID and the Notification URL again: Xee rejects a notification with the wrong bundle ID or an unknown URL.