> This page is part of the [Customer.io documentation](https://docs.customer.io). For the complete index, see [llms.txt](https://docs.customer.io/llms.txt).
> Last updated: August 28, 2026
> To take actions in Customer.io from an agent, use the [Customer.io CLI](https://docs.customer.io/ai/cli/get-started.md) or the [MCP server](https://docs.customer.io/ai/mcp/get-started.md).

# iOS 27 UIScene migration guide

Keep Customer.io working when your app adopts UIScene or builds with Xcode 27, covering native iOS, Flutter, React Native, and Expo.

This guide explains how to keep Customer.io working when your iOS app adopts `UIScene` or is built with the iOS 27 SDK. It covers **native iOS, Flutter, React Native, and Expo**. Customer.io’s scene integrations are released, but the React Native and Expo paths also depend on framework releases. The relevant availability is listed below. Scene support is additive: Customer.io does not replace your app’s root lifecycle.

Apple beta software

[Xcode 27 ↗](https://developer.apple.com/documentation/xcode-release-notes/xcode-27-release-notes) and iOS 27 are currently beta releases. Recheck Apple’s current release notes before shipping with the iOS 27 SDK.

## Who needs to migrate

The scene launch requirement applies only when **both** of these are true:

*   Your app is built with the iOS 27 SDK, **and**
*   it runs on iOS 27 or later.

A binary built with an earlier SDK keeps launching normally when a user upgrades to iOS 27, so you don’t have to change anything to keep existing installs working.

UIKit can get your scene configuration from either a nonempty `UIApplicationSceneManifest` or your AppDelegate’s `application(_:configurationForConnecting:options:)` method. Your app needs migration when neither source provides a valid scene configuration. If one of those sources already provides the configuration, your app has adopted the scene lifecycle.

**Existing AppDelegate-only integrations require no Customer.io change** while your toolchain still permits that lifecycle. The scene steps in this guide are additive. Adopt them when you adopt `UIScene` or move to the iOS 27 SDK.

Jump to your platform: [native iOS](#native-ios) · [Flutter](#flutter) · [React Native](#react-native) · [Expo](#expo).

## Customer.io and framework versions

Use these versions for the scene path:

Integration

Baseline for this guide

Native iOS

Use the current `customerio-ios` 4.7.6 release. It uses existing public APIs and does not require a separate scene-specific release.

Flutter

`customer_io` 4.4.0 or newer and Flutter 3.44.8 or newer for a scene host. Flutter’s scene APIs start in 3.38 and become the default in 3.41; 3.44.8 is the supported Customer.io scene floor and also contains a separate Xcode 27 build-tool fix.

React Native

`customerio-reactnative` 6.10.0 or newer with React Native 0.88 or newer for acknowledged scene routing. Version 6.9.0 remains compatible through the earlier `Linking` path. React Native 0.87 and earlier don’t expose the scene URL APIs that the bridge requires. Keep the AppDelegate path until React Native 0.88 is stable.

Expo

`customerio-expo-plugin` 3.9.0 and its required `customerio-reactnative` 6.9.0 peer with Expo SDK 58 or newer. Expo SDK 58 is currently pre-release; stable Expo SDK 57 apps keep the AppDelegate path.

## Shared AppDelegate and UIScene ownership

The division of responsibility is the same on every platform:

*   **You keep application-level setup in your AppDelegate.** A host that adopts `UIScene` continues to initialize Customer.io, register APNs tokens, and configure notification handling at the application level. That doesn’t move to a scene.
*   **Your host or framework owns the scene layer:** the scene manifest, the SceneDelegate, the root window, and the final navigation.
*   **Customer.io reports attribution and offers the destination:** through the configured native callback (native iOS) or the wrapper/bridge your framework provides (Flutter, React Native, Expo). Without a host handoff, the native SDK may fall back to opening the destination through the system.
*   **Customer.io does not add its own SceneDelegate** and does not choose which of your windows owns a URL. Scene support is additive; it never replaces your app’s root lifecycle.

Each platform section below shows the exact handoff for that framework.

## Native iOS

Native iOS uses public APIs in the current `customerio-ios` 4.7.6 release, so it does not need a separate scene-specific release. This section assumes you’ve already set up [push notifications](/integrations/sdk/ios/push/push-setup/) and, if you use them, [Live Activities](/integrations/sdk/ios/live-activities/set-up/). Your existing APNs, FCM, and multiple-provider setups are unchanged by scene adoption.

### No change needed (AppDelegate-only apps)

If your app uses an app-delegate lifecycle, with no `UIApplicationSceneManifest` and no `application(_:configurationForConnecting:options:)`, keep your current integration. Nothing on this page is required while your toolchain still permits that lifecycle.

### Keep initialization in your AppDelegate

A scene-adopting app still initializes Customer.io, registers APNs tokens, and configures `UNUserNotificationCenter` at the application level. Leave `CustomerIO.initialize`, `MessagingPushAPN.initialize` (or `MessagingPushFCM.initialize`), and your notification-center handling in your AppDelegate. Customer.io does not add a SceneDelegate and does not choose which of your windows opens a URL. Your app owns the scene manifest, the SceneDelegate, the root window, and the final navigation.

### Route Customer.io destinations with a deep-link callback

Under `UIScene`, iOS delivers universal links to `scene(_:continue:)` instead of your AppDelegate. The SDK’s own fallback still calls `application(_:continue:restorationHandler:)` directly for `http` and `https` destinations, but it cannot deliver a URL to your SceneDelegate. Configure the callback below to keep scene-based routing under your control.

If your app uses Customer.io deep-link destinations and wants in-app routing, configure `SDKConfigBuilder.deepLinkCallback` where you initialize the SDK. Return `true` when your app takes ownership of the URL. Return `false` to let the SDK try the legacy AppDelegate continuation callback and then open the URL through the system if the AppDelegate doesn’t handle it. Make the ownership decision synchronously and dispatch UI work to the main thread when your router requires it. Apps with no Customer.io destinations, or that intentionally accept the fallback behavior, don’t need a callback.

```
let config = SDKConfigBuilder(cdpApiKey: cdpApiKey)
    .autoTrackDeviceAttributes(true)
    .autoTrackUIKitScreenViews()
    .migrationSiteId(siteId)
    .deepLinkCallback { url in
        guard canHandleDeepLink(url) else { return false }

        DispatchQueue.main.async {
            handleDeepLink(url)
        }
        return true
    }

CustomerIO.initialize(withConfig: config.build())
```

### Handle URLs and universal links in your SceneDelegate

The system delivers URLs to the scene. Handle cold launches in `scene(_:willConnectTo:options:)` and warm opens in `scene(_:openURLContexts:)` (app-scheme URLs) and `scene(_:continue:)` (universal links). Handle app-scheme URLs (URL contexts) and universal links (user activities) independently.

```
import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?

    func scene(_ scene: UIScene, willConnectTo session: UISceneSession,
               options connectionOptions: UIScene.ConnectionOptions) {
        guard let windowScene = (scene as? UIWindowScene) else { return }
        window = UIWindow(windowScene: windowScene)
        // Set up your root view controller, then window?.makeKeyAndVisible().

        handle(urlContexts: connectionOptions.urlContexts)      // cold app-scheme URLs
        handle(userActivities: connectionOptions.userActivities) // cold universal links
    }

    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
        handle(urlContexts: URLContexts)                         // warm app-scheme URLs
    }

    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
        handle(userActivities: [userActivity])                  // warm universal links
    }

    private func handle(urlContexts: Set<UIOpenURLContext>) {
        for context in urlContexts {
            // Route context.url into your app's navigation.
        }
    }

    private func handle(userActivities: Set<NSUserActivity>) {
        for activity in userActivities {
            guard let url = activity.webpageURL else { continue }
            // Route the universal link into your app's navigation.
        }
    }
}
```

### Optional: Live Activities

This step applies only if you installed the separate [Live Activities](/integrations/sdk/ios/live-activities/set-up/) package. Pass each opened URL through `CustomerIO.liveActivities.handleWidgetUrl(_:)` exactly once before you route the returned destination. On a cold launch, call it from `scene(_:willConnectTo:options:)`. On a warm open, call it from `scene(_:openURLContexts:)`. A `nil` result means the activity had no destination; an ordinary URL is returned unchanged.

Add the Live Activities imports at the top of your `SceneDelegate` file:

```
import CioDataPipelines
import CioLiveActivities
```

Then replace the placeholder `handle(urlContexts:)` method inside `SceneDelegate` with this implementation:

```
private func handle(urlContexts: Set<UIOpenURLContext>) {
    for context in urlContexts {
        guard let destination = CustomerIO.liveActivities.handleWidgetUrl(context.url) else {
            continue
        }
        _ = handleDeepLink(destination)
    }
}
```

### Verify (native iOS)

Run through the [cold and warm verification checklist](#verification-checklist) and confirm the [single-scene support boundary](#single-scene-boundary) applies to your app. Detailed native steps live on the [deep links](/integrations/sdk/ios/push/deep-links/#uiscene-deep-links) and [Live Activities](/integrations/sdk/ios/live-activities/set-up/#uiscene-live-activities) pages.

## Flutter

This section covers the Flutter SDK (`customer_io`). If your app keeps the AppDelegate-only lifecycle, you don’t need to change anything for Customer.io. Follow the steps below only when your app adopts `UIScene` or you build with Xcode 27.

### When Flutter apps need to migrate

*   Flutter’s scene APIs are available from **Flutter 3.38** and become the default in **Flutter 3.41**.
*   Customer.io supports scene hosts on **Flutter 3.44.8 or newer**. This version also contains a separate Flutter build-tool fix required for **Xcode 27**.
*   Use **`customer_io` 4.4.0 or newer** on a scene-adopting app. AppDelegate-only apps keep the package’s published Flutter minimum.

### Let Flutter own the migration

Follow Flutter’s official [UIScene adoption guide ↗](https://docs.flutter.dev/release/breaking-changes/uiscenedelegate) for the framework migration. This page only describes the Customer.io-specific result of that migration.

After you migrate, your iOS host has:

*   **`CioAppDelegateWrapper` still as your application delegate.** Keep `@main class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {}`.

*   **The wrapped `AppDelegate` conforming to `FlutterImplicitEngineDelegate`**, with `GeneratedPluginRegistrant` registration moved out of `didFinishLaunchingWithOptions` and into `didInitializeImplicitFlutterEngine(_:)`:

    ```
    import UIKit
    import Flutter
    import CioMessagingPushFCM
    import CioFirebaseWrapper
    import FirebaseMessaging
    import FirebaseCore

    @main
    class AppDelegateWithCioIntegration: CioAppDelegateWrapper<AppDelegate> {}

    @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
        // Register your Flutter plugins here instead of in didFinishLaunchingWithOptions.
        func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
            GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
        }

        override func application(
            _ application: UIApplication,
            didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
        ) -> Bool {
            // Customer.io init, APNs, and notification handling stay at the application level.
            // FirebaseApp.configure()
            MessagingPushFCM.initialize(
                withConfig: MessagingPushConfigBuilder().build()
            )
            UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate

            return super.application(application, didFinishLaunchingWithOptions: launchOptions)
        }

        override func application(_ application: UIApplication,
                                 didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
            super.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
            Messaging.messaging().apnsToken = deviceToken
        }
    }
    ```

*   **A standard `UIApplicationSceneManifest`.** Flutter only auto-migrates an unmodified template AppDelegate. A Customer.io integration customizes that file, so add the standard single-scene manifest manually:

    ```
    <key>UIApplicationSceneManifest</key>
    <dict>
        <key>UIApplicationSupportsMultipleScenes</key>
        <false/>
        <key>UISceneConfigurations</key>
        <dict>
            <key>UIWindowSceneSessionRoleApplication</key>
            <array>
                <dict>
                    <key>UISceneClassName</key>
                    <string>UIWindowScene</string>
                    <key>UISceneDelegateClassName</key>
                    <string>FlutterSceneDelegate</string>
                    <key>UISceneConfigurationName</key>
                    <string>flutter</string>
                    <key>UISceneStoryboardFile</key>
                    <string>Main</string>
                </dict>
            </array>
        </dict>
    </dict>
    ```

*   **`FlutterSceneDelegate` as your scene delegate.** The manifest above uses Flutter’s delegate directly, so you don’t need to create a `SceneDelegate` class.

    If you need a custom scene delegate, subclass `FlutterSceneDelegate`:

    ```
    import Flutter
    import UIKit

    class SceneDelegate: FlutterSceneDelegate {
    }
    ```

    Then change the manifest’s `UISceneDelegateClassName` to `$(PRODUCT_MODULE_NAME).SceneDelegate`. If you write a custom scene delegate that does **not** subclass `FlutterSceneDelegate`, it must conform to Flutter’s `FlutterSceneLifeCycleProvider` and forward every scene lifecycle callback to Flutter. Point `UISceneDelegateClassName` at your class in that case too.

### How Customer.io routing behaves under UIScene

*   **Push and in-app destinations route through Flutter automatically** when the standard scene manifest is present and Flutter deep linking is enabled (`FlutterDeepLinkingEnabled` is not set to `false`). The plugin installs its SDK deep-link callback during plugin registration, so a cold push tap that arrives before `CustomerIO.initialize` runs is still delivered to your Dart router.
*   **Ordinary links stay owned by Flutter.** Customer.io does not claim links it did not issue.
*   **Live Activity links** are unwrapped and attributed by Customer.io, which then offers the resulting destination to Flutter. In a scene host, `http`/`https` destinations from the SDK also reach the Dart router, so your **host router owns unknown-route and browser-opening policy**.
*   Live Activity **scene** attribution additionally requires the opt-in Live Activities module **and** a Flutter registrar that supports scene delegates (Flutter 3.44.8 or newer). Without the Live Activities module, push/in-app deep-link routing still works, but scene-delivered Live Activity taps are not attributed. See [Set up live notifications](/integrations/sdk/flutter/live-notifications/set-up/).

Warning

Don’t set the native `SDKConfigBuilder.deepLinkCallback` in a Flutter host. The Flutter plugin owns the SDK’s single callback slot and uses it to deliver Customer.io destinations to Dart.

### Advanced: `FlutterDeepLinkingEnabled = false`

If you set `FlutterDeepLinkingEnabled` to `false`, the plugin registers **neither** its scene adapter **nor** its SDK deep-link callback. Your host then owns **all** Customer.io destinations, including push taps, and must process Live Activity URLs before your own router handles them.

## React Native

These steps apply to hosts using `customerio-reactnative` **6.10.0 or newer** with **React Native 0.88 or newer**. Version 6.10.0 adds an acknowledged handler so JavaScript can accept or decline a Customer.io destination before the native SDK decides whether to fall back. If your app still uses the AppDelegate lifecycle, you don’t need any change here. Your existing integration keeps working.

React Native framework requirement

React Native 0.87 and earlier don’t expose the scene URL APIs that the Customer.io bridge requires. Keep the AppDelegate lifecycle and its existing deep-link integration until React Native 0.88 is available as a stable release. Installing `customerio-reactnative` 6.10.0 alone doesn’t add those framework APIs.

### Deep links (required for scene hosts)

A scene-adopting host owns its `SceneDelegate`. Four things are required, in order:

1.  **Install acknowledged deep-link routing before React Native starts.** Call this first in `scene(_:willConnectTo:options:)`, before you start the React Native factory:

    ```
    import customerio_reactnative

    NativeCustomerIO.configureAcknowledgedSceneDeepLinkRouting()
    ```

2.  **Pass cold connection options when React Native starts.** This preserves ordinary app-scheme URLs and universal links delivered when the app is terminated:

    ```
    reactNativeFactory?.startReactNative(
      withModuleName: "YourApp",
      in: window,
      connectionOptions: connectionOptions
    )
    ```

    If you use Customer.io Live Activities, use the `launchOptions` call in the next section instead. That helper attributes a Live Activity tap and preserves ordinary connection URLs for React Native.

3.  **Forward warm scene URLs and universal links to React Native.** These callbacks preserve your existing app links independently of Customer.io destinations:

    ```
    import React

    func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
      RCTLinkingManager.scene(scene, openURLContexts: URLContexts)
    }

    func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
      RCTLinkingManager.scene(scene, continue: userActivity)
    }
    ```

    If you use Customer.io Live Activities, replace the URL-context callback with the `handleAndRouteWidgetUrl` version in the next section. It passes ordinary URLs through to React Native as well.

4.  **Register the Customer.io deep-link handler before `CustomerIO.initialize`.** Return `true` after your router accepts a destination. Return `false` to use the native fallback.

    ```
    import { useEffect } from 'react';
    import { CustomerIO, CioRegion } from 'customerio-reactnative';

    useEffect(() => {
      const subscription = CustomerIO.setDeepLinkHandler(async (url) => {
        if (!canRouteInApp(url)) {
          return false;
        }

        await routeInApp(url);
        return true;
      });

      CustomerIO.initialize({
        cdpApiKey: 'your-cdp-api-key',
        region: CioRegion.US,
      });

      return () => subscription.remove();
    }, []);
    ```

**How routing behaves in a scene host:**

*   Customer.io push, in-app, and inbox destinations are **buffered during cold launch** and then delivered after the acknowledged handler registers.
*   The handler has **ten seconds** to return a result. Returning `false`, throwing, rejecting, missing registration, or timing out makes the SDK try the host AppDelegate. It then sends an app-owned custom scheme to React Native `Linking`, or opens another URL through the system.
*   Keep a `Linking` listener when your handler might decline an app-owned custom scheme.
*   A handler that routes after the timeout can cause a second navigation because its late result can’t cancel a fallback that already ran.
*   The **JavaScript handler owns the routing decision**. Return `true` only after your app accepts the destination.

Backward-compatible Linking path

Version 6.9.0 scene integrations can keep `NativeCustomerIO.configureSceneDeepLinkRouting()` and a `Linking` listener registered before `CustomerIO.initialize`. That path still buffers cold URLs, but a `Linking` event can’t report whether JavaScript handled it. The SDK therefore can’t safely fall back after publishing the event. Prefer the acknowledged handler for new integrations.

### Live Activities in a scene host (optional)

Only applies if you installed the Live Activities module. In a `UIScene` host, handle both lifecycle paths.

**Cold scene connection:** pass launch options through the wrapper so a cold Live Activity tap is attributed and React Native receives the customer’s destination instead of Customer.io’s internal tracking URL:

```
import customerio_reactnative

reactNativeFactory?.startReactNative(
  withModuleName: "YourApp",
  in: window,
  launchOptions: NativeLiveActivities.reactNativeLaunchOptions(from: connectionOptions)
)
```

**Warm scene URL opens:** report the tap and route each remaining URL through React Native Linking:

```
import customerio_reactnative

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
  for context in URLContexts {
    NativeLiveActivities.handleAndRouteWidgetUrl(context.url)
  }
}
```

Do **not** also pass those URLs to `RCTLinkingManager`. `handleAndRouteWidgetUrl` already sends them through the Customer.io scene bridge, and forwarding them again can deliver the same URL twice.

For an AppDelegate-only (legacy) host, keep the existing `NativeLiveActivities.handleWidgetUrl` example. See the [React Native live notifications setup](/integrations/sdk/react-native/live-notifications/set-up/).

### Unsupported: multiple simultaneous window scenes

This release’s compatibility scope is **one simultaneous window scene**. Multiple simultaneous React Native window scenes are outside this release. React Native’s URL notification is process-wide, so SDK-published destinations may reach every connected React Native instance rather than one selected window.

## Expo

The Customer.io Expo config plugin follows the lifecycle that Expo generates for your project. Expo owns the scene lifecycle, and the plugin configures the native Customer.io bridge to match it.

Expo framework requirement

Expo SDK 58 is currently pre-release. Stable Expo SDK 57 apps keep the AppDelegate path and don’t need a scene-related Customer.io change. Apply the scene instructions below after Expo SDK 58 is available as a stable release.

### When the scene path applies

The scene path applies when **all** of these are true:

*   Your app targets **Expo SDK 58 or newer**, which generates a `UIScene`\-based iOS project.
*   You use **`customerio-expo-plugin` 3.9.0 or newer** with its required **`customerio-reactnative` 6.9.0** peer.

Below Expo SDK 58, the plugin keeps the AppDelegate path and **no scene-related change is required**. Your existing setup keeps working while your toolchain still supports that lifecycle.

Note

Expo owns the scene manifest, `SceneDelegate`, root window, and final navigation. Customer.io reports attribution and hands your app the destination to route. Scene support is additive. Customer.io never replaces your app’s root lifecycle.

### Deep link readiness

The readiness step depends on how you initialize Customer.io:

*   **Native auto-initialization (`config` in `app.json`):** register your router first, then call `CustomerIO.setDeepLinkRoutingReady()`.
*   **JavaScript initialization:** register your router first, then call `CustomerIO.initialize(...)`. Initialization marks scene routing ready automatically, so don’t also call `setDeepLinkRoutingReady()`.

For native auto-initialization:

1.  Register your React Native `Linking` URL listener.
2.  Call `CustomerIO.setDeepLinkRoutingReady()`.

```
import { Linking } from 'react-native';
import { CustomerIO } from 'customerio-reactnative';

// Register your URL listener first...
Linking.addEventListener('url', ({ url }) => {
  // your app's navigation
});

// ...then signal that URL handling is ready.
CustomerIO.setDeepLinkRoutingReady();
```

*   **Apps without Expo Router** register their `Linking` listener first, then call `setDeepLinkRoutingReady()`.
*   **Expo Router apps** must **not** add a second navigating `Linking` listener. Expo Router already owns that subscription. Call `setDeepLinkRoutingReady()` once your router is ready.

Customer.io buffers cold-start notification destinations and delivers them to your `Linking` listener as `url` events **after** you signal readiness. They do **not** come back from `Linking.getInitialURL()`. If readiness isn’t signaled within ten seconds, Customer.io falls back to opening the destination through the system.

handleDeeplinkInKilledState is legacy

`handleDeeplinkInKilledState` is the legacy AppDelegate killed-state workaround for Expo SDK 53–57. It is **not** injected for the Expo SDK 58+ scene lifecycle, because scene routing replaces it. Setting it on a scene project has no effect.

### Live Activity links on the scene path

The scene path removes the AppDelegate Live Activity URL handling the plugin used to inject. Live Activity attribution therefore moves to JavaScript, so the `+native-intent.tsx` (or central `Linking`) step below is **required in the same upgrade** if you use Live Activities.

Process each incoming URL through `CustomerIO.liveActivities.handleWidgetUrl(path)` **exactly once**. The helper reports the opened event, then returns the destination to route. An ordinary URL passes through unchanged, and a Customer.io tracking URL with no destination returns `null`.

**Expo Router:** process Live Activity URLs in a top-level `app/+native-intent.tsx`:

```
import { CustomerIO } from 'customerio-reactnative';

export async function redirectSystemPath({ path }: { path: string }) {
  return CustomerIO.liveActivities.handleWidgetUrl(path);
}
```

If the helper returns `null`, Expo Router performs no redirect and keeps the current path.

**Apps without Expo Router:** apply the same helper exactly once in your central initial-URL and URL-subscription pipeline.

Warning

Don’t call `handleWidgetUrl` from both `+native-intent.tsx` and a `Linking` listener. Processing the same tracking URL twice reports two opened events.

## Cold and warm verification checklist

For each integration you use, verify:

*   The app launches from a terminated (cold) state.
*   A notification tap reaches the intended destination.
*   App-scheme links and universal links work in both **cold** and **warm** states.
*   Live Activity taps report attribution and route the destination **once** (if you use Live Activities).
*   Non-Customer.io notifications and any third-party routers still receive their callbacks.
*   Your existing APNs, FCM, and multiple-provider setups are unchanged by scene adoption. See the [iOS push setup](/integrations/sdk/ios/push/push-setup/) and [push certificates](/integrations/sdk/ios/push/push-certificates/) pages rather than re-configuring anything here.

An iOS 16 or newer Simulator on macOS 13 or newer can receive remote notifications from the APNs sandbox when its physical Mac has Apple silicon or an Apple T2 Security Chip. Use a physical device for production APNs or device-specific behavior. A simulator inside a virtualized macOS runner may not receive an APNs token.

## Single-scene support boundary

The supported compatibility scope is **one simultaneous window scene**. That covers the vast majority of iPhone and iPad apps.

Multiple simultaneous window scenes (multi-window ownership, for example, several windows of the same app open side by side on iPad or macOS) are **out of scope for this release** and aren’t yet supported by the SDK. Adopt the single-scene path described here; don’t rely on multi-window scene ownership.

## Troubleshooting

**A deep link opens in the browser (or the system) instead of your app.** The host handoff isn’t configured, so the SDK used its system-open fallback. Configure the platform’s callback: native iOS uses `SDKConfigBuilder.deepLinkCallback`; the framework platforms use their bridge/wrapper as described in each section.

**A notification tap does nothing on a cold launch.** Make sure your scene handler reads the launch URL/user activity from the connection options in `scene(_:willConnectTo:options:)` (or the framework equivalent), not only the warm-open callbacks.

**A Live Activity tap is attributed twice, or not at all.** Route each opened URL through the Live Activity handler exactly once before your own routing. Don’t also forward the same URL to a second handler.

**Universal links don’t reach your app after migration.** Universal-link ownership moves to the scene callback (`scene(_:continue:)` on native iOS). Confirm your associated-domains setup is intact and your scene handler processes user activities.

[GitHub project](https://github.com/customerio/customerio-ios "View the iOS SDK on GitHub")

Version

4.7.6 (Current)3.x (3.14.0)2.x (2.14.2)1.x (1.2.9)

©2026 Peaberry Software, Inc. [Status](https://status.customerio.com/) [Terms of Service](https://customer.io/legal/terms-of-service/) [Privacy Policy](https://customer.io/legal/privacy-policy/)

[](https://www.linkedin.com/company/customer-io)[](https://twitter.com/customerio)[](https://www.youtube.com/channel/UCkCaWdezRoa8ZyR9pEVaipA)[](https://www.instagram.com/customer.io/)
