> ## Documentation Index
> Fetch the complete documentation index at: https://docs.autosana.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Testing Push Notifications

> Test real APNs and FCM delivery on Autosana virtual devices.

Autosana can test the complete push-notification journey on iOS Simulators and Android Emulators: your app registers for remote notifications, your backend or managed provider sends a real notification, and the flow verifies both delivery and deep-link behavior.

Use this guide for visible notifications such as alerts, reminders, messages, and order updates.

<Info>
  Push-notification testing on physical iOS and Android devices requires an
  enterprise contract. [Contact us](mailto:founders@autosana.ai) for
  private-device availability and setup.
</Info>

## How a push test works

A typical test follows the same path as a real user:

1. Autosana installs and opens your test build.
2. The app requests notification permission and registers for remote notifications.
3. The app associates the resulting token or provider subscription with the test user.
4. The flow performs the action that schedules the notification, or runs a [runtime hook](/hooks#runtime-hooks) that calls your existing test endpoint.
5. The agent presses Home and waits for the notification.
6. The agent verifies the notification, taps it, and checks the destination screen.

Autosana does not need your APNs key or Firebase service-account credentials. Your backend or managed provider remains responsible for sending the notification. If a runtime hook triggers the push, store only the test endpoint URL and its API key in Autosana. Keep the APNs `.p8` file, Key ID, and Team ID—or the equivalent provider credentials—on the backend that sends the notification.

Depending on the integration, your backend may store a raw APNs device token, an FCM or Expo registration token, or a managed-provider subscription.

## Prepare your test environment

Before writing the flow:

1. Choose a test account that can safely receive notifications in your development or staging environment.
2. Register for remote notifications on every launch. After login, make the app send its latest device or registration token to your backend—or refresh its managed-provider subscription—even when the identifier has not changed.
3. Make sure your backend or provider can confirm that this installation registered and associated its current push identifier with the test account after the run began. For direct APNs, also record the expected bundle ID and environment.
4. Configure the build to use the same push provider and backend environment that the flow will exercise.

<Tip>
  A dedicated account is not required for sequential tests; you can reuse an
  existing automation account. If push tests can overlap, give each run or
  worker a separate account or another unique targeting key. Reusing one account
  can cause a notification to reach the wrong run when registrations overwrite
  one another or multiple registrations remain associated with the same user.
</Tip>

<Warning>
  Treat push registrations as temporary. Installing a new build, clearing app
  data, or resetting a virtual device can change the token or provider
  subscription. Refresh it during every run; do not copy one from an earlier run
  into the flow.
</Warning>

<Tip>
  An automation-friendly notifications screen makes setup failures visible. Keep
  notification authorization and remote-registration status separate. When
  authorization is undetermined, offer the system permission prompt. After the
  user has made a choice, offer **Open Settings**, and re-check the
  authorization and registration state whenever the app returns to the
  foreground.
</Tip>

## Platform requirements

<Tabs>
  <Tab title="iOS Simulator">
    Autosana supplies an iOS 16 or later Simulator runtime. Your app's minimum deployment target can be lower than iOS 16.

    Every layer below must agree:

    | Layer                                 | Required value                                                                                       |
    | ------------------------------------- | ---------------------------------------------------------------------------------------------------- |
    | Artifact architecture                 | `arm64` iOS Simulator `.app` for Autosana's Apple-silicon worker fleet                               |
    | App capability and signed entitlement | Push Notifications enabled and `aps-environment=development`                                         |
    | APNs gateway                          | `api.sandbox.push.apple.com` when sending directly to APNs                                           |
    | APNs credential                       | A Sandbox-enabled key or certificate, or an older APNs key that supports both Sandbox and Production |
    | `apns-topic`                          | The exact `CFBundleIdentifier` of the installed Simulator app                                        |
    | Device registration                   | A token or provider subscription registered and associated by this installation after the run began  |

    The `arm64` architecture is an Autosana worker requirement, not an APNs protocol requirement. Apple also supports remote notifications in compatible Simulators running on Intel Macs with a T2 processor, but Autosana's fleet runs on Apple silicon.

    Configure the Push Notifications capability in Xcode by selecting the project and app target, opening **Signing & Capabilities**, clicking **+ Capability**, and adding **Push Notifications**. With manual signing, also enable Push Notifications for the explicit App ID in **Certificates, Identifiers & Profiles**, then regenerate its development profile.

    Xcode adds `aps-environment=development` to the app target's signed entitlements when you enable the capability and use development signing. It does not go in `Info.plist`. Do not disable code signing or strip the app's signature when producing this build.

    In the app's existing notification integration, request permission for user-visible notifications and make sure the app or SDK registers with APNs. Do not add a native registration call when the SDK already manages this step. Use the integration's token or subscription callback to associate its current identifier with the signed-in test account on every launch or sign-in.

    ### Registration APIs by integration

    #### [Native iOS](https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns)

    Request authorization with `UNUserNotificationCenter`, call `registerForRemoteNotifications()`, and send the APNs token received by the app delegate to your backend. Swift and Objective-C use the corresponding UIKit APIs.

    #### [React Native Firebase](https://rnfirebase.io/messaging/usage)

    Request permission through `@react-native-firebase/messaging`, then associate the FCM token returned by `getToken()` with the test account and update it through `onTokenRefresh()`. The SDK registers with APNs automatically by default; call `registerDeviceForRemoteMessages()` only if your project disables automatic registration.

    #### [Expo notifications](https://docs.expo.dev/versions/latest/sdk/notifications/)

    Request permission with `requestPermissionsAsync()`, retrieve the native APNs token with `getDevicePushTokenAsync()`, and handle changes with `addPushTokenListener()`. Use `getExpoPushTokenAsync()` instead only when your backend sends through the Expo Push Service.

    #### Managed-provider SDKs

    Let the provider SDK manage APNs registration, then use its user-identity or subscription API after sign-in. For example, OneSignal associates its automatically created mobile subscription when the app calls `login()` with the test user's external ID. Do not also upload the raw APNs token unless the provider's integration requires it.

    ### Configure an Expo EAS build

    EAS Build and the Expo Push Service are independent. EAS Build syncs the Apple capabilities declared by your project, but running `eas build` alone does not add the push entitlement.

    Create a dedicated Autosana Simulator profile in `eas.json`:

    ```json theme={null}
    {
      "build": {
        "autosana-simulator": {
          "ios": {
            "simulator": true
          }
        }
      }
    }
    ```

    The `ios.simulator` setting controls the target artifact. You only need `developmentClient: true` if you also want Expo development-client tooling. The `aps-environment=development` entitlement below selects the APNs Sandbox environment for this artifact; it does not turn the app into an Expo development client.

    If the app uses the `expo-notifications` client library, add its config plugin:

    ```json theme={null}
    {
      "expo": {
        "plugins": ["expo-notifications"]
      }
    }
    ```

    The plugin adds the development APNs entitlement even when your backend sends directly through APNs, FCM, or another provider instead of the Expo Push Service.

    If the app does not use `expo-notifications`, declare the entitlement directly:

    ```json theme={null}
    {
      "expo": {
        "ios": {
          "entitlements": {
            "aps-environment": "development"
          }
        }
      }
    }
    ```

    These app-config options apply through Expo Prebuild and Continuous Native Generation. If the repository contains a checked-in native `ios/` directory, add **Push Notifications** to the app target in Xcode or run Prebuild and commit the generated native changes.

    Create a new EAS build after changing an entitlement; an over-the-air update cannot change the signed app. See [Expo's direct APNs and FCM guide](https://docs.expo.dev/push-notifications/sending-notifications-custom/) for the complete configuration.

    ### Verify the Simulator artifact

    Run this preflight before compressing the `.app`. Set `EXPECTED_BUNDLE_ID` to the `apns-topic` used by your backend:

    ```bash theme={null}
    #!/usr/bin/env bash
    set -euo pipefail

    APP_PATH="/path/to/YourApp.app"
    EXPECTED_BUNDLE_ID="com.example.app"

    EXECUTABLE_NAME=$(/usr/libexec/PlistBuddy \
      -c "Print :CFBundleExecutable" "$APP_PATH/Info.plist")
    BUNDLE_ID=$(/usr/libexec/PlistBuddy \
      -c "Print :CFBundleIdentifier" "$APP_PATH/Info.plist")
    ARCHITECTURES=$(lipo -archs "$APP_PATH/$EXECUTABLE_NAME")
    APNS_ENVIRONMENT=$(codesign -d --entitlements :- "$APP_PATH" 2>/dev/null \
      | plutil -extract aps-environment raw -)

    case " $ARCHITECTURES " in
      *" arm64 "*) ;;
      *) echo "Missing arm64 architecture: $ARCHITECTURES" >&2; exit 1 ;;
    esac

    test "$APNS_ENVIRONMENT" = "development"
    test "$BUNDLE_ID" = "$EXPECTED_BUNDLE_ID"

    printf "architectures=%s\naps-environment=%s\nbundle-id=%s\n" \
      "$ARCHITECTURES" "$APNS_ENVIRONMENT" "$BUNDLE_ID"
    ```

    A successful check prints an architecture list containing `arm64`, `aps-environment=development`, and the bundle ID your backend uses as `apns-topic`. A failed `codesign` or entitlement check means the uploaded build cannot register with APNs.

    <Warning>
      **iOS Simulator tokens only work with the APNs sandbox.** If your backend
      connects to APNs directly, send these tokens to
      `api.sandbox.push.apple.com`. Do not send them to the production endpoint,
      `api.push.apple.com`; APNs will reject the token, commonly with a
      `BadDeviceToken` response.

      If you send through Firebase, Expo, OneSignal, or another push provider,
      you normally do not select the APNs hostname yourself. Make sure that
      provider is configured to deliver this development build through the APNs
      sandbox.
    </Warning>

    ### Configure your push provider

    Your provider must preserve the Simulator token's development environment all the way to APNs. Use the matching instructions below; if your provider is not listed, look for its **development**, **sandbox**, or **APNs environment** setting.

    #### [Direct APNs](https://developer.apple.com/documentation/usernotifications/sending-notification-requests-to-apns)

    Send the Simulator token to `api.sandbox.push.apple.com` and use the app's Simulator bundle ID as the `apns-topic`. Authenticate with a [Sandbox-enabled APNs key](https://developer.apple.com/documentation/usernotifications/establishing-a-token-based-connection-to-apns) or certificate. Older team-scoped keys that support both Sandbox and Production continue to work, but a Production-only key fails against the Sandbox. Do not retry a `BadDeviceToken` response against the production endpoint.

    #### [Firebase Cloud Messaging](https://firebase.google.com/docs/cloud-messaging/ios/get-started)

    Continue sending the FCM registration token through the normal Firebase Admin SDK or FCM HTTP v1 endpoint. In the Firebase console, make sure the iOS app has a development APNs authentication key or certificate; FCM handles the connection to the APNs sandbox.

    #### [Expo](https://docs.expo.dev/push-notifications/push-notifications-setup/)

    When using an `ExpoPushToken`, send to the Expo Push Service normally. Install `expo-application` so `expo-notifications` can detect the iOS push environment, or pass `development: true` to `getExpoPushTokenAsync` for the Simulator build. Do not force `development` to `false`. If you retrieve a native APNs token and send it yourself, follow the Direct APNs instructions above.

    #### [OneSignal](https://documentation.onesignal.com/docs/en/ios-sdk-setup)

    Configure the OneSignal app for the APNs development environment. If you provision it through the OneSignal API, set `apns_env` to `development` because the API defaults this field to `production`; use a `.p8` key enabled for both Sandbox and Production.

    #### [Customer.io](https://docs.customer.io/journeys/channels/push/developer-guide/)

    In **Workspace Settings > Push > iOS**, enable **Send all push notifications to sandbox**. Customer.io recommends using a separate workspace for the sandbox environment, which also prevents Simulator registrations from mixing with production users.

    #### [Braze](https://braze-inc.github.io/braze-swift-sdk/documentation/braze/apns-certificate/)

    Use a separate Braze App or App Group for the development build and configure it with the development APNs credential. Braze allows only one active `.p12` certificate per app, so do not replace a live app's production certificate just to test a Simulator build.

    #### [Airship](https://docs.airship.com/developer/sdk-integration/apple/installation/advanced-integration/)

    Initialize the Simulator build with the Airship development/test app key and secret. Let the current Airship SDK infer the APNs environment from the build, or set `inProduction` to `false` when your integration selects it explicitly.

    #### [AWS SNS](https://docs.aws.amazon.com/sns/latest/api/API_CreatePlatformApplication.html)

    Create an SNS platform application whose platform is `APNS_SANDBOX`, register the Simulator token against that application's ARN, and send through the resulting endpoint. Do not register the token under an `APNS` production platform application.

    #### [Azure Notification Hubs](https://learn.microsoft.com/en-us/azure/notification-hubs/configure-apple-push-notification-service)

    Use a separate notification hub configured under **Apple (APNS)** in **Sandbox** mode. Do not switch a shared production hub to sandbox because registrations are tied to the APNs environment.

    #### [Iterable](https://support.iterable.com/hc/en-us/articles/360035112332-Push-Notification-Testing-and-Troubleshooting)

    Configure the Iterable app for `APNS_SANDBOX` and target the token generated by the Simulator build. A production registration or production push credential cannot be used with that token.

    Each Simulator and host Mac combination receives its own token, and the token's length can vary. Store the token as an opaque string without assuming a fixed size or format. Register on every app launch and send the returned token or provider subscription to your backend even when its value matches the previous launch.

    If you use Firebase Cloud Messaging on iOS, use Firebase Apple SDK 10.3.0 or later and configure an APNs authentication key in the matching Firebase project. If Firebase method swizzling is disabled, pass the APNs token to Firebase Messaging in your app code.
  </Tab>

  <Tab title="Android Emulator">
    Upload an `.apk` or `.aab` configured for the Firebase project used by your test environment. The build must:

    * Include Firebase Cloud Messaging and the correct Firebase configuration.
    * Request `POST_NOTIFICATIONS` at runtime on Android 13 or later.
    * Create the notification channel used by the test on Android 8 or later.
    * Upload the newest FCM registration token to your test backend.

    Unlike APNs, FCM does not have a separate sandbox endpoint. Send the Emulator's registration token through the normal Firebase Admin SDK, FCM HTTP v1 endpoint, or managed provider. Use a development or staging Firebase project when you need to keep test registrations and messages separate from production.

    Autosana's Android Emulator image includes Google APIs and Google Play services. If your app enforces Play Integrity or Firebase App Check, configure the provider's supported test or debug path for the Autosana build.
  </Tab>
</Tabs>

See [Building Your Mobile App for Our Cloud](/app-build-guide) for artifact and architecture requirements.

## Create the flow

### Trigger the notification from the app

Use the normal application workflow whenever an in-app action schedules the notification. For example:

```text theme={null}
Log in as ${env:PUSH_TEST_EMAIL}.
Accept notification permission if the app asks for it.
Enable the order-ready reminder for the test order.
Verify that the app confirms the reminder is scheduled.
Press Home so the app is in the background.
Wait up to 30 seconds for an “Order ready” notification.
Verify that the notification appears.
Tap the notification.
Verify that the app opens the test order.
```

The scheduling action and delay belong to your application. Autosana only needs instructions that describe when to press Home, how long to wait, what notification to expect, and what should happen after it is tapped.

### Trigger the notification with a runtime hook

Use a runtime hook when the application has no convenient in-app trigger. The hook should call a narrow test endpoint that tells your backend to send a named notification to the dedicated test user.

1. Add these values to the app's Autosana environment:
   * `PUSH_TEST_URL`
   * `PUSH_TEST_API_KEY` as a secret
   * `PUSH_TEST_USER_ID`

2. Create a cURL hook named `Trigger Test Push`:

```bash theme={null}
curl --fail-with-body -X POST "${env:PUSH_TEST_URL}" \
  -H "Authorization: Bearer ${env:PUSH_TEST_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{"user_id":"${env:PUSH_TEST_USER_ID}","template":"order-ready"}'
```

3. Reference the hook at the correct point in the flow:

```text theme={null}
Log in as the push-notification test user.
Accept notification permission if the app asks for it.
Wait until the app confirms that this installation registered for notifications and associated the registration with this account.
Press Home so the app is in the background.
Run ${hooks:Trigger Test Push}.
Wait up to 30 seconds for an “Order ready” notification.
Verify that the notification appears.
Tap the notification.
Verify that the app opens the test order.
```

Runtime hooks run synchronously, so the agent waits for the endpoint response before continuing. A successful endpoint response confirms only that your backend accepted the request; the notification appearing on the virtual device is the end-to-end assertion.

<Tip>
  Have the test endpoint return a distinct error when the test user has no
  registration confirmed after the current run began. For direct APNs, return
  the APNs status, `reason`, `apns-id`, gateway/environment, and `apns-topic`
  while showing only a redacted device token. This makes registration,
  authentication, routing, and delivery failures easier to distinguish.
</Tip>

## Test the important app states

Create separate flows when your application handles notifications differently in each state:

* **Background:** press Home before sending, then verify and tap the notification.
* **Foreground:** keep the app open and verify the app's foreground presentation or in-app handling.
* **Cold start:** terminate or fully close the app through your normal test workflow, send the notification, and verify the deep-linked destination after tapping it.
* **Notification service extension:** verify any modified title, body, attachment, or category produced by the extension.

Background or silent data delivery is controlled by the operating system and is not guaranteed to run immediately. Use bounded waits, avoid sending notifications repeatedly in a tight loop, and retain physical-device coverage for business-critical background processing.

## Testing notifications on real devices

For device selection, run behavior, and availability, see [Real Device Testing](/real-device-testing). For supported artifacts and standard physical-device build instructions, see [Real device builds](/app-build-guide#real-device-builds-ipa).

Physical push-notification testing uses private devices so the app's original signing and notification entitlements can be preserved. It requires an enterprise contract; [contact us](mailto:founders@autosana.ai) for availability and onboarding.

## Troubleshooting

### Direct APNs responses

APNs returns an HTTP status, an `apns-id` response header, and—for errors—a JSON `reason`. Preserve all three in your backend logs and test-endpoint response.

| Response                                                 | Meaning and next check                                                                                                                                                     |
| -------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `200`                                                    | APNs accepted the request. This does not prove that the operating system delivered or presented the notification; the flow must still observe it.                          |
| `400 BadDeviceToken`                                     | The token is invalid or does not match the Sandbox environment. Confirm that it came from the current installation and that the request uses `api.sandbox.push.apple.com`. |
| `400 BadTopic` or `400 DeviceTokenNotForTopic`           | The `apns-topic` is invalid or does not match the token. Compare it with the installed app's `CFBundleIdentifier`.                                                         |
| `403 BadEnvironmentKeyIdInToken`                         | The APNs key does not support the Sandbox environment. Use a Sandbox-enabled key or an older key that supports both environments.                                          |
| `403 InvalidProviderToken` or `403 ExpiredProviderToken` | Check the JWT signature, Key ID, Team ID, issued-at time, and server clock. Generate a current provider token when it has expired.                                         |
| `410 Unregistered` or `410 ExpiredToken`                 | The token is no longer active for this topic. Stop sending to it until the app registers again and supplies its current token.                                             |

See [Apple's APNs response reference](https://developer.apple.com/documentation/usernotifications/handling-notification-responses-from-apns) for the complete status and reason list.

### Common problems

| Problem                                             | What to check                                                                                                                                                                                                                           |
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| The permission prompt does not appear               | The app may already have permission in the current session, or Android permission may have been granted during setup. Confirm the OS notification setting and run from a clean device state when testing first-run permission behavior. |
| The backend reports no target                       | Confirm the app logged in as the expected user and finished updating its push registration during this run before the trigger.                                                                                                          |
| APNs rejects the request                            | Use the direct APNs response table above to separate token, topic, credential-environment, and provider-token failures.                                                                                                                 |
| FCM returns `UNREGISTERED`                          | The app was reinstalled or its data was cleared. Launch the app and upload the new token before sending again.                                                                                                                          |
| Android receives a message but displays nothing     | Check notification permission, the channel ID and channel settings, and whether the payload is a notification message or a data message.                                                                                                |
| The iOS Simulator never registers                   | Check the Push Notifications capability, development entitlement, APNs credentials, and that the build targets iOS 16 or later.                                                                                                         |
| The hook succeeds but no notification appears       | Inspect the APNs or FCM response and confirm that the provider accepted the current token. The hook response and device delivery are separate stages.                                                                                   |
| A silent or background notification is inconsistent | The OS can delay or suppress background work. Reduce test frequency and use a private physical device for critical coverage.                                                                                                            |

## Provider references

* [Apple: Xcode 14 Simulator remote-notification support](https://developer.apple.com/documentation/xcode-release-notes/xcode-14-release-notes)
* [Apple: Registering an app with APNs](https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns)
* [Firebase: Set up FCM for Android](https://firebase.google.com/docs/cloud-messaging/android/get-started)
* [Firebase Apple SDK release notes](https://firebase.google.com/support/release-notes/ios)
