Skip to main content
After a user completes (or cancels) checkout in a browser, ZeroSettle redirects them back to your app via a universal link (iOS) or deep link (Android). This guide covers the setup required to receive these callbacks.

How It Works

  1. User initiates purchase via .checkoutSheet() (iOS) or the platform-specific purchase API
  2. SDK opens Stripe checkout in Safari
  3. User completes payment (or cancels)
  4. Stripe redirects to https://api.zerosettle.io/checkout/callback?...
  5. iOS opens your app via universal link
  6. SDK parses the callback and updates entitlements

Step 1: Add Associated Domains Entitlement

In Xcode, add the Associated Domains capability to your app target:
  1. Select your app target in Xcode
  2. Go to Signing & Capabilities
  3. Click + Capability
  4. Select Associated Domains
  5. Add: applinks:api.zerosettle.io
Your entitlements file should include:
<key>com.apple.developer.associated-domains</key>
<array>
    <string>applinks:api.zerosettle.io</string>
</array>

Developer Mode

During development, add ?mode=developer to bypass Apple’s AASA caching:
<key>com.apple.developer.associated-domains</key>
<array>
    <string>applinks:api.zerosettle.io?mode=developer</string>
</array>
ModeBehavior
NormaliOS caches the AASA file and refreshes infrequently. Changes can take hours to propagate.
DeveloperiOS fetches the AASA file directly on each app install, bypassing CDN caching.
Use ?mode=developer during development for faster iteration. Remove it before submitting to the App Store—developer mode only works on devices registered in your Apple Developer account.
Associated Domains requires a paid Apple Developer account. The capability won’t work with free provisioning profiles.

Step 2: Register Your App ID

Contact ZeroSettle support to register your app’s bundle ID and team ID for universal link handling. You’ll need to provide:
FieldExampleWhere to Find
Team IDYUM5K9J52MApple Developer Portal → Membership
Bundle IDcom.yourcompany.yourappXcode → Target → General
ZeroSettle will add your app to the apple-app-site-association file hosted at https://api.zerosettle.io/.well-known/apple-app-site-association.

Step 3: Handle the Callback

SwiftUI

Use the .zeroSettleHandler() view modifier on your root view (or handle deep links in your Android Activity):
@main
struct YourApp: App {
    var body: some Scene {
        WindowGroup {
            ContentView()
                .zeroSettleHandler()
        }
    }
}
This automatically handles both onOpenURL and onContinueUserActivity for you on iOS. On Android, handleDeepLink parses the callback URI, and onResume lets the SDK poll for transaction status when the user returns from the browser.

UIKit (SceneDelegate)

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
        guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
              let url = userActivity.webpageURL else {
            return
        }
        ZeroSettle.shared.handleUniversalLink(url)
    }
}

UIKit (AppDelegate - iOS 12 and earlier)

class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        continue userActivity: NSUserActivity,
        restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
    ) -> Bool {
        guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
              let url = userActivity.webpageURL else {
            return false
        }
        return ZeroSettle.shared.handleUniversalLink(url)
    }
}

Step 4: Handle Checkout Results

Use the delegate to respond to checkout completion:
class PurchaseManager: ZeroSettleDelegate {
    init() {
        ZeroSettle.shared.delegate = self
    }

    func zeroSettleCheckoutDidComplete(transaction: ZSTransaction) {
        // Purchase successful - unlock content
        unlockProduct(transaction.productId)
        showSuccessMessage()
    }

    func zeroSettleCheckoutDidCancel(productId: String) {
        // User cancelled - no action needed, or show a prompt
    }

    func zeroSettleCheckoutDidFail(productId: String, error: Error) {
        // Something went wrong
        showError(error.localizedDescription)
    }
}

Callback URL Format

The callback URL contains these parameters:
https://api.zerosettle.io/checkout/callback
    ?transaction_id=txn_abc123
    &product_id=premium_monthly
    &status=success
ParameterDescription
transaction_idUnique transaction identifier
product_idThe product that was purchased
statussuccess or cancelled
You don’t need to parse this URL yourself. The SDK’s handleUniversalLink(_:) method handles parsing and verification.

Simulator Limitations

Universal links don’t work reliably in the iOS Simulator. Test on a physical device.

Testing Checklist

  1. Install a development build on a physical device
  2. Ensure the device has internet access
  3. Start a purchase flow
  4. Complete checkout in Safari
  5. Verify you’re redirected back to the app

Debugging Tips

If the redirect isn’t working:
  1. Check Associated Domains - Verify the entitlement is correctly added in Xcode
  2. Verify Registration - Confirm your app ID is registered with ZeroSettle
  3. Check AASA - Visit https://api.zerosettle.io/.well-known/apple-app-site-association and verify your app ID is listed
  4. Reinstall the App - iOS caches AASA; reinstalling forces a refresh
  5. Check Console Logs - Look for swcd process logs in Console.app

Manual Testing

You can test the link handler directly:
// In your app, simulate a callback
let testURL = URL(string: "https://api.zerosettle.io/checkout/callback?transaction_id=test&product_id=test&status=success")!
ZeroSettle.shared.handleUniversalLink(testURL)

Fallback Handling

If the universal link fails to open your app, users will see the callback page in Safari. This page displays a success/cancel message and provides a button to manually open the app. To handle this case, you can also:
  1. Poll for transaction status - The SDK stores a pending transaction ID that can be verified on next app launch
  2. Restore entitlements - Call restoreEntitlements(userId:) on app launch to sync any missed purchases
// On app launch, restore entitlements to catch missed callbacks
Task {
    let entitlements = try? await ZeroSettle.shared.restoreEntitlements(
        userId: currentUser.id
    )
    updateUI(entitlements)
}

On Android, checkout callbacks arrive via Android App Links (verified deep links). This section covers the equivalent setup for Android apps.
Deep link handling is only needed for Custom Tab and external browser checkout modes. The WebView mode (default) handles callbacks internally — no deep link setup required.

Step 1: Add Intent Filter

In your AndroidManifest.xml, add a deep link intent filter to your main Activity:
<activity android:name=".MainActivity">
    <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data
            android:scheme="https"
            android:host="api.zerosettle.io"
            android:pathPrefix="/checkout/callback" />
    </intent-filter>
</activity>
The android:autoVerify="true" attribute tells Android to verify the domain via a Digital Asset Links file hosted on api.zerosettle.io.

Step 2: Handle the Callback

In your Activity, handle the deep link in onNewIntent:
class MainActivity : ComponentActivity() {
    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        intent.data?.let { uri ->
            ZeroSettle.handleDeepLink(uri)
        }
    }
}

Step 3: Call onResume

On Android, Custom Tabs have no dismiss callback, so the SDK detects the user’s return via onResume. Call ZeroSettle.onResume() from your Activity:
override fun onResume() {
    super.onResume()
    ZeroSettle.onResume()
}
This allows the SDK to poll for transaction status when the user returns from the browser.

Troubleshooting

  • Ensure ZeroSettle.shared.delegate is set before the callback
  • Verify the SDK is configured with configure()
  • Check that handleUniversalLink is being called from onOpenURL or scene delegate