Skip to main content
Use this guide to add Sagepilot push notifications to an existing React Native app. By the end, a customer can sign in, opt into notifications, receive a campaign or journey notification, and have their notification open recorded in Sagepilot. The base SDK manages customer identity, sessions and device registration. The push add-on handles native display and captures, stores and uploads notification analytics. Install both, including when Firebase displays the notification.

Choose your integration

Choose one client for the same installation and push channel. The chat integration shares its existing session with push; you do not manage a second SDK session token. The order is: channel setup → packages and native setup → backend identity proof → Firebase adapter → client startup and identity → permission and consent → test a campaign or journey. After the one-time setup, the runtime flow has two stages: Registration runs after sign-in or session restoration. Without OS notification permission, the app continues without push. Campaigns and journeys skip message categories the customer has opted out of. The steps below cover each check and callback.

1. Prepare your channel and app

You need:
  • An Android/iOS React Native app. This guide uses matching Sagepilot 0.4.2 packages. The push add-on requires React Native 0.81.5+, Android API 24+ and iOS 15.1+. The example integration uses RN 0.81.5, Expo 54 and React Native Firebase 26.4.0; validate other compatible combinations in your app.
  • A physical device for end-to-end testing. Expo apps need a native development or release build; Expo Go cannot load this add-on.
  • Your Sagepilot workspace ID, push channel ID, and workspace host, such as https://app.sagepilot.ai or https://eu.sagepilot.ai.
  • A stable customer ID from your own system and a backend that can issue the proof in step 4 through your login/session response or a separate authenticated endpoint.
  • A Firebase project containing your Android/iOS app configuration. Use the same project for the app and Sagepilot’s FCM sending credentials.
In Sagepilot, create or open a Push Notifications channel:
  1. Enable Android Push and upload the Firebase service-account JSON for FCM delivery. This also supplies FCM sending credentials when an iOS app registers an FCM token.
  2. Open SDK & delivery, generate the identity signing secret, copy it to your backend’s secret configuration, and save. SDK identity verification is required.
  3. Choose Anonymous customers, Notification display and Notification images to suit your app. Start with Firebase / operating system display if you want Firebase to display background alerts.
  4. Copy the workspace and push channel IDs for the app configuration.
The app’s google-services.json and GoogleService-Info.plist are Firebase app configuration. The channel’s service-account JSON and identity signing secret are server credentials and must never be bundled into the app. See push channel settings for every UI field, secret rotation and anonymous-customer policy.

2. Install matching packages

From your app directory:
react-native-webview is a peer dependency of the base package, including for a push-only integration. AsyncStorage holds installation state; Keychain holds the SDK session securely. You can use Expo SecureStore instead of Keychain. If your app already uses the camera add-on, align it too:
You do not need the camera add-on for push. Keep the base and installed Sagepilot add-ons on the same version, commit the dependency lockfile, and rebuild native targets after upgrading. JavaScript reload alone does not install native changes.

3. Connect Firebase and native callbacks

Complete React Native Firebase setup for your installed Firebase version first. For iOS, configure the matching Apple app and APNs key in Firebase and enable Push Notifications in Xcode. FCM still uses APNs to reach an iPhone. Native forwarding lets the add-on observe messages and taps before JavaScript starts. It is required in addition to the JavaScript callbacks in step 5.
Add the Sagepilot plugin to your existing app config. This example uses Firebase 26.4’s CocoaPods option; keep other plugins and settings your app already needs.
app.json
Replace the package/bundle IDs and file paths with yours. Match the APNs entitlement to your signing environment. Merge UIBackgroundModes with any existing modes.
The plugin installs an Android RNFirebase receiver wrapper, forwards cold/warm Activity taps, and installs an iOS notification delegate. If your custom Activity or AppDelegate is unsupported, prebuild reports an error; use the manual forwarding instructions instead of ignoring it.

iOS image extension

If your notifications include images, add the Notification Service Extension after generating the iOS project:
Replace YourApp.xcodeproj with the actual project. Sign the app and generated SagepilotPushImages extension with your team. Rerun the helper after a clean prebuild or package upgrade. If you already have a notification service extension, integrate the shipped image helper into it rather than embedding a second extension. No App Group is needed for this image-only extension. For both integration paths, install pods and build a new binary after completing the code below:
Use npx expo run:android / npx expo run:ios --device for Expo, or your existing Android Studio/Xcode build flow for React Native CLI. Confirm both Firebase and Sagepilot native modules load before testing registration.

Server identity proof

Step 4 runs on your backend. It establishes which customer owns a device registration. The signing secret stays on the backend; the app receives only a short-lived signed proof. The dashboard’s Identity signing secret is the key used to create this proof. You do not need a second secret. Copy the value generated in Push Channel Settings → SDK & delivery into your backend’s secret configuration. SAGEPILOT_PUSH_IDENTITY_SECRET in the example below holds that same value. For example, your backend uses the dashboard secret to sign: “Our brand authorizes customer 123 for this push channel; this proof expires in five minutes.” The app receives that signed result. Sagepilot verifies it with the saved channel secret, establishes the customer’s SDK session, and the SDK registers the device’s FCM token under that customer. Why user details and an FCM token alone are insufficient: someone could submit another customer’s ID together with their own device token and try to receive that customer’s notifications. The proof establishes that your backend authorized the customer identity. Putting the dashboard secret in the app would let someone extract it and create proofs for other customers, so signing happens on the backend. The brand controls user verification. You choose how your app identifies signed-in or guest customers and which customer identity your backend supplies. Sagepilot does not independently verify your user’s login, email or phone. It checks the proof’s signature, expiry and channel scope, then resolves the supplied customer identity subject to normal validation and ownership checks. The app flow can stay simple: return the proof in your existing login or authenticated session response, then pass it to the SDK. No separate proof-fetch request is needed for that response. A dedicated endpoint such as POST /api/sagepilot/push-proof is an alternative, useful when renewing authorization without signing in again. You need the signed proof; you do not have to create that exact endpoint. Whichever response you use, your backend issues the proof as follows:
  1. Determine the customer using your own login or guest-session rules. This guide’s example uses your existing authenticated app session.
  2. Choose the canonical customer ID and contact details your brand authorizes. Keep proof issuance under your backend’s control so callers cannot request a proof for an unrelated customer.
  3. Call the function below and return the proof with Cache-Control: no-store. For example, include sagepilot_push: createPushIdentityProof(customer) in your existing login response, or return the helper’s result from a dedicated endpoint.
This Node.js/TypeScript helper needs no additional package:
server/createPushIdentityProof.ts
The two environment names above are example names for your backend. Set them to the saved channel secret and push channel ID. Use the secret exactly as copied; do not hex-decode it. The app must never call this signing function or contain the secret. Use client_customer_id for your external ID. customer_id is an alternative only when your backend already knows the correct Sagepilot customer UUID. Do not put an FCM token, email or installation ID in place of the canonical external customer ID. The proof lasts at most five minutes. Exchange it promptly; do not hardcode a JWT or reuse yesterday’s proof. The resulting SDK push authorization lasts 24 hours. Fetch a fresh proof at sign-in/session restoration, and renew before that authorization expires during a long-running session. To renew, call identify(freshProof) on the standalone client or authorizePush(freshProof) on the chat client. The SDK cannot generate a new backend proof itself.

Configure the add-on

Step 5 runs in the app. Create the following shared files. Both client options in step 6 reuse them.

Firebase transport and message mapping

src/sagepilot/pushTransport.ts
The adapter takes functions, not a token. In getToken: () => getToken(messaging), the left-hand getToken is the callback Sagepilot expects; the right-hand getToken(messaging) is Firebase’s function. Sagepilot calls it when registration needs the current token. createFirebasePushAdapter connects Firebase to the base SDK. createSagepilotNativePushAdapter adds native presentation, buffering and analytics upload. Creating them does not identify a customer or grant consent. This example assumes Firebase’s normal iOS remote-message registration is enabled. If your app disabled auto-registration, call Firebase’s registerDeviceForRemoteMessages(getMessaging()) before obtaining an FCM token. Keep Firebase’s notification delegation setting compatible with callbacks; see RNFirebase messaging configuration.

Background entry point

Register this handler once, at the top level of your app’s entry file, before registering the root component. In an Expo Router app, use a custom entry file, set package.json’s main to it, and load expo-router/entry after this setup.
index.js
If you already have a background handler, add this forwarding inside it and keep your existing handling. Do not start chat, ask for permission or navigate from a background handler. Native forwarding from step 3 is still needed for capture when JavaScript is unavailable.

Fetch a proof from your backend

fetchPushProof below is an example helper you implement in your app, not a method exported by the Sagepilot SDK. Its only job is to obtain the signed proof created in step 4. It does not fetch an FCM token or register the device. Choose one way to obtain the proof: Both return the same proof string, which you pass to connectPush in step 6. The client examples accept that string, so either approach works. loginResponse means the response from your own backend after adding the field above; it is not a Sagepilot API response.
src/sagepilot/pushProof.ts
Replace the URL and authentication header with your existing backend contract. appAccessToken means your app’s login credential, not an FCM token or Sagepilot session token. The backend derives the customer from that login, so this request deliberately does not send a customer ID to be trusted. The app cannot create this proof safely because that would require bundling the signing secret. Sagepilot does not need access to your customer database: it verifies the backend’s signature. The SDK then manages its own session credential, shared with chat when you use the chat integration. Do not fetch a new proof for every notification; renew authorization as described in step 4.

6. Start the client and identify the customer

Choose one tab. Replace all YOUR_... values with your actual IDs and keep one client at app scope, outside React screen components. connectPush and disconnectPush are app-owned example helpers. The Sagepilot methods they call are listed in the API reference.
src/sagepilot/pushClient.ts
start() restores the stored SDK session and installs listeners. identify(proof) validates the proof with Sagepilot and establishes the customer’s SDK session. It accepts a signed string, not { userId, name, email }. Your backend puts the external ID and optional email/phone into that proof. Other profile fields, such as name and custom properties, belong in your existing customer-profile integration.After obtaining the proof from your login response or fetchPushProof, call await connectPush(identityToken).
Call connectPush(...) after sign-in and after restoring a valid app login on a new launch. Await it and handle errors in your existing login/notification-settings UI; allow retry when connectivity or proof generation fails. Guard your normal initialization against iOS background/headless launches so background receipt does not start the login UI or request permission. identify/authorizePush starts automatic registration when permission already exists. The explicit await push.sync() makes registration errors observable to your caller. sync() returning is not proof of an active endpoint when permission is denied or identity is missing: in those cases it can do nothing or revoke a previous binding. Complete permission and consent next.

What registration and upsert mean

You do not send a user profile alongside every token. The authenticated SDK session supplies the customer; registration supplies the device information. With the adapter, Sagepilot calls Firebase’s getToken() and registers automatically. push.register(...) is the manual alternative for an app that already owns token management; it is not an extra step after automatic registration. Registration uses upsert behavior: create a device registration if one does not exist, otherwise update the existing installation’s token and permission. A normal token rotation updates that installation instead of creating another customer. Other devices belonging to the customer keep their own registrations. OS permission and notification consent are separate. The OS decides whether the app may show notifications. Sagepilot preferences decide whether the customer wants the relevant messages. Add the following handlers to your notification settings screen. They work with either pushClient.ts above:
src/sagepilot/pushPreferences.ts
Call these handlers from explicit user actions and catch failures before showing success. Do not enable marketing on every login or overwrite a previous opt-out. This example uses the top-level customer preferences. If your app already uses app/web overrides, update the corresponding override as well; inspect preference semantics before mixing the two. requestPermission() also synchronizes registration after permission is granted. You do not need another getToken() or register() call. iOS provisional permission can allow quiet notifications; it does not mean banners are enabled.

Pop-up and lock-screen setup

Matching 0.4.2 packages include the add-on’s guided setup. After access is granted and the signed-in app is in the foreground, the add-on can offer Open Settings for pop-ups and lock-screen notifications. It remembers Open Settings or Not now using push.storage and does not repeat the question on every login. There is no separate OS permission popup that grants Android heads-up banners or lock-screen visibility. The SDK opens settings; the user controls those choices. On iOS it can inspect alert/lock-screen settings and open notification settings. Android may report a setting as unknown, especially manufacturer-specific lock-screen layouts. Set notificationSettingsPrompt: false if your app already provides this guidance. For a custom Open notification settings button, use getSagepilotPushNotificationSettings() and openSagepilotPushNotificationSettings() from the add-on. See the method reference. New Android notification channels use high importance. Android retains an existing channel’s choices across app upgrades, so users with an older silent channel may need to enable sound/pop-up manually. Do Not Disturb, Focus, user privacy and manufacturer settings can still suppress banners or lock-screen content.

8. Handle sign-out and account switching

Call your chosen client’s disconnectPush() before clearing the app’s login credentials or switching accounts:
If revocation fails, retain the session and let the user retry. This prevents accidentally leaving the old customer’s device binding active. Once logout succeeds, sign in the next customer and run connectPush for that customer. destroy() only stops SDK listeners; it is not logout. push.unregister() revokes this installation, but a later automatic synchronization can register it again. Use saved preferences for a lasting opt-out and logout for an account transition.

Opens and analytics

Firebase/provider display supports Sagepilot push analytics. Keep both the native forwarding and JavaScript callbacks above. Firebase/OS can display the notification while the add-on captures and uploads the available observations. Merely registering an FCM token does not collect opens. Only Sagepilot-sent messages with their original signed tracking data contribute to these metrics. Preserve message.data; do not rebuild a message with just its title and body. Firebase console analytics are not imported. Tapping a notification opens the app. To route to a particular screen, add onOpen to the push configuration in step 6. Your app owns its deep-link routes and must wait until authentication and navigation are ready, including on a cold launch. For an app that already handles links under https://shop.yourbrand.com/, this helper requests an allowed destination:
src/sagepilot/pushNavigation.ts
Import it into your chosen pushClient.ts and extend the existing push config:
Replace the host with yours and configure your app’s universal/app links. If your router is not ready, retain the message in your existing navigation queue until it is. Linking.openURL() confirms a request to open a URL, not that a product screen loaded. Pass the original message through your routing flow to report the actual outcome:
message above is the original SagepilotPushMessage received by onOpen, and push is exported from your chosen client module. These are alternative outcome callbacks, not consecutive steps. Do not send both for a successful navigation. For separate actions or conversions, pass a stable ID as the fourth argument, such as your order ID. Reuse it on retries. track() reports whether local capture accepted the observation; it does not confirm server upload. push.flush() optionally retries pending uploads. This API is for notification observations, not general Product Viewed/Add to Cart app events. The add-on keeps up to 200 observations for seven days and retries while JavaScript runs or when the app next starts. Offline expiry, buffer overflow, force-stop and unavailable OS callbacks can leave analytics incomplete. Every SDK request includes version metadata; queued observations retain their capture-time version.

Rendering and images

Sagepilot’s server resolves template variables and sends the payload through FCM/APNs. The device displays it. SDK integration does not bypass Firebase delivery. The channel’s Notification display setting can follow the app preference or override it where the installation supports that mode. Older installations keep provider delivery. iOS SDK mode does not promise reliable silent-message rendering. Add an HTTPS Image URL to push content in the template, journey or Test Send, and enable the channel’s Notification images setting. Use a public static JPEG/PNG without URL credentials or redirects, preferably below FCM’s 1 MB notification image limit. The native image helper additionally enforces 5 MiB/16-megapixel limits and falls back to text if the image cannot load. iOS needs the image extension. Video and carousels are not supported in this version.

Anonymous customers and existing webhook users

To enable guest notifications, select Allow anonymous push in the channel and issue proofs from an authenticated, backend-controlled guest session. Use a persistent external customer ID even if the customer has no email or phone. Reuse the same external ID in the proof’s client_customer_id, chat userId when applicable, and your custom-webhook mapping. Sagepilot reuses the matching customer and can adopt a matching existing FCM registration. If no matching customer exists and anonymous push is allowed, it creates an anonymous customer with that external ID. An FCM token alone cannot prove customer ownership; a conflicting owner is rejected. When the guest becomes known, retain the canonical external ID and supply contact details accepted by your brand’s identification flow. If your identity genuinely changes, revoke the old registration before switching. Do not assume a new login ID will automatically merge two unrelated profiles. The channel’s compatibility choice preserves existing webhook behavior while requiring known contact details for SDK registration. Block anonymous push prevents future anonymous sends through both paths. Anonymous eligibility still requires appropriate consent and device permission.

9. Verify the complete flow

Use a real test customer with the same external ID in your app and Sagepilot:
  1. Install the new native build, sign in, and confirm connectPush completes without an authorization error.
  2. Choose Enable marketing notifications in your app and allow OS permission. Read preferences back to confirm consent was saved.
  3. Send a Sagepilot notification to that customer. Test foreground, background and a normally terminated app. For lock-screen/banner checks, inspect the device’s notification settings separately. An Android force-stopped app must be reopened.
  4. Tap the notification. Confirm the app opens and, if configured, reaches the intended destination once.
  5. Create a small segment containing only this test customer, then send through a push campaign or a journey’s Send Push Notification node. Inspect its push analytics for provider acceptance and the observed open. Test Send alone does not create campaign analytics or a journey entry.
  6. Compare Firebase/OS and SDK display on Android. Check a valid image and an unavailable image; text must remain usable.
  7. Test sign-out, a second account, denied permission, a marketing opt-out, and offline receipt followed by reconnect. The old customer must not remain bound after successful logout, and retrying analytics must not inflate opens.
Journey reports keep push opens separate from WhatsApp reads in mixed-channel journeys. See where to view push analytics. For exact inputs, return values and automatic versus manual calls, keep the push method reference beside this guide.