# A/B Testing Source: https://documentation.onesignal.com/docs/en/ab-testing Optimize your push and email messaging with OneSignal's A/B testing to improve engagement and campaign performance. A/B Testing helps you optimize messages by testing up to **10 variants** and evaluating performance based on metrics like open rates and click-through rates. This allows you to improve user engagement and make data-driven messaging decisions. Marketers focused on Lifecycle and Growth can use insights from A/B tests to make impactful improvements that align with broader business goals. Whether you’re testing push notifications or emails, A/B testing allows you to experiment with different designs, copy, calls-to-action (CTAs), and more. For example, test whether: * A push with an image outperforms text-only * A CTA like “Claim Offer” works better than “Get Started” * A short subject line gets more opens than a longer one *** ## Plan availability * **Pro & Enterprise Plans**: Up to **10 variants** * **Free & Growth Plans**: **2 variants** [Compare Plans](https://onesignal.com/pricing) *** ## How A/B Testing works A/B testing is only available when sending through the dashboard. It is not available with the API. To create A/B tests with Journeys, use [Journey Split Branches](./journeys-overview#split-branches). When creating [push](./push) and [email](./email-messaging) campaigns through the dashboard, click the **A/B Test** button and **Add Variant** to create additional tests. Your Audience (included and excluded segments) will be all the users eligible for the campaign. After you create the variants, you can select a portion of your audience to receive randomized variants i.e. 25% means 25% of the segment(s) selected will randomly receive one of the variants. A message with 2 variants (A & B) targeting 25% will have 12.5% of the audience get variant A and another 12.5% get variant B. A message with 10 variants (A-J) targeting 25% will have 2.5% of the users get each variant. By default, 25% of your audience receives the A/B test. For valid results, each variant must receive enough users. The more variants you use, the larger your test group must be. If you set 100%, the message will be sent evenly to all users in the audience and eliminates the ability to send the "winner" to remaining users. ## Select a Winner Messages that are sent as A/B tests will be marked as such under the **Messages Tab**. Click into the report for each test to see the full report or view the variant-specific reports. We provide some statistics for you to view the performance and choose a winner. The below screenshot describes how to view different stats and select a winning variant. We will then send the winner variant to all the remaining members of your target audience. *** ## Platform-specific instructions ### Create a Push A/B Test 1. Go to **Messages > Push > New Push** 2. Name your message (e.g., "Push AB Test - CTA Button") 3. Select your segment(s) 4. Click the **A/B Test** button 5. Add variants * Click **Add Variant** to duplicate and edit each new version. * Only change **one variable** at a time for meaningful insights. 6. A/B Test settings Select the percentage of your target audience that should receive each variant. See [How A/B Testing works](#how-a%2Fb-testing-works) for more details. The percentage is applied to the total audience, evenly distributed across each variant. Examples: * 25% of a message with 2 variants will sent 12.5% to each variant. * 25% of a message with 10 variants will sent 2.5% to each variant. * 50% of a message with 2 variants will sent 25% to each variant. * 50% of a message with 3 variants will sent 16.67% to each variant. * 100% of a message with 2 variants will sent 50% to each variant. * 100% of a message with 4 variants will sent 25% to each variant. Any percentage other than 100% will allow you to select a winner. 7. Review results View results under **Messages > Push > A/B Tests Tab**. Click any test to view variant-specific reports. 8. [Select a Winner](#select-a-winner) Use performance metrics to manually select a winner. ### Create an Email A/B Test 1. Go to **Messages > Email > New Email** 2. Name your message (e.g., "Email AB Test - Subject Line") 3. Select your segment(s) 4. Click the **A/B Test** button 5. Add variants * Click **Add Variant** to duplicate and edit each new version. * Only change **one variable** at a time for meaningful insights. 6. A/B Test settings Select the percentage of your target audience that should receive each variant. See [How A/B Testing works](#how-a%2Fb-testing-works) for more details. The percentage is applied to the total audience, evenly distributed across each variant. Examples: * 25% of a message with 2 variants will sent 12.5% to each variant. * 25% of a message with 10 variants will sent 2.5% to each variant. * 50% of a message with 2 variants will sent 25% to each variant. * 50% of a message with 3 variants will sent 16.67% to each variant. * 100% of a message with 2 variants will sent 50% to each variant. * 100% of a message with 4 variants will sent 25% to each variant. Any percentage other than 100% will allow you to select a winner. 7. Review results View results under **Messages > Email > A/B Tests Tab**. Click any test to view variant-specific reports. 8. [Select a Winner](#select-a-winner) Use performance metrics to manually select a winner. *** ## Best practices for A/B Testing ### Understand benchmarks Review past performance data so you can evaluate test success meaningfully. ### Set a goal and hypothesis Clearly define what you're testing and what success looks like. ### Change one variable at a time Control your experiment by isolating variables like: * Subject lines * Layouts * CTA copy * Length of copy * Images * Offers * Emojis * Colors * Fonts * Icons * GIFs ### Use controls Include your "usual" version as a baseline for measuring improvements. Create control groups by tagging users randomly and exclude that segment. You can [Export user data](./exporting-data) and create a segment from the CSV [import](./import). ### Test simultaneously Send all variants at the same time to avoid timing bias. ### Continue testing Iterate based on results to continuously optimize message performance. *** ## FAQ ### Can I A/B test different segments? Not within the standard message form. However, you can test different segments using [Journeys](./journeys-overview) with **Split Branches** and **Yes/No Branches**. ### Can I automatically select a winner? Not yet. You must manually choose the winning variant or use 100% to send all variants. *** You're done! You can now test different variants of your messages and select a winner. Need help? Chat with our Support team or email `support@onesignal.com` Please include: * Details of the issue you're experiencing and steps to reproduce if available * Your OneSignal App ID * The External ID or Subscription ID if applicable * The URL to the message you tested in the OneSignal Dashboard if applicable * Any relevant [logs or error messages](/docs/en/capturing-a-debug-log) We're happy to help! *** # Action buttons for push notifications Source: https://documentation.onesignal.com/docs/en/action-buttons Add interactive action buttons to your push notifications on iOS, Android, and the web. Learn setup via Dashboard & API, supported platforms, click handling, icons, and event tracking. This guide applies only to push notifications. For in-app messages, see [In-App Messages: How to add Click Actions](./iam-click-actions). Action Buttons let you add multiple, labeled actions to a single push notification, so users can respond without opening your app or site first. Depending on the operating system and device, users reveal buttons by expanding the notification (long press, swipe + View, or an expand affordance). *** ## Add action buttons You can configure Action Buttons in [Templates](./templates), directly when composing a message in the dashboard, or via the [API](/reference/push-notification). ### Dashboard & template setup When creating a push, open **Advanced Options > Action Buttons**. ### API setup * **Mobile Apps**: Use the `buttons` parameter. Pass an array of up to 3 objects with `id`, `text`, and `icon`. * **Web (Chrome)**: Use `web_buttons`, passing up to 2 objects with `id`, `text`, `icon`, and `url`. *** ## Action button properties * **Action ID**: A unique identifier for the specific button action. The ID of the clicked button is passed to you so you can identify which button was clicked. (e.g. 'accept-button') * Must be unique per button. * Available in the OSNotification Payload and can be accessed within the SDK Notification Opened Event Handler. * API: `id` property * **Label**: The text the button should display to your users. (e.g. 'Accept') * API: `text` property * **Icon**: Optional icon displayed with the button label. Not available on all platforms and operating systems. See FAQ below for details. * Mobile apps must include the button within its image resources. See Action Button Icons below for details. * Websites can use a valid publicly reachable URL to an icon. Keep this small because it's downloaded on every notification display. (e.g. `http://site.com/icon.png`) * API: `icon` property * **Launch URL 1**: The URL to open when the action button of the first button is clicked. Pass `'do_not_open'` to prevent opening any URL. (e.g. 'do\_not\_open') * Web only * Max 2 web buttons * API: `web_buttons.url` property ### Action button icons * iOS supports action button icons for iOS 15+ * Android stopped supporting action button icons for [Android N (AKA 7)](https://android-developers.googleblog.com/2016/06/notifications-in-android-n.html) *** ## Handle action button clicks When a user taps a button, OneSignal delivers the Action ID to your app/site. You can either use the default behavior (open your app/site) or override it. ### Default behavior (Open App/Site, Then Handle) 1. The app/site opens (or is focused on web). 2. Your click/open listener receives the event with the Action ID. (See [mobile SDK reference](./mobile-sdk-reference#addclicklistener-push) or [web SDK reference](./web-sdk-reference#addeventlistener-notifications) for click listener details.) 3. Use the Action ID to track which button was clicked and handle the click event accordingly. This could mean deep linking to a specific screen in your app or triggering a custom event. #### Prevent app launch from action button clicks * **Android:** Follow [Android SDK setup > Disable default open behavior](./android-sdk-setup#disable-default-open-behavior). This lets you intercept clicks in a [Service Extensions](./service-extensions) and run custom logic (e.g. call an API) without opening the app. * **iOS:** Include an `ios_category` on the notification to associate actions via the [UNNotificationCategory Object](https://developer.apple.com/documentation/usernotifications/unnotificationcategory). See Apple's [Declaring Your Actionable Notification Types](https://developer.apple.com/documentation/usernotifications/declaring_your_actionable_notification_types) for more details. * **Web (Chrome):** Use the `_osp=do_not_open` magic string to prevent opening any URL. This is supported on Chrome and Firefox, but not supported for the Safari web browser. *** ## Supported platforms & limits | Platform | Buttons Supported | Notes | | ------------------------- | ----------------- | -------------------------------------------------------------- | | iOS | Up to 4 | Icons on iOS 15+. Requires categories for background handling. | | Android / Amazon / Huawei | Up to 3 | No button icons from Android 7+. | | Web – Chrome | Up to 2 | Buttons and icons supported. `_osp=do_not_open` supported. | | Web – Firefox | No buttons | `_osp=do_not_open` works for the launch URL only. | | Web – Safari | No buttons | `_osp=do_not_open` not supported. Provide a real URL. | Users often need to **expand** the notification to see buttons (e.g., long press on iOS, swipe + **View** on some Android OEMs). *** ## Troubleshooting ### Buttons don’t show up * Expand the notification (long press, swipe + View, or expand). * Verify you added Action ID and Label for each button. * Check platform limits (e.g., only 2 buttons on Chrome). ### Clicking a button doesn’t open the browser on mobile web If the browser is in the background or fully closed, most mobile browsers (including Chrome) will not come to the foreground or open the URL, even though click events still trigger in the service worker. This is intentional browser behavior to prevent background apps from interrupting the user. * Most mobile browsers won’t foreground themselves from a background service worker. Clicks still fire in the worker, but the tab doesn’t open. This is intentional. * Ensure the launch URL and the button URL are exactly identical (including trailing slashes) if you expect the tab to be focused instead of opening a new one. ### Icons don’t appear * iOS must be 15+ for button icons. * Android 7+ does not render action button icons. * On web, confirm the icon URL is publicly accessible and small (fast to download). ### Why is there a close action button? By default web push notifications on Windows 10 include the Close button. However, if you add your own action button, then this close button is removed. So, in either case the notification will remain on-screen till the user interacts with it. This is designed by Google to give the users the chance to interact with the notification. Need help? Chat with our Support team or email `support@onesignal.com` Please include: * Details of the issue you're experiencing and steps to reproduce if available * Your OneSignal App ID * The External ID or Subscription ID if applicable * The URL to the message you tested in the OneSignal Dashboard if applicable * Any relevant [logs or error messages](/docs/en/capturing-a-debug-log) We're happy to help! *** # Android notification categories Source: https://documentation.onesignal.com/docs/en/android-notification-categories Set up Android notification categories (channels) in OneSignal to improve user control and customization of push notifications. Android notification categories (aka notification channels) were introduced in Android 8.0 (Oreo) to give users greater control over how they receive notifications from your app. This allows you to categorize your notifications and define different experiences such as display behavior, sound, vibration, badges, and lock screen visibility. For example, if you have breaking news notifications, you can create a category for them and set "Urgent" importance and a custom sound to ensure they are displayed prominently versus less important notifications that may be muted or have a default sound. OneSignal makes it easy to create and manage these categories directly within the dashboard. Alternatively, you can define categories programmatically in your app. See [Android’s guide to creating notification channels](https://developer.android.com/develop/ui/views/notifications/channels). OneSignal's Android notification categories work for Google Android, Huawei Android, and Amazon FireOS. Android notification categories shown in device settings *** ## Default notification categories OneSignal automatically creates two default categories: ### Miscellaneous Used when you do not set a category. * **Importance:** High * **Sound:** Default * **Vibration:** Default * **Badges:** Enabled * **Lockscreen:** Private ### Restored Used when your app is force-quit and reopened. If your app has push notifications in the Notification Center when the app is force-quit, they are removed from the device. Reopening the app recreates (restores) those notifications. The OneSignal SDK automatically sets the category to "Restored" with the following settings to prevent unwanted sounds and pop-ups from multiple restored notifications. * **Importance:** Low * **Sound:** Off * **Vibration:** Off * **Badges:** Disabled * **Lockscreen:** Private If you always send push notifications with a custom category, the "Miscellaneous" channel won’t appear on user devices. The "Restored" channel will always appear to handle restored notifications after force-quitting the app. ### Huawei-specific behavior On Huawei devices, OneSignal does **not** set a default category. If you don't include one, Huawei will apply **High** importance by default. For badge control on Huawei devices, you can also use dedicated Huawei badge parameters (`huawei_badge_class`, `huawei_badge_set_num`, `huawei_badge_add_num`) in the [Create message](/reference/create-message) API. See [Badges](./badges#huawei-badges) for details. *** ## Create Android notification categories in OneSignal Before you begin, make sure you have a [OneSignal app configured with an Android platform](./android-sdk-setup). 1. Go to **Settings > Push & In-App > Android Notification Channels** in the OneSignal Dashboard. 2. Click **Add Group** to organize your categories (e.g., “News Updates”, “Social Activity”). 3. Click **Add Channel** within the group to create a new category. OneSignal dashboard showing where to add Android notification categories You’ll be asked to define the following: ### Name *User-visible.* Keep it clear and descriptive. ### Description *User-visible.* Briefly explain the type of notifications this category will handle. ### Importance Controls how visible and interruptive the notification is: * **Low:** Silent, no alerts * **Medium:** No sound/vibration, minimal visual interruption * **High:** Plays sound or vibrates, no screen pop-up * **Urgent:** Plays sound and appears as a heads-up or banner-style notification. ### Sound * **Off:** No sound * **Default:** Device’s default notification tone * **Custom:** Upload and reference a custom sound (no file extension). Example: `alert_beep` (not `alert_beep.wav`) Full setup instructions for adding custom sounds to your notifications. ### Vibration * **Off:** No vibration * **Default:** Uses device’s vibration pattern * **Custom:** Define your own using a pattern (in ms). Example: `0, 300, 500, 300` → Wait 0ms, vibrate 300ms, pause 500ms, vibrate 300ms. ### LED Color Some Android devices support LED indicators: * **Off:** No LED * **Default:** Device default * **Custom:** ARGB hex value (e.g., `FF0000FF` for blue) ### Badges Shows badge count on app icon: * **Enabled:** Badge is shown * **Disabled:** No badge displayed ### Lockscreen visibility * **Public:** Full content shown * **Private:** Only app name, hides content * **Secret:** No notification visible on lock screen Once your category is created, you can use it in your notifications. *** ## Updating categories After a device receives a notification with a category, Android locks that category’s behavior. **Changes to importance, sound, vibration, or other settings will not apply retroactively**. For example, if you send a push notification with a category using "High" importance and sound, then change the importance to "Urgent" and use a different sound file, the next push notification to that same device with that same category will not have "Urgent" importance or the new sound. **Options**: * **To update behavior:** Create a new category. * **To test changes:** Clear app data or uninstall and reinstall the app. You can update: * Category name (shown as "channel name" in Android settings) * Category group name (shown as "channel group name" in Android settings) These update in Android's notification settings when the next notification is received using that category. *** ## Deleting categories To remove a deleted category from the user’s device: 1. Delete the category from the OneSignal dashboard. 2. Ensure all notifications are cleared from the Notification Center. 3. Have the user: * Put the app in the background for 60+ seconds * Open it again (triggers SDK sync) The SDK will re-sync and remove the deleted category from Android settings. *** ## Adding categories to notifications Depending on how you created the Android Category and how you are sending the message, these are the ways you can reference the category in your push notifications. ### Sending from the OneSignal Dashboard 1. Within your Template or Push Message Composer, navigate to the Android settings. 2. Under **Category**, select your category if created within the OneSignal dashboard or select **(Created in App)** if created programmatically in your app. * If created programmatically, also set the **Existing Channel** field to the name defined in your code. Android category selection dropdown in the push notification composer ### Sending with the REST API If you created the category in the OneSignal dashboard, use the `android_channel_id` in the [Create message](/reference/push-notification) API request. You can find the Channel ID in the Android Category setup screen. Channel ID field in the Android category setup screen If using your own programmatically created Android channels, use the `existing_android_channel_id` parameter in the [Create message](/reference/push-notification) API request and set the name as defined in your code. *** ## FAQ ### Can categories play sounds in Do Not Disturb (DND) mode? No. OneSignal does not set `setBypassDnd` on categories. To override DND, create your own channel programmatically and enable this setting. See [setBypassDnd](https://developer.android.com/reference/android/app/NotificationChannel#setBypassDnd\(boolean\)). ### Can I localize category names or descriptions? No. OneSignal does not support multiple languages for categories. To support localization, define your own Android channels and reference them via `existing_android_channel_id` in your push API requests. ### Why is my Android category not working? There are several reasons why your Android category may not be working as expected. To troubleshoot, check the following: * **What is not working?** * Is the sound file not playing? * Is it not displaying on the device? * Do you not see the category in the Android Notification Settings? * **How was the category created?** * If created in the OneSignal dashboard, make sure the settings are defined as you expect. * If created programmatically in your app, review your code. See [Android’s guide to creating notification channels](https://developer.android.com/develop/ui/views/notifications/channels). * **Review the category settings:** * Make sure the settings are defined as you expect. * Is the sound file being referenced correctly? See the [Sound](#sound) section above. * Is the category name or ID being referenced correctly when sending the message? * **Did you update the settings after sending a notification?** * If you updated the settings after sending a notification, Android will not apply those updates to your device. See [Updating categories](#updating-categories) above. * **Check OneSignal SDK initialization:** * Make sure OneSignal is initialized in the `Application` class, not an `Activity`. See [Android SDK Setup](./android-sdk-setup). Still need help? We're here to assist! Email `support@onesignal.com` with the above information including: * The Android category code if created programmatically in your app * The URL to the message in your OneSignal dashboard with the issue We'll assist you as soon as possible! *** ## Related pages Set up custom notification sounds for Android, iOS, and other platforms. Configure badge counts on app icons across platforms. Install and initialize the OneSignal Android SDK in your app. Send push notifications programmatically using the REST API. # Apps, Organizations, & Accounts Source: https://documentation.onesignal.com/docs/en/apps-organizations Add, rename, move, and delete OneSignal apps and organizations. Understand the relationship between accounts, apps, and organizations. When you sign up at [OneSignal.com](https://onesignal.com), a user account is created and tied to your email address. With your account, you can create, manage, or be invited to multiple **Apps** and **Organizations**. If your company already has a OneSignal App or Organization, email the account admin to request an invite. Share the [Manage Team Members](./manage-team-members) guide to help them give you access. *** ## OneSignal account structure Your email address is used to log in to your OneSignal account. From there, you can access the **Apps** and **Organizations** in which you are a [Team Member](./manage-team-members). ### Apps vs. Organizations * A **OneSignal App** holds user and messaging data for a single project, across all platforms (web, iOS, Android, email, etc.). Each app exists within a single Organization. * A **OneSignal Organization** is a container for managing multiple apps, billing, and team permissions. You can have: * Unlimited apps (free to create) * Multiple organizations (free or paid) * Apps for separate environments (e.g., dev, staging, production) * Different access levels per app or organization *** ## Access levels and roles OneSignal supports two levels of access: ### App-level access * Access to only the specific app(s) a user is invited to. * Cannot view billing or upgrade plan settings. ### Organization-level access * Access to all apps within the organization. * Only **Admin** roles can manage billing and upgrades. ### User roles Access can be further scoped by roles: #### Organization Roles | Role | Best for | Access summary | | ----------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Admin | Developers, Owners | Full control over all org settings, billing, and messaging. Automatically includes all App Admin privileges across every app in the org | | Finance | Finance teams | View org settings, apps, members, and billing. Edit billing. No app-level permissions | | Operations | Ops teams | View access across all apps plus manage suppressions and sender identities | | Editor | Marketers, PMs | Full messaging workflow: create segments, build and send messages, manage webhooks and imports. Cannot modify underlying user or subscription records, or change app settings | | Composer | Content writers, Designers | Create and edit messages, templates, segments, and journeys. Cannot send, activate, or delete most content. No export access | | Viewer | Analysts, Read-only users | View-only access across all apps. Cannot edit, send, or export | | Team Member | Minimal access users | Can view the org and its apps list. No app-level permissions on its own. Access is layered on through app-level role assignments | #### App Roles | Role | Best for | Access summary | | ---------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------- | | Admin | App owners, Lead developers | Full control over the app including settings, keys, integrations, and team management | | Operations | Ops teams | View access across app features plus manage suppressions and sender identities | | Editor | Marketers, PMs | Create, edit, send, and delete messages and related content. Manage webhooks and imports. Cannot change app settings | | Composer | Content writers | Create and edit messages, templates, and segments. Cannot send or activate. No export access | | Viewer | Read-only users | View-only access to app data. Cannot edit, send, or export | Invite users, assign roles, and manage access at the app or organization level. *** ## Managing your user account Enable [two-factor authentication](./2-step-authentication) to add an extra layer of security to your account. See [Data collection & security FAQ](./data-questions) for details. ### Reset password or email 1. Navigate to [Account Management](https://dashboard.onesignal.com/profile) or click **your email drop-down > Manage Account**. 2. Add your Email, New Password, and Confirm Password. 3. Click **Submit**. OneSignal account management page showing email and password fields ### Delete your account Deleting your user account only removes your email from OneSignal. It does not delete apps or downgrade paid plans. Be sure to [Downgrade your plan](./billing-faq) if needed. To delete: 1. Navigate to [Account Management](https://dashboard.onesignal.com/profile) or click **your email drop-down > Manage Account**. 2. Scroll down to **Delete Account**. 3. If you don't have the option to delete your account, contact `ar@onesignal.com`. *** ## Manage apps * **Create an app**: * Log in at [onesignal.com](https://onesignal.com) and click **New App/Website**. * Or use the [Create an App API](/reference/create-an-app). * **Rename an app**: * From the dashboard **All Apps** page, click **Options > Rename** next to the app. * Or use the [Update an App API](/reference/update-an-app) * **Find App ID**: * See [Keys & IDs](./keys-and-ids). * **Delete an app**: * You can delete apps within Free organizations and under 5,000 total subscriptions from the **All Apps** page via **Options > Delete**. * For larger apps, contact `support@onesignal.com`. *** ## Manage organizations Enable [two-factor authentication](./2-step-authentication) at the organization level to require all team members to use 2FA. * **Create an organization**: * Visit the **Organizations** page or click **New Organization** in the dashboard (no paid plan required). * **Rename an organization**: * In **Organizations**, click **Options > Rename** next to the organization. * **Find Org ID**: * Go to **Organizations**, select your organization, and copy the UUID from the address bar. Example: `https://dashboard.onesignal.com/organizations/THE_ORG_ID/apps` * **Delete an organization**: * Go to **Organizations**, click **Options > Remove**. * You must move the apps out of the organization before deleting it. * If you need assistance, contact `support@onesignal.com` with the Org ID you want to delete. ### Add or move apps between organizations * **Add app to organization**: * Go to **Organizations > select your Organization > Move Apps Into Organization**. * Select your apps and click **Move Apps**. * You need: * Admin access to the source Organization (where the app currently lives) * Admin access to the destination Organization (where you want to move it) * Admin access to the App itself * The app must currently belong to a Free Organization (apps in paid organizations cannot be moved self-service). When moving an app between two organizations, you must be an Admin on both. Being an Admin on the destination Organization and the app is not enough. You also need Admin access to the source Organization. If you are unsure who the source Organization admin is, contact `support@onesignal.com` with your App ID and we can help identify them. If you need assistance, email `support@onesignal.com` with: * The App ID(s) you want to move * The Org ID to move them to. * You must contact Support from an email with Admin access to the Apps and Organization. *** ## FAQ ### Where can I see when my app was created? Use the [View App](/reference/view-an-app) or [View Apps](/reference/view-apps) API to get the `created_at` timestamp. ### Why do I see limitations on a paid plan? Your app might not be assigned to the correct paid Organization. Follow [Add or move apps between organizations](#add-or-move-apps-between-organizations) to move your app to the paid org. If you need help, see the support contact details in that section. ### What are the best practices for agencies? Agencies can manage client apps using one of two approaches: 1. **Centralized billing** Use a single Organization to manage and pay for all client apps. 2. **Client-managed billing** Each client sets up their own Organization and handles their own billing. You can mix paid and free apps by assigning them to the appropriate Organization. Need help? [Contact our Sales Team](https://onesignal.com/contact). ### How can we access analytics, messages, and users across multiple Apps? OneSignal does not have a single cross-app dashboard view. Each app's data is accessed separately. Here are the recommended approaches for working across apps: * **Analytics** — Use [Event Streams](./event-streams) to route message delivery and engagement data from each app to a centralized analytics platform like Snowflake, BigQuery, or Amplitude. * **Messaging** — Use the [Create message API](/reference/create-message) to send messages to multiple apps in parallel from your backend. You can also create [Templates](./templates) once and copy them across apps using the [Copy template API](/reference/copy-template-to-another-app). * **Users** — Use the [REST API](/reference/view-user) to query user data per app. If you need a unified view across apps, export Subscription records per app via [CSV export](/reference/csv-export) or stream events via Event Streams to an external data warehouse. ### How do we change our dashboard timezone? Dashboard graphs display in UTC and cannot be changed. All other dates and times use your browser's timezone. To change the displayed timezone, update the timezone setting in your browser or operating system. *** ## Related pages Invite users, assign roles, and manage access at the app or organization level. Find your App ID, REST API key, and other credentials. Enable 2FA for your account or require it for your organization. Manage plans, billing, and subscriptions across organizations. # Audit Logs Source: https://documentation.onesignal.com/docs/en/audit-logs Track and review user activity across your OneSignal organization for security, compliance, and accountability. Export to JSONL or query via the REST API. ## Overview Audit Logs provide a **read-only** record of user actions across your OneSignal organization. Use audit logs to: * **Investigate security incidents** by reviewing login activity, IP addresses, and user sessions. * **Meet compliance requirements** by providing evidence for SOC 2, HIPAA, and internal audits. * **Maintain accountability** by tracking who created, updated, or deleted templates, journeys, and notifications. * **Export data** to JSONL for offline analysis, or **query the REST API** for programmatic access and SIEM integration. Audit logs are **immutable**. You cannot edit or delete entries, ensuring an accurate and trustworthy audit trail. Audit Logs table showing user actions across an organization ### Plan availability Audit log retention varies by plan. | Plan | Retention Period | Notes | | ------------ | ---------------- | ------------------------------------------------------- | | Free | 48 hours | Included | | Growth | 48 hours | Included | | Professional | 48 hours | Upgrade to 90 days with Legal & Security Package add-on | | Enterprise | 90 days | Included | To increase retention or learn more about the Legal & Security Package, contact your Account Manager or our [Sales Team](https://onesignal.com/contact). *** ## Accessing Audit Logs ### Org Level Audit Logs Audit logs are available at the **Organization level** and include activity across **all apps** in that organization. **Requirements:** * You must be an **Organization Admin**. See [Manage Team Members](./manage-team-members) to update roles. To access audit logs: 1. Navigate to **Organizations** 2. Select your organization 3. Click **Audit Logs** ### App Level Audit Logs In addition to organization-level audit logs, you can view audit logs for a specific app: 1. Navigate to your **App** 2. Go to **Settings** 3. Click **Audit Logs** App-level audit logs show only activity for that specific app, making it easier to investigate app-specific changes. **Requirements:** * You must be an **App Admin** or **Organization Admin** *** ## Understanding the Audit Log Each row represents a single user action. You can customize visible columns using the Columns button in the top-right corner. Audit logs columns button showing the available columns | Column | Description | | --------------- | ---------------------------------------------------------------- | | **Date & Time** | When the action occurred | | **User Action** | The type of action performed (e.g., Logged In, Template Updated) | | **Email** | Email address of the user who performed the action | | **Org Role** | The user's organization-level role (Admin, Editor, Viewer) | | **App Role** | The user's app-level role, if applicable | | **Item Type** | The type of object affected (Template, Notification, Journey) | | **Item Name** | The name of the affected object | | **App Name** | The app where the action occurred | | **User** | Display name of the user | | **IP Address** | The IP address from which the action was performed | *** ## Tracked events Audit logs track user actions across your organization, including logins, template changes, notification activity, journey updates, member management, billing events, and more. For a comprehensive list of all tracked events, see the [Complete event reference](#complete-event-reference). *** ## Viewing event details Click the expand arrow (›) next to any event to view additional details: Expanded audit log event showing detailed information including App ID, Browser, Event ID, and IP Address Expanded details include: | Field | Description | | --------------- | --------------------------------------------------------------- | | **API Version** | API version used (when applicable) | | **App ID** | Unique identifier for the app | | **App Name** | Name of the app where the action occurred | | **App Role** | User's role within the specific app | | **Browser** | User agent string (browser and OS information) | | **Channel** | Notification channel (push, email, sms) for notification events | | **Email** | User's email address | | **Event ID** | Unique identifier for this specific event | | **IP Address** | Full IP address of the user | | **Item Name** | Name of the affected object | | **Item Type** | Type of object (template, notification, etc.) | | **Location** | Geographic location based on IP address | | **Name** | Display name of the user | | **Org ID** | Unique identifier for the organization | | **Org Role** | User's organization-level role | | **Target ID** | Unique identifier for the affected object | | **Target Role** | Role associated with the target (for member events) | | **Timestamp** | Formatted date and time of the event | | **User Action** | The action type (e.g., notification.sent) | | **User Agent** | Full browser/client user agent string | | **Version** | Event schema version | *** ## Search and filters Use the search and filter options to find specific events: ### Search by email Enter an email address in the search bar to find all actions performed by a specific user. ### Date range Select from the following preset date ranges: * **Last 24 hours** * **Last 48 hours** (default) * **Last 7 days** * **Last 30 days** * **Last 90 days** (requires extended retention) * **Custom** - Select specific start and end dates Date range options beyond your plan's retention period will display a lock icon. Contact sales for extended audit log history. ### Filters Click **Filters** to narrow results by: | Filter | Description | | --------------- | --------------------------------------------------------- | | **User Action** | Filter by action type (Logged In, Template Updated, etc.) | | **App ID** | Filter by a specific app's unique identifier | | **IP Address** | Filter by IP address | | **Item Type** | Filter by object type (Template, Notification, Journey) | | **Target ID** | Filter by the unique ID of the affected object | ### Item type values Filter audit logs by the type of object affected: | Item Type | Description | | ----------------- | --------------------------------- | | A/B Test | A/B test experiments | | API Key | API keys and tokens | | App | Application settings | | Data Feed | Data feed configurations | | Dynamic Content | Dynamic content blocks | | Email Domain | Email sending domains | | Email Suppression | Email suppression list entries | | Event Stream | Event stream destinations | | Export | Data exports | | In-App Message | In-app message campaigns | | Integration | Third-party integrations | | Journey | Automated journey workflows | | Member | Team members | | Notification | Push, email, or SMS notifications | | Organization | Organization settings | | Segment | Audience segments | | Subscription | User subscriptions | | Template | Message templates | | User | End users/subscribers | | Webhook | Webhook configurations | *** ## Exporting audit logs Export audit log data to a zipped `.json` file for offline analysis, compliance reporting, or integration with external tools. To export audit logs: 1. Navigate to the **Audit Logs** page (at the organization or app level). 2. Apply any desired [filters](#search-and-filters) and date range. 3. Click **Export**. 4. The `.json.gz` file(s) will be emailed to you. You'll have 3 days from creation to download your file before it expires. ### Export availability The export window matches your plan's retention period. | Plan | Export Window | | ------------ | ---------------------------------------------------- | | Free | 2 days | | Growth | 2 days | | Professional | 2 days (up to 90 days with Legal & Security Package) | | Enterprise | Up to 90 days | Combine exports with the [Audit Logs API](/reference/list-audit-logs) for automated, recurring data pulls. *** ## API access Query audit logs programmatically using the REST API. The API supports pagination, time-scoped queries, and filtering by app, action type, actor, target, and IP address. **Requirements:** * **Enterprise plan** with the audit logs entitlement enabled * **Organization API Key** for authentication (app-level keys are not accepted) See the [List audit logs API reference](/reference/list-audit-logs) for endpoint details, parameters, and example responses. *** ## Data retention Audit log data is retained based on your plan: * **Free and Growth plans**: 48-hour retention * **Professional plan**: 48-hour retention by default, or 90-day retention with the Legal & Security Package add-on * **Enterprise plan**: 90-day retention included After the retention period, audit log entries are automatically removed and cannot be recovered. Need longer retention? Contact your Account Manager or our [Sales Team](https://onesignal.com/contact) to discuss upgrading your plan. *** ## Complete event reference The following is a comprehensive list of all events tracked in audit logs, organized by category. | Action | Description | | ---------------------- | ------------------------------------ | | App Created | A new app was created | | App Renamed | An app was renamed | | App Deleted | An app was deleted | | App Settings Updated | App settings were modified | | App API Key Reset | App API key was reset | | App Auth Token Created | App authentication token was created | | App Auth Token Updated | App authentication token was updated | | App Auth Token Deleted | App authentication token was deleted | | App Auth Token Rotated | App authentication token was rotated | | Action | Description | | ---------------- | ---------------------------------- | | Template Created | A new message template was created | | Template Updated | An existing template was modified | | Template Deleted | A template was removed | | Action | Description | | ---------------------- | ------------------------------------- | | Notification Created | A new notification was created | | Notification Sent | A notification was sent | | Notification Updated | A notification was modified | | Notification Canceled | A scheduled notification was canceled | | Notification Deleted | A notification was removed | | Notification Previewed | A notification was previewed | | Action | Description | | ------------------------ | ------------------------------------- | | A/B Test Created | A new A/B test was created | | A/B Test Updated | An A/B test was modified | | A/B Test Deleted | An A/B test was deleted | | A/B Test Canceled | An A/B test was canceled | | A/B Test Winner Selected | A winner was selected for an A/B test | | Action | Description | | ----------------- | -------------------------------- | | Journey Created | A new journey was created | | Journey Updated | An existing journey was modified | | Journey Set Live | A journey was activated | | Journey Archived | A journey was archived | | Journey Resumed | A paused journey was resumed | | Journey Scheduled | A journey was scheduled | | Journey Deleted | A journey was deleted | | Action | Description | | ----------------------- | -------------------------------- | | In-App Message Created | A new in-app message was created | | In-App Message Updated | An in-app message was modified | | In-App Message Deleted | An in-app message was deleted | | In-App Message Enabled | An in-app message was enabled | | In-App Message Disabled | An in-app message was disabled | | Action | Description | | ---------------------- | ---------------------------- | | Segment Created | A new segment was created | | Segment Updated | A segment was modified | | Segment Deleted | A segment was deleted | | Segment Paused | A segment was paused | | Segment Resumed | A segment was resumed | | Segment Set as Default | A segment was set as default | | Action | Description | | -------------------------- | ---------------------------------------------- | | Member Created | A new team member was created | | Member Deleted | A team member was deleted | | Member Role Changed in App | A member's app-level role was changed | | Member Role Changed in Org | A member's organization-level role was changed | | Member Added to App | A member was added to an app | | Member Removed from App | A member was removed from an app | | Member Added to Org | A member was added to the organization | | Member Removed from Org | A member was removed from the organization | | Action | Description | | --------------------- | ------------------------------------------- | | Organization Created | A new organization was created | | Organization Updated | Organization settings were modified | | Organization Deleted | An organization was deleted | | Organization Disabled | The organization was disabled | | Organization Enabled | The organization was enabled | | Logged In | A user logged in | | Logged Out | A user logged out | | User Added to Org | A user was added to the organization | | User Removed from Org | A user was removed from the organization | | User Role Changed | A user's role was changed | | App Added to Org | An app was added to the organization | | App Removed from Org | An app was removed from the organization | | 2FA Required | Two-factor authentication was made required | | 2FA Made Optional | Two-factor authentication was made optional | | Auth Token Created | Organization auth token was created | | Auth Token Updated | Organization auth token was updated | | Auth Token Deleted | Organization auth token was deleted | | Auth Token Rotated | Organization auth token was rotated | | Action | Description | | ---------------------- | ------------------------------ | | Subscription Created | A new subscription was created | | Subscription Updated | A subscription was modified | | Subscription Canceled | A subscription was canceled | | Payment Method Added | A payment method was added | | Payment Method Updated | A payment method was updated | | Action | Description | | ------------------- | ------------------------- | | Webhook Created | A new webhook was created | | Webhook Updated | A webhook was modified | | Webhook Deleted | A webhook was deleted | | Webhook Tested | A webhook was tested | | Webhook Activated | A webhook was activated | | Webhook Deactivated | A webhook was deactivated | | Action | Description | | ------------------------ | ------------------------------- | | Event Stream Created | A new event stream was created | | Event Stream Updated | An event stream was modified | | Event Stream Deleted | An event stream was deleted | | Event Stream Activated | An event stream was activated | | Event Stream Deactivated | An event stream was deactivated | | Event Stream Tested | An event stream was tested | | Action | Description | | --------------------- | --------------------------- | | Data Feed Created | A new data feed was created | | Data Feed Updated | A data feed was modified | | Data Feed Deleted | A data feed was deleted | | Data Feed Activated | A data feed was activated | | Data Feed Deactivated | A data feed was deactivated | | Data Feed Tested | A data feed was tested | | Action | Description | | ---------------------------- | ---------------------------------- | | Integration Activated | An integration was activated | | Integration Deactivated | An integration was deactivated | | Integration Settings Updated | Integration settings were modified | | Action | Description | | -------------- | ------------------------ | | Import Created | A new import was created | | Import Updated | An import was modified | | Import Started | An import was started | | Import Failed | An import failed | | Action | Description | | -------------- | ------------------------ | | Export Created | A new export was created | | Action | Description | | ------------ | ---------------------- | | User Created | A new user was created | | User Updated | A user was modified | | User Deleted | A user was deleted | | Action | Description | | -------------------- | ------------------------------ | | Subscription Created | A new subscription was created | | Subscription Updated | A subscription was modified | | Subscription Deleted | A subscription was deleted | | Action | Description | | ------------------------------ | --------------------------------------------- | | Custom Event Retention Updated | Custom event retention settings were modified | | Action | Description | | ----------------------- | --------------------------------------- | | Dynamic Content Created | A new dynamic content block was created | | Dynamic Content Updated | A dynamic content block was modified | | Dynamic Content Deleted | A dynamic content block was deleted | | Action | Description | | --------------------------- | ---------------------------------------- | | Email Domain Created | A new email sending domain was added | | Email Domain Activated | An email domain was activated | | Email Domain Deactivated | An email domain was deactivated | | Email Domain Deleted | An email domain was removed | | Email Domain Sends Canceled | Sending was canceled for an email domain | | Action | Description | | ------------------- | ------------------------------------------------------ | | Suppression Created | An email address was added to the suppression list | | Suppression Deleted | An email address was removed from the suppression list | *** ## FAQ ### Who can access audit logs? **Organization-level audit logs:** Organization Admins can access audit logs for the entire organization. **App-level audit logs:** App Admins can access audit logs for apps they administer. Organization Admins can also access app-level audit logs for all apps in their organization. See [Manage Team Members](./manage-team-members) to update roles. ### Can I view audit logs for a specific app? Yes. Navigate to your app's **Settings** page and select **Audit Logs** to view activity for that specific app only. This is useful for investigating changes to a particular app without seeing activity from other apps in your organization. ### Can I export audit logs? Yes. Click **Export** on the Audit Logs page to download a `.jsonl` file of the currently filtered results. The export window depends on your plan — see [Export availability](#export-availability) for details. For programmatic access, use the [List audit logs API](/reference/list-audit-logs) (Enterprise plans only). ### Can audit logs be deleted? No. Audit logs are immutable and cannot be deleted by any user. ### Are API actions logged? Yes, for select actions. API actions performed with Organization and App API keys are logged for auth token management (create, update, rotate, delete), app management (create, update), segment management (create, update, delete), template management (create, update, delete, copy to app), and subscription CSV export. The actor type displays as "API Key" with the key's name and scope. Additional API actions are planned for future releases. ### How do I increase my retention period? Enterprise customers receive 90-day retention automatically. Professional customers can upgrade to 90-day retention by adding the Legal & Security Package. Contact your Account Manager or our [Sales Team](https://onesignal.com/contact) to discuss your requirements. *** # Badges Source: https://documentation.onesignal.com/docs/en/badges Manage app icon badge counts for push notifications on iOS, Android, and Huawei, including auto-clearing, API control, and native badge logic. Badges are the little numbered dots that appear on your mobile app icon. They help capture user attention and can influence engagement behavior. On iOS in particular, badges require additional setup and offer flexible control, as outlined below. For Android Web Push notifications, the badge refers to the small icon shown on notifications—not the app icon—and can be customized. See [Web Push Badges](./push#badges). *** ## Android badges Android app icon badge behavior can be managed through [Android notification categories](./android-notification-categories). You can control whether a category (channel) displays a badge and set badge behavior on a per-category basis. *** ## Huawei badges On Huawei devices, a badge can be displayed as a number or a dot on the app icon, depending on the user's device settings. Badges help indicate unread messages or pending actions, encouraging users to open the app. OneSignal lets you control Huawei badge counts directly through the dashboard or API. The badge displays on the app icon as either a **numeric count** or a **dot**, depending on the user's device setting (**Settings > Notifications > App icon badges**). Your API call controls the underlying count; the device decides the visual style. **Parameters** | Parameter | Type | Range | Description | | ---------------------- | ------- | ----- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `huawei_badge_class` | string | — | **(Required)** Fully qualified class name of your app's launcher Activity (e.g., `com.example.myapp.MainActivity`). Tells the Huawei system which app icon to badge. | | `huawei_badge_set_num` | integer | 0–99 | Sets the badge to an exact number. `0` clears the badge. | | `huawei_badge_add_num` | integer | 1–99 | Increments the existing badge count by this amount. | **Behavior rules** * `huawei_badge_class` is required for any badge operation. * If both `huawei_badge_set_num` and `huawei_badge_add_num` are provided, `huawei_badge_set_num` takes priority. * If neither is provided (but `huawei_badge_class` is set), the badge count increments by 1 by default. **Send Huawei push with badges** 1. Go to **Messages > Push** or **Templates** 2. Under **Platform Settings > Send to Huawei Android > Badge** 3. Choose either: * **Don't set** — badge is not affected by this notification * **Set to** — sets the badge to a specific number (0–99) * **Increase by** — increments the existing badge count (1–99) Use the following parameters in the [Create message](/reference/create-message) API: * `huawei_badge_class` — *(string, required for badge)* The fully qualified class name of the app's launcher activity (e.g., `"com.example.myapp.MainActivity"`) * `huawei_badge_set_num` — *(integer, 0–99)* Sets the badge count to this exact number. Set to `0` to clear the badge. * `huawei_badge_add_num` — *(integer, 1–99)* Increments the existing badge count by this number. If omitted along with `huawei_badge_set_num`, defaults to incrementing by 1. **Set badge to a specific number:** ```json theme={null} { "huawei_badge_class": "com.example.myapp.MainActivity", "huawei_badge_set_num": 5 } ``` **Increment badge by a specific amount:** ```json theme={null} { "huawei_badge_class": "com.example.myapp.MainActivity", "huawei_badge_add_num": 1 } ``` **Clear the badge:** ```json theme={null} { "huawei_badge_class": "com.example.myapp.MainActivity", "huawei_badge_set_num": 0 } ``` **Clearing badges** Huawei does **not** automatically clear the badge when a user opens the app or taps a notification. To clear the badge, you have two options: * **Via the API or dashboard**: Send a notification with `huawei_badge_set_num` set to `0` (or use **Set to > 0** in the dashboard). This can be a [data/background notification](./data-notifications) if you don't want a visible notification to appear. * **Via client-side code**: Your app can clear the badge locally using the Huawei badge API. This requires the `com.huawei.android.launcher.permission.CHANGE_BADGE` permission in your `AndroidManifest.xml`. See [Huawei's badge development guide](https://developer.huawei.com/consumer/en/doc/hmscore-guides/android-badging-0000001050042083) for implementation details. The `huawei_badge_set_num` parameter requires EMUI 10.0.0 or later and Push SDK 10.1.0 or later. On older devices, only `huawei_badge_add_num` is supported. *** ## iOS badges To ensure badge counts increment correctly on iOS, you must configure: * The `OneSignalNotificationServiceExtension` * App Groups See [Mobile SDK setup](./mobile-sdk-setup) for full instructions. By default, the OneSignal SDK will: 1. Clear the app icon badge when the app is opened. 2. Remove notifications from the Notification Center. If you want to retain notifications and manage badge logic manually (e.g., using your own counter or syncing state across devices), you can disable this automatic behavior. **Common use cases for manual badge control** * Reset badge when the app launches or resumes * Increment badge when a notification is received in the foreground * Decrement when a message is read or dismissed * Sync badge state across devices or app extensions via App Groups or your backend ### Disable automatic notification and badge clearing In your app's `info.plist`, add the Key: `OneSignal_disable_badge_clearing` with Boolean type to Value `YES` Xcode Info.plist editor showing OneSignal_disable_badge_clearing set to YES ```xml theme={null} OneSignal_disable_badge_clearing ``` This prevents the SDK from automatically removing notifications or resetting the badge when the app opens. #### iOS native badge management If you disable OneSignal's automatic badge clearing, you can use Apple's native APIs to control badge behavior. Apple deprecated `UIApplication.shared.applicationIconBadgeNumber` in iOS 17. You should now use the following methods from the [UserNotifications framework](https://developer.apple.com/documentation/UserNotifications/UNUserNotificationCenter/setBadgeCount\(_:withCompletionHandler:\)): **Set badge count** To set the badge on the app icon to a specific value: ```swift theme={null} import UserNotifications import UIKit if #available(iOS 17.0, *) { UNUserNotificationCenter.current().setBadgeCount(5) { error in if let error = error { print("Failed to set badge count: \(error.localizedDescription)") } else { print("Badge count updated successfully.") } } } else { UIApplication.shared.applicationIconBadgeNumber = 5 } ``` **Get current badge count** iOS does not provide a method to retrieve the current badge count from the system. You must keep track of the badge count in your app's state (for example, using `UserDefaults`, your app's data model, or syncing with your backend). ```swift theme={null} // Example: Store and retrieve badge count using UserDefaults let badgeCount = UserDefaults.standard.integer(forKey: "badgeCount") // Update badge count as needed UserDefaults.standard.set(badgeCount, forKey: "badgeCount") ``` **Increment or decrement badge** You must manage badge logic manually, as relative changes (like +1 or -1) are not supported in payloads. Update your stored badge count and then set it: ```swift theme={null} // Example: Increment badge count and update system badge let currentCount = UserDefaults.standard.integer(forKey: "badgeCount") let updatedCount = max(0, currentCount + 1) UserDefaults.standard.set(updatedCount, forKey: "badgeCount") if #available(iOS 17.0, *) { UNUserNotificationCenter.current().setBadgeCount(updatedCount) } else { UIApplication.shared.applicationIconBadgeNumber = updatedCount } ``` **Clear badge** To remove the badge entirely: ```swift theme={null} if #available(iOS 17.0, *) { UNUserNotificationCenter.current().setBadgeCount(0) } else { UIApplication.shared.applicationIconBadgeNumber = 0 } ``` ### Send iOS push with badges You can set the badge count in the OneSignal dashboard or using the API. 1. Go to **Messages > Push** or **Templates**. 2. Under **Platform Settings > Send to Apple iOS > Badges**. 3. Choose either: * Set to a specific number. * Increase by a relative amount. OneSignal dashboard push message form showing iOS badge count options Use the following parameters in the [Create message](/reference/create-message) API: * `ios_badgeType` — Set to `SetTo` or `Increase`. * `ios_badgeCount` — The number to set or increase by. ```json theme={null} { "ios_badgeType": "Increase", "ios_badgeCount": 1 } ``` When sending iOS push notifications, the badge count changes based on these options. If the app is open, the badge count resets unless you [disable automatic badge clearing](#disable-automatic-notification-and-badge-clearing). *** ## FAQ ### Why isn't my badge count incrementing on iOS? Ensure you have configured the `OneSignalNotificationServiceExtension` and App Groups. Without these, badge counts cannot increment correctly. See [Mobile SDK setup](./mobile-sdk-setup) for full instructions. ### How do I clear badges on Huawei? Send a notification with `huawei_badge_set_num` set to `0`, or use **Set to > 0** in the dashboard. You can also use a [data/background notification](./data-notifications) to clear badges without showing a visible notification. Alternatively, clear the badge client-side using the Huawei badge API. ### Can I set badges for web push? No. App icon badges are only supported on iOS, Android, and Huawei. For Android web push, the "badge" refers to the small icon shown on the notification itself — see [Web Push Badges](./push#badges). *** ## Related pages Control badge display and behavior per notification channel on Android. Set up Huawei push messaging with OneSignal. Full reference for push notification features including web push badges. Configure the OneSignal SDK including badge prerequisites for iOS. # Usage & billing Source: https://documentation.onesignal.com/docs/en/billing-faq OneSignal billing overview covering invoices, payment methods, plan changes, usage metrics, and cost management for all plan types. For plan features and pricing tiers, see the public [Pricing page](https://onesignal.com/pricing). This guide explains: * How to complete common billing tasks * What affects your invoice amount * How usage is calculated * How to manage or reduce costs * How billing relates to Apps and Organizations *** ## Common billing tasks View your Organization's Billing and Usage within **Organizations > Billing**. You must be an Organization Admin to view the billing page. See [Manage Team Members](./manage-team-members) and ask an Org Admin to add you as a Billing Contact. You can also view App-level usage in **Settings > Usage**. OneSignal Organization Billing page showing invoices, payment method, and usage ### Download an invoice **Self-serve (Growth & legacy plans)** 1. Go to **Organizations > Billing**. 2. Under **Invoices**, select the invoice. 3. Click **Download**. The invoice downloads immediately as a PDF. **Professional & Enterprise (contract plans)** Email `ar@onesignal.com` with: * Full company name * Billing email (if available) * Organization ID (found in **Organizations > Keys & IDs**) *** ### Update your payment method **Self-serve (Growth & legacy plans)** 1. Go to **Organizations > Billing**. 2. Next to **Payment Method**, click **Update**. 3. Enter your updated payment details and **Save**. Your updated payment method appears immediately in the billing page. **Professional & Enterprise (contract plans)** 1. Open the secure billing portal from your **Billing Welcome Email**. 2. Enter your updated payment details. 3. Save changes. If you cannot find the billing portal link, email `ar@onesignal.com` with: * Full company name * Billing email (if available) * Organization ID (found in **Organizations > Keys & IDs**) *** ### Change billing information **Self-serve (Growth & legacy plans)** 1. Go to **Organizations > Billing**. 2. Next to **Billing Profile**, click **Edit**. 3. Enter your updated billing information and **Save**. Your updated billing information appears immediately in the billing page. **Professional & Enterprise (contract plans)** 1. Open the secure billing portal from your **Billing Welcome Email**. 2. Enter your updated billing information. 3. Save changes. If you cannot find the billing portal link, email `ar@onesignal.com` with: * Full company name * Billing email (if available) * Organization ID (found in **Organizations > Keys & IDs**) *** ### Upgrade your plan If you need higher limits or additional features, [contact our Sales team](https://onesignal.com/contact). *** ### Downgrade to Free **Self-serve (Growth & legacy plans)** 1. Go to **Organizations > Billing**. 2. Select **Change Plan**. 3. Click the **Free** plan. 4. Confirm the downgrade. You will continue on the same plan until the end of your current billing cycle. Paid features are removed at the end of your current billing cycle. **Professional & Enterprise (contract plans)** Contract plans must follow agreement terms. Contact your Account Manager or `ar@onesignal.com` to discuss changes. *** ### Reactivate a suspended account Self-serve accounts are disabled after **three failed payment attempts**. To reactivate: 1. Pay any outstanding invoices. 2. Allow up to one business day for reactivation. If your account is not restored, email `support@onesignal.com` with: * Full company name * Billing email (if available) * Organization ID (found in **Organizations > Keys & IDs**) *** ## What affects your invoice amount? Your invoice is based on one or more of the following: * Active **Mobile Monthly Active Users (MAU)** * Active **Web Push Subscribers** * **Email sends** * **In-App message impressions** * **Event Streams volume** * **Custom Event storage** To confirm your pricing model, go to **Organizations > Billing**. You will see either: * **Mobile MAU + Web Push Subscribers** (current pricing model) * **Push Subscribers** (legacy plans only) *** ## How billing is calculated ### Monthly Active Users (MAU) A Monthly Active User (MAU) is a mobile push subscription active within the last 30 days, regardless of current subscription status. * Each active subscription counts separately. * One user with two active subscriptions counts as **2 MAU**. Billing is calculated in **increments of 10 MAU**, rounded **down** to the nearest multiple of 10. See [Subscriptions](./subscriptions) for more details. ### Web Push Subscribers Users who subscribed to receive web push notifications from a supported web browser. * Each subscribed browser counts separately. * One user subscribed on two browsers counts as **2 subscribers**. Billing is calculated in **increments of 10 subscribers**, rounded **down** to the nearest multiple of 10. See [Subscriptions](./subscriptions) for more details. *** ### Push Subscribers (legacy plans only) Push Subscriber pricing includes total subscribed mobile and web push subscriptions. To reduce subscriber count, remove users via the dashboard or API. See [Delete Users](./delete-users). To migrate from Push Subscriber pricing to MAU pricing, [contact our Sales team](https://onesignal.com/contact). *** ## Messaging usage ### Email sends You are billed for emails that leave OneSignal, including: * Delivered * Bounced * Most Failures Exceptions: * **Invalid ESP credentials** * **Delivery Error** *** ### RCS RCS messages are billed per message segment based on the message type: | Type | Audience | Description | | -------------- | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Rich** | US | Text-only messages — cannot include a title. Can include actions such as replies, dials, or links, but link actions must open in a new tab. Messages longer than 160 characters are broken into segments and charged per segment. | | **Rich Media** | US | Messages with media, cards with titles and/or actions that open in a web view (not a new tab), and carousel content. | | **Basic** | International | Text messages up to 160 UTF-8 characters. Does not include any actions, media, or carousels. | | **Single** | International | Text messages greater than 160 UTF-8 characters, up to the max message length of 3,072 characters. Can also include actions, media, or carousels. | For more on RCS message types, see [Rich Communication Services (RCS)](./rcs-messaging#types-of-rcs-messages). *** ### In-App impressions Counts each display of an [in-app message](./in-app-messages-setup) in mobile apps. This metric does not include push notifications or email. *** ## Event-related billing Event billing includes both **delivery volume** and **storage volume**. ### Event Streams (delivery volume) Includes: * [Event Streams](./event-streams) * Webhooks * Integration destinations Events delivered through integrations count toward your monthly limit. | Plan | Active Event Streams | Monthly Event Limit | | ------------ | -------------------- | ------------------- | | Free | 1 | Up to 1,000 events | | Growth | 3 | Up to 10,000 events | | Professional | Custom | Based on volume | | Enterprise | Custom | Based on volume | Webhooks are a type of Event Stream destination. Events delivered through webhooks and integrations count toward your Event Streams monthly event limit. *** ### Custom Events (storage volume) Custom Events storage is different from Event Streams. **Event Streams** send message data (delivery, clicks, failures) **out** of OneSignal to external destinations like webhooks and integrations. **Custom Events** are sent **into** OneSignal from your app or backend systems to power Segments, Journeys, and analytics. They have separate billing and separate plan limits. [Custom Events](./custom-events) are billed based on the number of events **stored** per month. Each plan includes a monthly storage allotment; overages are charged at your contracted unit rate. By default, all events are stored with unlimited retention. You can adjust the retention period per event type at any time. Each plan includes: | Plan | Included Stored Events / Month | | ---------- | ------------------------------ | | Growth | 1M | | Pro | 5M | | Enterprise | 10M | ### How retention affects Custom Event cost * Retention is measured per event name. * Default retention is **unlimited**. * Minimum retention is **30 days**. * Billing is based on stored events at the end of the billing cycle. * Shorter retention reduces billable stored events. To adjust retention, go to **Data > Custom Events** in the OneSignal dashboard, then select the **Event Storage** tab. From there you can set retention per event name. Changes take effect at the next billing cycle. Events received through integrations (e.g., Amplitude, Segment) also count toward your stored event total. *** ## Analytics data retention Dashboard reporting data — including message delivery reports, conversion metrics, and template analytics — is retained based on your plan: | Plan | Retention period | | ------------ | ---------------- | | Free | 30 days | | Growth | 90 days | | Professional | 1 year | | Enterprise | 2 years | See the [Pricing page](https://onesignal.com/pricing) for more details on plan features. ## Message event retention Message event data powers [segment filters](./segmentation#message-event-filters), letting you target users based on how they interacted with your messages. How far back that data is available depends on your plan: | Plan | Message event retention | | ------------ | ----------------------- | | Growth | 30 days | | Professional | 60 days | | Enterprise | 90 days | The segment filter UI shows a time window of up to 90 days, but queries beyond your plan's retention period will not return results. *** ## FAQ ### How can I manage or reduce costs? You can reduce billing by: **If you are on MAU pricing:** * Reduce duplicate mobile subscriptions or limit the number of subscriptions per user. * Use our [SDK privacy methods](./mobile-sdk-reference#privacy) to delay SDK initialization and subscription creation until you are ready to create a subscription for the user. **If you rely heavily on email:** * Send to more targeted segments. * Clean inactive and invalid email addresses. **If you use Custom Events:** * Shorten retention periods. * Consolidate similar event names. **If you use Event Streams:** * Audit integrations. * Reduce unnecessary events. For volume discounts (for example, more than 50k MAU or 250k emails per month), [contact our Sales team](https://onesignal.com/contact). ### How is billing related to Apps and Organizations? * You can create unlimited apps and organizations. * Apps are not billed individually. * Billing occurs at the **Organization level**. * You can create multiple Organizations to: * Separate apps that you want to be billed for vs those you do not want to be billed for. * Add apps for different brands, clients, or environments. * Example: a "Free" Organization for development or testing and a "Paid" Organization for production. See [Apps & Organizations](./apps-organizations). ### When am I charged? * **Self-serve (Growth & legacy plans)**: Monthly on the same calendar date you started. * **Professional & Enterprise (contract plans)**: According to your contract terms. ### Can I cancel anytime? * **Self-serve (Growth & legacy plans)**: Yes. Downgrade to Free. * **Professional & Enterprise (contract plans)**: Contract terms apply. ### Can I pay by invoice? **Custom Professional and Enterprise (contract plans)** can be paid by invoice. Ask your Account Manager or [contact our Sales team](https://onesignal.com/contact). ### Can someone else request invoices? * **Self-serve (Growth & legacy plans)**: Only [Org Admins](./manage-team-members) can view and edit billing information in the Organization Billing page. * **Professional & Enterprise (contract plans)**: Email `ar@onesignal.com` with: * Full company name * Billing email (if available) * Organization ID (found in **Organizations > Keys & IDs**) ### How do I add or remove billing contacts? See [Manage Team Members](./manage-team-members) to remove team members from accessing the App and/or Organization. To remove a billing contact, email `ar@onesignal.com` with: * Full company name * Billing email (if available) * Organization ID (found in **Organizations > Keys & IDs**) ### What payment methods are accepted? Visa, MasterCard, American Express, Discover, Diners Club International, JCB, and PayPal. Payments are processed by Recurly using 128-bit SSL. OneSignal does not store your card details. **Currency** We currently accept USD only. ### How does deleting subscriptions affect my bill? For **web push**, billing is calculated based on number of subscriptions, so deleting subscriptions before the end of your billing cycle will lower your bill. For **mobile push**, billing is calculated based on monthly active users. Deleting a subscription that has been active in your current billing cycle before the end of the cycle will lower your bill. For **in-app messages, SMS, and Email**, billing is not based on your subscription count, so deleting subscriptions will not affect these parts of your bill. *** ## Related pages Manage apps, organizations, and how billing is structured across them. Add or remove team members and control access to billing. How mobile and web push subscriptions are counted toward billing. Compare plan features and pricing tiers. # Web push browser behavior Source: https://documentation.onesignal.com/docs/en/browser-behavior-and-unsubscribes Learn how to unsubscribe from notifications and understand how browsers handle web push subscriptions. This guide explains how to manage web push [subscriptions](./subscriptions) and how subscription status is affected by both user action and browser behavior. *** ## Understand push permissions Users must give your website permission to send them push notifications. It is not possible to receive push notifications without explicitly granting the site permission using the system-level permission prompt. Permissions can be either: * **Default**: permission has not been granted to denied. * **Granted**: you allowed the website to send you notifications. * **Denied**: you blocked the website to send you notifications. This can be a temporary block if you clicked the **x** to close the prompt repeatedly or a permanent block if you clicked **Block** or toggled off permission in the browser settings. For more details on the native system-level permission prompt and/or any of the OneSignal prompts, see [Web permission prompts](./permission-requests). *** ## How to unsubscribe from web notifications You can unsubscribe from web push notifications in three ways: ### Unsubscribe within browser settings You can manage or remove notification permissions directly in browser settings. Here are quick-access URLs and official docs to learn more: * **Chrome**: `chrome://settings/content/notifications` ([Learn more on Chrome's docs](https://support.google.com/chrome/answer/3220216?hl=en\&co=GENIE.Platform%3DDesktop\&sjid=12874758545589453111-NA)) * **Edge**: `edge://settings/content/notifications` ([Learn more on Microsoft's docs](https://www.microsoft.com/en-us/edge/learning-center/how-to-turn-off-block-browser-notifications?form=MA13I2)) * **Firefox**: `about:preferences#privacy` scroll to Permissions > Notifications > Settings ([Learn more on Mozilla's docs](https://support.mozilla.org/en-US/kb/push-notifications-firefox)) * **Safari**: Settings > Websites > Notifications ([Learn more on Safari's docs](https://support.apple.com/guide/safari/customize-website-notifications-sfri40734/16.1/mac/13.0)) On these pages, just click the options to remove or block the website(s) you don't want notifications from. ### Unsubscribe while on the website **Reset permission** Most browsers have a "lock" or "settings" icon next to the URL. Clicking it reveals site-specific permissions where users can disable push notifications. **OneSignal prompts** If the website contains the OneSignal [Bell Prompt](./permission-requests) or [Custom Link prompt](./permission-requests) users can unsubscribe directly via those UI elements and be able to resubscribe using the same as desired. ### Deleting browser data, clearing cookies and site data If you delete history and/or delete your cookies and site data, it will temporarily prevent notifications from showing. However, if you don't remove push permissions from the site, you may be automatically re-subscribed and start getting notifications again upon returning to the site.
*** ## How to test your permission prompts These steps explain how to test your prompt and subscription flow like a first time visitor. **Do not use an incognito, private, or guest browser setting.** This example uses Chrome version 135 on macOS but the flow should be relatively the same for most browsers. Click the site settings or lock icon next to the site URL and select **Reset permission** or remove permissions for Notifications. Skip to the next step if you don't see this permission option. Click **Cookies and site data > Manage on-device site data** or follow the browser's flow to see your site's data option. Delete the data for your site and exit the settings to get back to your site. Usually you can just right click the screen and press **Inspect**. If you do not see the prompt or don't know the steps, see [Web permission prompts](./permission-requests). If you see anything in red related to OneSignal, see our [Web SDK troubleshooting](./troubleshooting-web-push) docs. In the **Console** type or copy-paste this code: `OneSignal.User.PushSubscription.id` 1. This will log your OneSignal subscription ID. Copy-paste this into your OneSignal Dashboard Audience > Subscriptions tab. 2. If a subscription ID was not logged in the console, then you are not successfully subscribed. Please see [Web SDK troubleshooting](./troubleshooting-web-push) for details. See [Push](./push) for more details if needed. If you did not receive a push, see [Web push: Notifications not shown](./notifications-not-shown-web-push) for further debugging. You have successfully setup web push with OneSignal. Next steps: * [Web push setup](./web-push-setup) - additional non-developer web setup steps. * [Web SDK setup](./web-sdk-setup) - developer web SDK setup steps. * [Web SDK troubleshooting](./troubleshooting-web-push) - troubleshooting if you see errors in the console or not getting a subscription ID. * [Web push: Notifications not shown](./notifications-not-shown-web-push) - troubleshooting notifications not displaying on your device. *** ### Receiving Notifications When the Browser is Closed Browsers behave differently across platforms. Please refer to the table below for support for receiving notifications even when the browser is closed. | Browser Name | Android | Windows | macOS | | ----------------- | ------- | ------- | ----- | | Chrome / Chromium | Yes | Yes | No | | Firefox | Yes | Yes | No | | Safari | N/A | N/A | Yes | | Opera | Yes | Yes | No | | Edge | Yes | Yes | No | **Chrome** - Chrome runs as a background process by default even when all the windows are closed. As long as the background process is running, notifications will still be received. If the Chrome background process is not running, notifications will not be received. **Firefox** - On Mac OS X, the process still exists even if windows are closed, and a notification can be received if all windows are closed (as long as there is still a dot in the dock showing Firefox is still running). On Windows, the process exits after all windows are closed so notifications cannot be received unless a Firefox window is still open. **Safari** - Safari does not have to be running to receive notifications, as they are sent directly to the operating system. The user still has to sign up for Safari web notifications, but after that they will be received even when Safari is completely closed. Subscribers have up to 3 days to retrieve the last known missing notification before messages expire permanently. For example, suppose a subscriber was supposed to receive a Firefox web push notification, but Firefox was closed. If the subscriber opens Firefox within 3 days, the subscriber will receive only the last known web push notification that didn't expire. If the subscriber opens Firefox after 3 days, the web push notification sent more than 3 days ago will not be received. *** # Channel setup Source: https://documentation.onesignal.com/docs/en/channel-setup Set up push notifications, in-app messages, email, SMS, RCS, and Live Activities in OneSignal to reach Users across every channel. OneSignal supports multiple messaging channels, each with different capabilities and setup requirements. Choose the channels that match your goals, or combine several to build a multi-channel experience with [Journeys](./journeys-overview). Email and SMS can be configured without a developer. Push notifications, in-app messages, and Live Activities require SDK integration — see the [Developer guide](./developers) or [invite a developer](./manage-team-members) to your team. *** ## Messaging channels Alert-style notifications on iOS, Android, Huawei, and Amazon — even when the app isn't open. Browser-based notifications that reach Users even when your site isn't open. Marketing and transactional emails with built-in deliverability tools. Rich, interactive messages displayed while Users are active in your app. Direct text messages for urgent or time-sensitive communication. Rich messaging with branded content and read receipts on Android. Real-time updates on the iOS lock screen. Similar capabilities available for Android. *** ## Channel comparison | Channel | Developer required? | Setup time | Best for | | --------------- | ------------------- | ---------- | -------------------------------------------------------- | | Email | No | 15–60 min | Marketing campaigns, transactional messages, newsletters | | SMS | No | 15–60 min | Time-sensitive alerts, OTPs, appointment reminders | | Web push | Yes | 15–45 min | Re-engaging website visitors, announcements | | Mobile push | Yes | 30–60 min | App re-engagement, real-time alerts, promotions | | In-app messages | Yes (SDK) | 30–45 min | Onboarding, feature announcements, surveys | | RCS | No | Varies | Rich branded messaging on Android with read receipts | | Live Activities | Yes (SDK) | 45–60 min | Live scores, delivery tracking, event countdowns | *** ## Next steps After setting up a channel, continue with the rest of your OneSignal implementation. Full implementation walkthrough: Users, segments, sending messages, and analytics. SDK reference, API docs, User identity, and testing for engineering teams. Compose and send your first message after setting up a channel. Automate multi-channel flows based on User behavior. *** ## FAQ ### Which channel should I set up first? Start with the channel that matches your most immediate goal. **Email** and **SMS** are the fastest to configure and don't require a developer. If you already have a mobile app, **push notifications** provide the highest visibility for re-engagement. ### Can I use multiple channels in the same OneSignal app? Yes. A single OneSignal app supports all channels. Adding multiple channels lets you use [Journeys](./journeys-overview) to automate multi-channel flows — for example, sending a push notification, then following up with an email if the User doesn't engage. ### Do I need separate apps for testing and production? Yes, this is strongly recommended. Use separate OneSignal apps for development, staging, and production to avoid sending test messages to real Users. See [Apps, Organizations, and accounts](./apps-organizations) for details. # Confirmed receipt Source: https://documentation.onesignal.com/docs/en/confirmed-delivery Confirmed receipt tracks when a device receives a push notification sent through OneSignal. Covers requirements, platform limitations, and troubleshooting for iOS, Android, Huawei, and Web. Confirmed receipt (also known as "Confirmed Delivery") tracks when a device actually **receives** a push notification sent from OneSignal. When the OneSignal SDK on the device receives a push, it sends a **confirmation receipt** back to OneSignal containing the notification ID and the device's [Subscription ID](./subscriptions). This lets you see exactly which Subscriptions received which notifications. In your OneSignal Dashboard, "Confirmed receipt" appears in the [Push Notification Message Reports](./push-notification-message-reports). For all delivery and engagement metrics, see the [Analytics Metrics Glossary](./analytics-metrics-glossary). Confirmed receipt flow Confirmed receipt is different from "Delivered." Platform push services (APNs, FCM, ADM, HMS) report whether a notification was accepted by the service — not whether the device actually received it. Confirmed receipt is the device-side confirmation. *** ## Requirements * Available only on **paid plans**. [Compare plans](https://onesignal.com/pricing). * Complete the [Web SDK Setup](./web-sdk-setup) and/or [Mobile SDK Setup](./mobile-sdk-setup). * Confirmed receipt only works if the device has the OneSignal SDK installed. * **Not supported** for subscriptions created via API only. ### Platform limitations #### iOS * Requires both the **Notification Service Extension** and **App Group** to be set up correctly. * Push notifications must include the `mutable-content` key set to `1`. This is set automatically by OneSignal. Make sure you are not explicitly setting `mutable_content: false`. * APNs keeps only **one message per app** when offline. If multiple pushes are sent while offline, only the latest is delivered. #### Huawei * Supported only for the `data` [Huawei message type](/reference/push-notification#body-huawei-msg-type). * For the `message` type, Huawei provides receipt data only in their own dashboard. #### Web * Safari does **not** support confirmed receipt. *** ## Troubleshooting confirmed receipt If you are not receiving push notifications at all, see [Notifications not showing](./notifications-show-successful-but-are-not-being-shown) first. ### iOS Confirmed receipt on iOS requires two things working together: 1. A **Notification Service Extension** (NSE) that runs OneSignal code when a push arrives 2. An **App Group** shared between your main app target and the NSE target so they can exchange data If either is missing or misconfigured, the device receives the push but never reports it back to OneSignal. #### Verify your iOS setup In Xcode, check that you have a **OneSignalNotificationServiceExtension** target listed under your project targets. If it does not exist, follow [Step 2 of the iOS SDK Setup](./ios-sdk-setup#step-2-add-a-notification-service-extension-nse). Open the NSE's `NotificationService.swift` (or `.m`) file. It must call `OneSignalExtension.didReceiveNotificationExtensionRequest` inside `didReceive(_:withContentHandler:)`. If the file still contains Apple's default template code, replace it with the [OneSignal NSE code](./ios-sdk-setup#step-2-add-a-notification-service-extension-nse). Select your **NSE target > General > Frameworks and Libraries** (or **Build Phases > Link Binary With Libraries**). Verify that `OneSignalExtension` is listed. The main app target uses `OneSignalFramework`, but the NSE target must use `OneSignalExtension` — these are different packages. The OneSignal SDK uses an App Group to share data between your main app and the NSE. There are two ways to configure this — pick the one that matches your setup. 1. Select your **main app target > Signing & Capabilities > App Groups**. 2. Confirm the App Group. If your App Group is `group.YOUR_MAIN_APP_BUNDLE_ID.onesignal` — where `YOUR_MAIN_APP_BUNDLE_ID` is your main app target's Bundle Identifier (found under **General > Identity**), then follow the Default App Group tab. Otherwise, follow the Custom App Group tab. 1. Select your **NSE target > Signing & Capabilities > App Groups**. 2. Confirm the **exact same** App Group is listed. If missing, add it via **+ Capability > App Groups** and select the same group. A common mistake is using the NSE's bundle identifier instead of the main app's: * **Correct:** `group.YOUR_MAIN_APP_BUNDLE_ID.onesignal` * **Wrong:** `group.YOUR_MAIN_APP_BUNDLE_ID.OneSignalNotificationServiceExtension.onesignal` Your main app target and NSE target should have the same App Group identifier. Use this option if your App Group identifier does not follow the `group.YOUR_MAIN_APP_BUNDLE_ID.onesignal` format. 1. Select your **main app target > Signing & Capabilities > App Groups**. 2. Confirm your custom App Group is listed. 3. Select the `Info.plist` of your main app target and add the following key: ```xml theme={null} OneSignal_app_groups_key group.your-custom-group-name ``` Replace `group.your-custom-group-name` with your actual App Group name. 4. Select your **NSE target > Signing & Capabilities > App Groups**. 5. Confirm the **exact same** custom App Group is listed. If missing, add it via **+ Capability > App Groups** and select the same group. 6. Select the `Info.plist` of your NSE target and add the following key: ```xml theme={null} OneSignal_app_groups_key group.your-custom-group-name ``` Replace `group.your-custom-group-name` with your actual App Group name. Your main app target and NSE target should have the same App Group identifier. If you use `.xcconfig` files for build settings, verify the App Group is not being overridden or omitted in those files. Select your **NSE target > General > Minimum Deployments**. This value must match your main app target's minimum deployment. A mismatch can prevent the NSE from running on certain OS versions. Select your **main app target > Build Phases > Embed App Extensions**. Make sure **"Copy only when installing" is unchecked**. If checked, the NSE is not embedded during development builds, so it never runs when testing. Select your **NSE target > Info** tab and expand the `NSExtension` key. Confirm it contains: ```xml theme={null} NSExtensionPointIdentifier com.apple.usernotifications.service NSExtensionPrincipalClass $(PRODUCT_MODULE_NAME).NotificationService ``` If your NSE is written in Objective-C, use `NotificationService` instead of `$(PRODUCT_MODULE_NAME).NotificationService`. OneSignal automatically sets `mutable-content: 1` in the push payload, which tells iOS to invoke the NSE. If you send pushes via the [REST API](/reference/push-notification), verify you are not explicitly setting `mutable_content: false`. Without `mutable-content`, iOS does not run the NSE and confirmed receipt cannot fire. Add this line temporarily inside `didReceive` before the OneSignal call: ```swift theme={null} bestAttemptContent.body = "[Modified] " + bestAttemptContent.body ``` Send yourself a test push. If the notification body starts with `[Modified]`, the NSE is running correctly. If it does not, revisit the steps above — the NSE is not being invoked. Remove this line after testing. For advanced NSE debugging with Xcode Console logs, see [Debugging the iOS Notification Service Extension](./service-extensions#debugging-the-ios-notification-service-extension). ### Android * If notifications are not displaying, see [Mobile push troubleshooting](./notifications-show-successful-but-are-not-being-shown). * If notifications show but confirmed receipt is missing, a custom Android Service Extension may be blocking it. Check the [Android Service Extension guide](./service-extensions#android-service-extension). ### Web * Safari is not supported. * For other browsers, ensure migration to **v16 SDK** is complete: * Correct SDK init: ```html theme={null} ``` * Correct Service Worker reference: ```html theme={null} importScripts("https://cdn.onesignal.com/sdks/web/v16/OneSignalSDK.sw.js"); ``` *** ## FAQ ### Why are my confirmed receipt numbers low or missing? The most common causes are setup issues (especially on iOS), inactive devices, and platform limitations. 1. **iOS misconfiguration:** The Notification Service Extension or App Group is missing or incorrect. See [Troubleshooting confirmed receipt on iOS](#ios). 2. **Inactive or abandoned devices:** Devices that are offline or no longer in use do not receive pushes or send confirmed receipt events. See [How do I handle inactive devices?](#how-do-i-handle-inactive-devices). 3. **Platform limitations:** Huawei `message` type and Safari do not support confirmed receipt. 4. **Android force quit:** Some device manufacturers treat swiping the app away as a force quit, which stops SDK events. See [Mobile push not shown guide](./notifications-show-successful-but-are-not-being-shown). ### How do I handle inactive devices? Devices that are offline or abandoned do not receive push notifications or send confirmed receipt events. This is common when users replace or abandon devices. To re-engage inactive users: * Use **Audience Activity** to resend to users who did not confirm receipt. * Create [Segments](./segmentation) based on **Last Session** (for example, inactive for 90+ days). * Combine with a [Re-engagement Journey](./journeys-examples#re-engagement-campaign) to win them back. * Periodically target inactive users to prune unreachable devices. See [When do push Subscription statuses update?](./subscriptions#when-do-push-subscription-statuses-update) for more details on how OneSignal updates subscription status. ### Why does confirmed receipt show but the notification doesn't appear? A confirmed receipt event means the device received the push payload. In rare cases, the notification may not display. Possible causes: * **Missed notification:** Send yourself a test push via [Find and set Test Users](./test-users) to rule this out. * **iOS Focus Mode:** "Do Not Disturb," "Sleep," or other [Focus modes](./ios-focus-modes-and-interruption-levels) delay or group notifications. You may have dismissed a grouped notification without seeing it. * **App code suppressing display:** * `event.preventDefault()` in the [foreground lifecycle listener](./mobile-sdk-reference#addforegroundlifecyclelistener-push) or [Notification Service Extension](./service-extensions) stops the notification from displaying. * Calls to [`removeDeliveredNotifications(withIdentifiers:)`](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter/removedeliverednotifications\(withidentifiers:\)) or [`removeAllDeliveredNotifications()`](https://developer.apple.com/documentation/usernotifications/unusernotificationcenter/removealldeliverednotifications\(\)) remove notifications after they arrive. * **Push payload settings:** * Ensure `priority` is set to high. See [Push priority](./push#priority). * [`collapse_id`](./push#collapse_id) replaces older pushes with newer ones using the same ID. *** ## Related pages Review delivery, engagement, and confirmed receipt metrics for each push. Definitions for every delivery and engagement metric in OneSignal. Add the Notification Service Extension and App Group required for iOS. Customize notification behavior on iOS and Android with service extensions. Need help? Chat with our Support team or email `support@onesignal.com` Please include: * Details of the issue you're experiencing and steps to reproduce if available * Your OneSignal App ID * The External ID or Subscription ID if applicable * The URL to the message you tested in the OneSignal Dashboard if applicable * Any relevant [logs or error messages](/docs/en/capturing-a-debug-log) We're happy to help! # Create a custom email unsubscribe page Source: https://documentation.onesignal.com/docs/en/create-custom-unsubscribe-page Learn how to replace OneSignal's default unsubscribe link with a branded, multi-language, and personalized email preferences page, while maintaining compliance and tracking. ## Overview OneSignal provides a [default email-compliant unsubscribe experience](./unsubscribe-links-email-subscriptions) that injects a link into your email templates so users can unsubscribe with ease and their [subscription statuses](./subscriptions) are updated in real-time. If you want full control over branding, copy, and fields (such as category opt-outs), you can replace the default link with your own custom page and use the OneSignal API to unsubscribe or update user preferences. This guide explains how to add your own custom unsubscribe page to emails (removing the default OneSignal link) and which of our APIs to use to unsubscribe the user's email Subscription. If you want to add more functionality to your custom unsubscribe page (like opt out of specific email categories instead of all), this is detailed in our [Preference Center](./preference-center) tutorial. *** ## Remove OneSignal's default unsubscribe link OneSignal automatically inserts a special link in the format `[unsubscribe_url]` to your email templates. This URL unsubscribes the user from all emails in OneSignal. See [Email Unsubscribe Links](./unsubscribe-links-email-subscriptions) for details. To use your own page, **locate and remove the default link** in your template. In the drag-and-drop editor, the default link may appear nested like: Drag-and-Drop editor unsubscribe link ```html theme={null} Unsubscribe ``` *** ## Add your custom unsubscribe link Now that you have removed our special link, you can replace it with your own URL. Many times, these links require some additional data to be passed to your page. Use [Liquid variables](./message-personalization) to pass OneSignal data to your page. Common parameters: | Parameter | Description | | -------------------------------- | ------------------------------------- | | `subscription.email` | Subscriber’s email address | | `subscription.external_id` | User’s external ID | | `app.id` | OneSignal App ID | | `message.id` | ID of the email notification | | `subscription.language` | Preferred language (for localization) | | `subscription.unsubscribe_token` | Security token for API verification | **Example URL:** ```liquid theme={null} https://examplesite.com/unsubscribe?app_id={{app.id}}¬ification_id={{message.id}}&email={{subscription.email}}&language={{subscription.language}}&token={{subscription.unsubscribe_token}} ``` ```html HTML theme={null}
Unsubscribe

from our emails

``` Add custom unsubscribe link ### Disable click tracking Unsubscribe clicks are generally not used for engagement metrics. If you want to disable link tracking, you can add the `data-disable-tracking="true"` attribute to your link like this: ```html HTML theme={null} Unsubscribe ``` **Provider-specific attributes:** | Provider | Attribute | | --------- | ------------------------------ | | OneSignal | `data-disable-tracking="true"` | | Mailgun | `disable-tracking=true` | | SendGrid | `clicktracking=off` | | Mandrill | `mc:disable-tracking` | *** ## Hosting your custom unsubscribe page Deploy a web page that: * Reads query parameters from the unsubscribe link. * Displays user-friendly opt-out or preference options. * Sends the unsubscribe or update request to OneSignal via API. We provide a working [GitHub sample project](https://github.com/OneSignalDevelopers/custom-email-unsubscribe-page-sample) you can fork and deploy. Sample unsubscribe page *** ## Calling the OneSignal API Depending on your use case, you can use the following APIs to unsubscribe or update user preferences: * [Update Subscription by Token](/reference/update-subscription-by-token) * [Unsubscribe Email with Token](/reference/unsubscribe-with-token) * [Update User](/reference/update-user) This API is most commonly used when you have the user's email address and just want to subscribe or unsubscribe them from all emails. **Required query parameters:** * `app_id` * `token` **Authentication required** * Call this API from your server. Use this API if you want to track which specific email caused the unsubscribe. This uses the email message ID to track the unsubscribe. See [Unsubscribe Email with Token](/reference/unsubscribe-with-token) API for more details. **Required query parameters:** * `app_id` * `notification_id` * `token` **Optional parameters:** * `email` * `language` **Example JavaScript:** ```javascript JavaScript theme={null} const unsubscribeURL = (href) => { const unsubscribeURL = new URL(href) const appID = unsubscribeURL.searchParams.get("app_id") const notificationID = unsubscribeURL.searchParams.get("notification_id") const unsubscribeToken = unsubscribeURL.searchParams.get("token") const language = unsubscribeURL.searchParams.get("language") const email = unsubscribeURL.searchParams.get("email") return { unsubscribeURL: `https://api.onesignal.com/apps/${appID}/notifications/${notificationID}/unsubscribe?token=${unsubscribeToken}`, meta: { language, email }, } } ``` Update the user's preferences using the [Update User](/reference/update-user) API. **Required query parameters:** * `app_id` * `alias_id` - The user's external ID is recommended. **Authentication required** * Call this API from your server. *** You should now be equipped with everything you need to know about creating a custom unsubscribe page. *** # Data feeds Source: https://documentation.onesignal.com/docs/en/data-feeds Pull real-time data from your APIs into OneSignal email messages at send time using Data Feeds, with Liquid syntax for personalization. Data Feeds let you pull **real-time data from your APIs directly into messages** at send time. This allows you to deliver highly personalized content without pre-loading data into OneSignal. Use Data Feeds when your data changes frequently, such as: * A user’s current rewards balance * Latest order status * Personalized product recommendations Other personalization methods (like [Tags](./add-user-data-tags) or [Dynamic Content](./dynamic-content)) work well for static data. **Data Feeds are best for live, fast-changing values**. Data Feeds are currently available **only for email messages sent through Journeys**. Need another channel? [Fill out this short survey](https://survey.survicate.com/954895d3e7ed1348/?p=anonymous). *** ## How Data Feeds work 1. **Create a Data Feed** – Configure how OneSignal connects to your API. 2. **Attach the Data Feed** to a message template. 3. **Insert response fields** in your message using [Liquid syntax](./using-liquid-syntax). 4. **At send time**, OneSignal makes an API call for each recipient, parses the response, and injects the data into your message. ### Example: Display reward points Suppose you want to show each customer their rewards balance: ```liquid theme={null} Hi {{ first_name }}, You have {{ data_feed.rewards.points }} points! Your membership status is {{ data_feed.rewards.status_level }}. Keep shopping to earn more points! ``` When Sarah receives this email, the Liquid variables are replaced with her actual points balance and membership status. The following sections walk through setting up this example step by step. ## Creating and using a Data Feed ### 1. Set up your Data Feed configuration Navigate to **Data > Data Feeds** in the sidebar to see the list of existing Data Feeds and create a new one. Each Data Feed must have: * **Name**: A descriptive name like "Customer Rewards API" to help you distinguish feeds. Unique names are recommended but not required. * **Alias**: A short identifier like `rewards` used in Liquid syntax (e.g., `{{ data_feed.rewards.points }}`). Must be unique, lowercase, alphanumeric, with no spaces or special characters. * **Method**: The HTTP method OneSignal uses to contact your API. Usually `GET`, but `POST` is also supported. * **URL**: Your API endpoint. Supports Liquid syntax so OneSignal can fetch user-specific data. For example, your rewards endpoint might accept the user's `external_id` (stored in OneSignal) as a URL parameter: ```liquid theme={null} https://acme.com/customers/user_id={{ external_id }}/rewards ``` * **Headers**: Key-value pairs required by your API (e.g., authentication tokens). Supports Liquid syntax. * **Body**: Optional JSON request body. Supports Liquid syntax, the same as [Journey webhooks](./journeys-webhook#personalization). For example, your API might require the user's ID in the request body instead of the URL: ```json theme={null} { "customer_id": "{{ subscription.external_id }}" } ``` A complete Data Feed configuration looks like this: Data Feed configuration showing name, alias, method, URL, and headers Test your feed before using it in production. Data Feed tests run against your [Test Users](./test-users), so verify that those subscriptions have attributes that return a real result from your API. Finally, **Activate** your new Data Feed so it’s ready for use. ### 2. Attach the Data Feed to your message template Attach your Data Feed to your message template so that OneSignal knows to use it. 1. Navigate to **Messages > Templates** 2. In the Message section, select the **Personalization** button Personalization button in the message template editor 3. Toggle on **Data Feeds** and select your feed Data Feeds toggle and feed selector in the message composer 4. Save your template ### 3. Use the data in your message Use Liquid syntax to insert response data anywhere in your message. Continuing the rewards example, the API response for Sarah (whose `external_id` is `a1-b2c3`) might look like this: ```json theme={null} { "external_id": "a1-b2c3", "points": 193, "status_level": "Gold" } ``` To insert the number of points and the status level, use dot notation to reference the Data Feed alias and response fields: ```liquid theme={null} You have {{ data_feed.rewards.points }} points! Your membership status is {{ data_feed.rewards.status_level }}. ``` This tells OneSignal: * Use a Data Feed * Use the `rewards` Data Feed * Recall: the `rewards` feed knows to call the API with the `external_id` of the recipient * From the response, insert the value of the `points` item (193) and the `status_level` item (Gold) *** ## Requirements and limits Your API needs to: * **Accept single-step authentication** with auth tokens in headers * **Respond quickly.** Under 250ms recommended (this directly affects send speed) * **Return JSON.** Other formats are not supported at this time. * If you need a different format, [share your use case](https://survey.survicate.com/954895d3e7ed1348/?p=anonymous). * **Handle your send volume.** A low rate limit on your API slows message delivery. * **Return reasonably-sized payloads.** Keep responses under 50 KB for best performance. Current limits: * **One Data Feed per template.** Fetch everything you need in a single API response. * **One API call per Data Feed per message.** * **Journeys only.** Not yet available for other sending methods. * **No chaining calls.** The payload from one Data Feed cannot be used to call another. Need multiple Data Feeds per template or support for other channels? [Share your use case](https://survey.survicate.com/954895d3e7ed1348/?p=anonymous) to help prioritize these features. *** ## Setting up your API Before creating a Data Feed, ensure your API can handle these requirements: ### Authentication Your API should accept authentication via headers: ``` Authorization: Bearer YOUR_TOKEN ``` or ``` X-API-Key: YOUR_KEY ``` #### Disallowed headers The following headers are restricted and cannot be set: * `content-length` * `referer` * `metadata-flavor` * `x-google-metadata-request` * `host` * `x-onesignal*` ### JSON request body If you need to include a body in the request, your API should accept JSON. This may mean your headers need to include `Content-Type: application/json`. ### JSON response Your API should return a JSON object. Typically this means your headers will include `Accept: application/json`. ### Personalization parameters You'll typically pass user identifiers in the URL like this: ```liquid theme={null} https://api.example.com/users/{{ external_id }}/data https://api.example.com/rewards?email={{ email | url_encode }} ``` And/or in the body: ```json theme={null} { "customer_id": "{{ external_id }}", "email": "{{ subscription.email }}" } ``` Make sure this data will exist in OneSignal (usually as Tags, but other options are available such as [custom event properties](#examples-and-advanced-use-cases)). ### Rate limits Consider your API's rate limits. Sending to 10,000 users means 10,000 API calls in rapid succession. Ensure your API can handle this volume. ### Error handling If your API returns an error or doesn't have data for a user, the message won't be sent to that recipient. Make sure your API returns data for all expected users. *** ## Getting started checklist Before implementing Data Feeds, answer these questions: * What data do I want to show in my message? Working backwards from a simple outline with the items to be populated from your API identified will help you organize your thinking. * Is this data available via a single API endpoint? * How will I authenticate API requests? * What identifier or other data item will I use to fetch personalized data? * Is that identifier already stored in OneSignal? If not, how will it be populated? * Can my API handle the volume of requests I'll generate? * What happens if my API doesn't have data for a user? *** ## Examples and advanced use cases Data Feeds can be used with Liquid syntax or in combination with other features in creative ways to produce more complex personalization. Let's say you have a Data Feed cart that returns an array of items in the user's cart, plus the cart total dollar amount: ```json theme={null} { "items": [ { "name": "Blue Running Shoes", "price": 84.00, "image_url": "https://acme.com/blue-running-shoes.png" }, { "name": "Protein Bar", "price": 5.99, "image_url": "https://acme.com/protein-bar.png" } ], "total": 89.99 } ``` If you want to show each item in the cart, plus the cart total, you can use a for loop in Liquid: ```html theme={null}
    {% for item in data_feed.cart.items %}
  • {{ item.name }}
    ${{ item.price }}
    {{ item.name }}
  • {% endfor %}

Cart total: ${{ data_feed.cart.total }}

``` This will result in: ```text theme={null} - Blue Running Shoes - $84.00 - - Protein Bar - $5.99 - Cart total: $89.99 ``` If you're using the email block editor, when inserting this sort of complex Liquid syntax, particularly if you need to include images or links, for best results use the custom HTML block element. Want to see how to trigger this abandoned cart email using custom events? Check out the **Custom events properties** section below for the complete workflow.
Continuing the previous abandoned cart example, how might we know how to fetch that particular cart in the first place? One method might be to create a Journey triggered by a `cart_abandoned` [custom event](./custom-events), where the properties includes a cart\_id. In this example that event is being sent to OneSignal via API: ```bash theme={null} curl --request POST \ --url https://api.onesignal.com/apps/{app_id}/custom_events \ --header 'Accept: application/json' \ --data '{ "events": [ { "name": "cart_abandoned", "external_id": "user_12345", "properties": { "cart_id": 98765 } } ] }' ``` Journey entry node triggered by a custom event The user `user_12345` enters the journey when this event is fired, then reaches a node sending an email. That email template is set up with the `cart` Data Feed, where the URL is set to retrieve the contents of a particular cart like so: ```liquid theme={null} https://acme.com/carts/{{ journey.event.cart_abandoned.data.cart_id }} ``` Thus, when this particular event is ingested and triggers the Journey: 1. The `cart_id` value of `98765` will be stored to the Journey 2. When the email step is reached, the `cart` Data Feed will reference that `cart_id` value and use it to call the cart API 3. The returned JSON properties will be parsed and inserted into the email as in the previous example above Want to see how to conditionally display Data Feed content based on order status? Check out the **Conditional display: order status** section below to learn more. Let's say you want to include the status of a customer's order, but only include a tracking number link if the order has been shipped. You can use an `if` statement to do so: ```liquid theme={null} Your order is {{data_feed.order.status}}! {% if data_feed.order.tracking_number != empty %} Track it here: {{data_feed.order.tracking_url}} {% endif %} ``` Here, the tracking link will only be displayed if the `tracking_number` exists. Data Feeds can be used to automatically insert up-to-date information into your messages without necessarily needing to be personalized per recipient. For example, perhaps you insert a banner image at the top of your emails and change it monthly to keep up with holidays and other monthly events. Rather than remembering to upload a new image to OneSignal and changing all your templates each month, you might set up a Data Feed that fetches the current banner image URL from your CMS or other asset-management location. You would set up a `banner` Data Feed that points to an endpoint **without** any variables in the URL like so: ```liquid theme={null} https://acme.com/assets/email-banner ``` Which returns a response with the current banner URL: ```json theme={null} { "banner_url": "https://acme.com/assets/email-banner/2025july.png" } ``` You'd set your email template to use `{{ data_feed.banner.banner_url }}` as the image source URL, automating this process going forward. This example covers how to send personalized, single-use coupon codes in emails using a Data Feed that fetches a unique value from your API for each recipient. ### Goal Send an email such as: > Hi George, > > Complete your booking in the next 2 hours and save 10% with your personal code: XYZ123ABC Each user receives a unique coupon code, generated live via an external API call when the email is sent, valid for the given time window, and scoped to that user. ### Prerequisites * [Email channel configured](./email-setup) in OneSignal (your app has email capability enabled) * An external API that accepts a user identifier (for example, [`external_id`](./users#external-id)) and campaign identifier, and returns JSON with the coupon code, discount, and expiration * A unique identifier (such as `external_id`) for each user that your API can use to generate coupons * A [segment](./segmentation) or trigger (for example, "abandoned search in last 24h") used to send the email through a OneSignal Journey ### Step 1: Create the Data Feed In your OneSignal dashboard, select **Data > Data Feeds** from the navigation bar, then click **New Data Feed**. Configure the following fields: * **Data Feed Name**: `coupon_code_generator` * **Alias**: `coupon` * **Method**: `GET` * **URL**: `https://api.example.com/generate-coupon?userId={{ external_id }}&campaign=AbandonedBooking10` * **Headers**: ```json theme={null} { "Authorization": "Bearer YOUR_API_TOKEN" } ``` When the API returns JSON like this: ```json theme={null} { "code": "AB10-5F3K-HT9L", "discount_percent": "10%", "expires_in_hours": 2 } ``` You can reference its values in your email template as: * `{{ data_feed.coupon.code }}` * `{{ data_feed.coupon.discount_percent }}` * `{{ data_feed.coupon.expires_in_hours }}` Click **Send Test** to verify your configuration, then click **Activate** to make the Data Feed available for use in templates. ### Step 2: Create the email template 1. In OneSignal, go to **Messages > Email > New Template** 2. Use the Data Feed alias and fields within your message body. For example: ```html theme={null}

Hi {{ first_name }},

Complete your booking in the next {{ data_feed.coupon.expires_in_hours }} hours and save {{ data_feed.coupon.discount_percent }} with your personal code: {{ data_feed.coupon.code }}

Use Code Now →

``` * Use Liquid syntax in the form `{{ data_feed.. }}` * Make sure the Data Feed is attached to the template * If using the drag-and-drop editor with custom Liquid logic, use an HTML block ### Step 3: Attach the Data Feed and trigger the email 1. In the template composer, under **Personalization > Data Feeds**, toggle the feed on and select `coupon_code_generator` 2. This ensures OneSignal makes the API call at send time for each recipient, populates the data, and injects it into the email 3. Set up a Journey to automate the message: * **Entry condition**: Segment such as "abandoned search in last 24 hours" * **[Wait until](./custom-events#wait-until-event)**: Create a `booking_completed` custom event. Configure the wait to exit the Journey if this event fires, or continue after 1 hour. If they complete a booking, they exit without receiving the email; otherwise, they continue to receive the coupon. * **Send email**: Use the personalized coupon email template * Ensure the Journey uses the email channel and that recipients are subscribed to email ### Step 4: Manage coupon redemption and tracking Your backend should: 1. Record each generated code along with `userId`, `code`, `campaign`, `expiration_time`, and a `redeemed` flag 2. Validate and mark codes as redeemed upon checkout 3. Log the redemption data (user, campaign, and time) for ROI analysis ### Complete workflow example | Step | Event | Action | | ---- | ---------------------------------------- | -------------------------------------------------------------------------------------------- | | 1 | User triggers "abandoned search" segment | User enters Journey via segment | | 2 | Journey triggers email send | OneSignal attaches Data Feed, calls your API with the user's `external_id` and campaign name | | 3 | API returns JSON | OneSignal populates data feed coupon fields and sends email | | 4 | User receives email | Example: "Hi George! Save 10% with your personal code: AB10-5F3K-HT9L" | | 5 | User redeems code | Backend validates and logs redemption | ### Testing * Test with a user who has a known `external_id` * Call your API manually to confirm correct JSON responses * Use **Preview** in the OneSignal template editor to verify that data feed coupon fields populate * Check API logs for latency and errors * If a Data Feed call fails, OneSignal will skip sending the message to that recipient
If you want to send dynamic content in your emails but don't have a backend database or API, Google Sheets combined with Google Apps Script is a simple alternative. This guide walks you through setting up a Google Sheet as a live JSON endpoint that OneSignal can pull from at send time. This approach works well for any manually curated content that updates on a regular cadence — weekly newsletters, product roundups, editorial digests, event listings, and more. ### How it works 1. You maintain a Google Sheet with your content (one row per item) 2. A small Apps Script converts the sheet into a JSON endpoint 3. OneSignal fetches that JSON at send time via a Data Feed 4. Liquid tags in your email template populate with the sheet data ### Step 1: Set up your Google Sheet Create a new Google Sheet with your content. Use the first row as column headers — these become your Liquid variable names. **Example structure:** | title | image | price | link | category | | --------- | ------------ | ------- | ------------ | ---------- | | Product A | https\://... | \$24.00 | https\://... | Category 1 | | Product B | https\://... | \$68.00 | https\://... | Category 2 | Keep column headers lowercase with no spaces — this makes them easier to reference in Liquid tags. ### Step 2: Add the Apps Script In your Google Sheet, go to **Extensions > Apps Script** and paste the following code: ```javascript theme={null} function doGet() { const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet(); const data = sheet.getDataRange().getValues(); const headers = data[0]; const rows = data.slice(1); const result = {}; rows.forEach((row, index) => { const slot = index + 1; headers.forEach((header, i) => { result[`item_${slot}_${header}`] = row[i]; }); }); return ContentService.createTextOutput(JSON.stringify(result)) .setMimeType(ContentService.MimeType.JSON); } ``` This script reads your sheet and transforms it into a flat JSON object. Each row becomes a numbered item slot: ```json theme={null} { "item_1_title": "Product A", "item_1_price": "$24.00", "item_2_title": "Product B", "item_2_price": "$68.00" } ``` ### Step 3: Deploy the script as a web app Click **Deploy > New deployment**. Select type: **Web app**. Set **Execute as**: Me. Set **Who has access**: Anyone. Click **Deploy** and copy the web app URL. This is the URL you'll use in OneSignal. Any time you update the script, you must create a **new version** under Manage Deployments for changes to take effect on the live URL. Saving the script alone is not enough. ### Step 4: Create a Data Feed in OneSignal In your OneSignal dashboard, go to **Data > Data Feeds** and click **New Data Feed**. Set a descriptive **Name** (e.g. `Weekly Content`) and an **Alias** you'll reference in Liquid tags (e.g. `weekly_content`). Set the **URL** to your Apps Script web app URL. Save and activate the Data Feed so it's available for use in templates. ### Step 5: Use Liquid tags in your email template Reference your sheet data in any email template using the following syntax: ```liquid theme={null} {{ data_feed.weekly_content.item_1_title }} {{ data_feed.weekly_content.item_1_price }} {{ data_feed.weekly_content.item_1_image }} {{ data_feed.weekly_content.item_2_title }} ``` The pattern is always: ```liquid theme={null} {{ data_feed.{alias}.item_{row number}_{column header} }} ``` ### Step 6: Attach the Data Feed to your message When creating a new email message or template, attach the Data Feed under **Personalization > Data Feeds** before sending. OneSignal fetches the latest data from your sheet at send time. ### Updating content To update your email content for the next send, open your Google Sheet and update the rows. No script changes or redeployment needed — the URL always serves the current sheet data. ### Tips * **Add or remove rows freely.** Just make sure your Liquid tags in the template match the number of rows in your sheet. * **Column headers are your variable names.** Renaming a column means updating the Liquid tags in your template to match. * **Test your endpoint first.** Paste your Apps Script URL directly in a browser to verify the JSON output before connecting it to OneSignal. * **Format numbers as text.** If you include currency or other formatted values, set those cells to Plain Text in Google Sheets to prevent formatting from being stripped. * **The script is reusable.** The same Apps Script works for any sheet structure — no modifications needed.
*** ## FAQ ### My Data Feed values are not appearing in the message. What should I check? Verify these in order: 1. The Data Feed is **attached** to the template (under **Personalization > Data Feeds**). 2. Your Liquid syntax matches the JSON response structure exactly — `{{ data_feed.. }}`. 3. The API endpoint returns valid JSON when called manually with the same identifiers. 4. The recipient has the required identifier (e.g., `external_id`) stored in OneSignal. ### Why are messages sending slowly? Data Feed API calls run at send time for every recipient. If your API responds slowly or cannot handle concurrent requests, message delivery slows proportionally. Aim for under 250 ms response time and ensure your infrastructure can handle your send volume. ### Why are some recipients not getting messages? If the Data Feed API call fails for a recipient — due to a 404, timeout, or missing data — OneSignal skips that recipient entirely. Check the error log in your Data Feed configuration and your own API logs for failures. Verify those users have the required identifiers in OneSignal. ### Can I use more than one Data Feed in a template? Not currently. Each template supports one Data Feed. Fetch all the data you need in a single API response. If you need multiple feeds, [share your use case](https://survey.survicate.com/954895d3e7ed1348/?p=anonymous). ### Are Data Feeds available for push notifications or SMS? No. Data Feeds are currently available only for email messages sent through Journeys. Support for additional channels is planned — [share your use case](https://survey.survicate.com/954895d3e7ed1348/?p=anonymous) to help prioritize. ### What happens if my API is down when the message sends? OneSignal skips any recipient whose Data Feed call fails. The message is not sent to that recipient, and no fallback value is inserted. Monitor your API uptime and error rates during scheduled sends. ## Related pages Full reference for Liquid templating in OneSignal messages. Build automated messaging workflows that trigger Data Feed emails. Trigger Journeys and pass event properties for Data Feed URLs. Overview of all personalization methods — tags, Liquid, dynamic content, and Data Feeds. # Data & background notifications Source: https://documentation.onesignal.com/docs/en/data-notifications Send silent push notifications to sync data or trigger background tasks on iOS and Android without displaying a visible message. ## What are silent notifications? Silent notifications let you wake your app and perform background tasks—such as syncing or refreshing data—without showing a visible message or playing a sound. On iOS, these are called **Background Notifications**, and on Android, they're known as **Data Notifications**. Together, they're often referred to as silent pushes and behave differently from normal, visible notifications. ### Limitations * **Apps cannot receive silent pushes if**: * **iOS**: The app has been closed by the user, such as swiping it away from the app switcher. (See [Apple support](https://support.apple.com/en-us/HT201330)). * **Android**: The app has been force-quit via device settings or automatically by some manufacturers when swiped away. ([Android force-stop behavior](./notifications-show-successful-but-are-not-being-shown#android-app-is-force-stopped)). * **Delivery is not guaranteed**: * Both Apple and Google treat silent notifications as *best-effort*. iOS may delay or drop delivery under Low Power Mode, Background App Refresh off, or if the app was closed by the user. Android may throttle or batch delivery under Doze or OEM power-saving rules. * Because of this, **silent notifications should never be used for critical updates**. * **Subscribed users only**: The OneSignal SDK only sends data notifications to subscribed [Subscriptions](./subscriptions). To reach unsubscribed users, see [this iOS SDK workaround](https://github.com/OneSignal/OneSignal-iOS-SDK/issues/302). * **Limited support for cross-platform SDKs**: * Silent notifications must be handled in native code (Java/Kotlin for Android, Swift/Obj-C for iOS). * iOS requires implementation of [`application:didReceiveRemoteNotification:fetchCompletionHandler:`](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623013-application). * Android requires implementation of a [Notification Service Extension](./service-extensions). *** ## Sending silent notifications from OneSignal Follow these steps to send a silent notification from OneSignal: Remove any visible text or titles from the message. This includes: * **API**: `contents`, `headings`, `subtitle` in your [Create notification](/reference/create-message) API request. * **Dashboard**: Message, Title, Subtitle * **API**: Set `content_available` to `true`. * **Dashboard**: Check **Content available** under "Send to Apple iOS". Despite the label, this setting applies to all platforms and signals to OneSignal that no visible message is included. * **API**: Use the `data` parameter. * **Dashboard**: Use the **Additional Data** fields. ### Example API payload ```bash theme={null} curl -X POST https://api.onesignal.com/notifications \ -H "Authorization: Key YOUR_APP_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "app_id": "YOUR_APP_ID", "include_aliases": { "external_id": ["user-123"] }, "target_channel": "push", "content_available": true, "data": { "action": "sync_data", "version": "2" } }' ``` *** ## Platform-specific setup ### iOS background notification setup To handle background notifications, your iOS app must have the **Background Modes > Remote Notifications** capability enabled in Xcode. This is typically added if you followed the [Mobile SDK Setup](./mobile-sdk-setup). To process the notification, implement the `AppDelegate` method [`application(_:didReceiveRemoteNotification:fetchCompletionHandler:)`](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623013-application). Apple documentation: * [Pushing Background Updates to Your App](https://developer.apple.com/documentation/usernotifications/setting_up_a_remote_notification_server/pushing_background_updates_to_your_app?language=objc) * [Generating a Remote Notification](https://developer.apple.com/documentation/usernotifications/generating-a-remote-notification) If the user has closed the app (swiped it away from the app switcher), iOS will not deliver the notification. In such cases, include a visible `contents` message and process data in the [`UNNotificationServiceExtension.didReceive`](./service-extensions#ios-notification-service-extension). *** ### Android data notification setup Handle data notifications on Android using the [Notification Service Extension](./service-extensions). This lets you: * Process notifications as long as the app hasn't been force closed * Customize how notifications are displayed or suppressed Android documentation: * [FCM data messages](https://firebase.google.com/docs/cloud-messaging/concept-options#data_messages) * [Optimize for Doze and App Standby](https://developer.android.com/training/monitoring-device-state/doze-standby) If the app has been force-stopped (via device settings or by some OEM battery optimization), Android will not deliver the notification. See [Android force-stop behavior](./notifications-show-successful-but-are-not-being-shown#android-app-is-force-stopped). *** ## Sending VoIP notifications VoIP notifications are supported but require additional configuration outside the standard OneSignal SDKs. OneSignal does not register VoIP tokens automatically. Configure VoIP push notifications for real-time calling on iOS. *** ## FAQ ### Can silent notifications be used to detect uninstalls or unsubscribes? Not reliably. Silent notifications are best-effort and not guaranteed to be delivered, as explained in the [Limitations](#limitations) section. Instead: * Send visible notifications (with content) to all your users at least once a month. * Optionally send silent notifications as a supplemental check. For more details on handling subscription status changes, see the [Subscriptions](./subscriptions) guide. ### Do confirmed deliveries work with silent notifications? No. Confirmed deliveries require the SDK to execute a callback when the notification is displayed. Silent notifications are not displayed, so the SDK receipt callback is never triggered. ### Can I use silent notifications to measure how many users are reachable? No, not reliably. Silent notifications aren't guaranteed delivery. 1. **Delivery is not guaranteed** — both Apple and Google treat silent notifications as best-effort and may delay or drop them. For example, iOS will drop them if the app is closed (force quit) and Android and iOS will throttle them under power-saving rules. 2. **Delivery is not confirmed** — confirmed deliveries do not work with silent notifications, so you have no way to know which users actually received the push. To measure reachable users, send a visible notification and track delivery metrics in [Analytics](./analytics-overview). *** Handle notification processing in native code on iOS and Android. Understand Subscription types and how they connect to Users. Send notifications programmatically, including silent pushes. # Deep linking Source: https://documentation.onesignal.com/docs/en/deep-linking Send users from OneSignal push, in-app, email, and SMS messages to a specific screen in your Android or iOS app using Universal Links, App Links, or custom URI schemes. ## Overview A deep link routes the recipient of a OneSignal message to a specific screen in your app instead of a web page. OneSignal delivers the link to the device; the operating system and your app are responsible for resolving it. This guide covers how to use deep links **inside OneSignal messages** — it does not cover how to configure your app to receive them. For platform setup, see Apple's [Supporting universal links in your app](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app) and Google's [About deep links](https://developer.android.com/training/app-links). *** ## Prerequisites Before you can deep-link from a OneSignal message, your app must already be set up to receive the link type you plan to use: * The [OneSignal Mobile SDK](./mobile-sdk-setup) is installed and initialized. * Your app developers have configured a deep link handler for **each platform you target**: * **iOS** — [Universal Links](https://developer.apple.com/documentation/xcode/supporting-universal-links-in-your-app) (Associated Domains entitlement + [apple-app-site-association (AASA) file](https://developer.apple.com/documentation/xcode/supporting-associated-domains) hosted on your domain) and/or a [custom URL scheme](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app) registered in `CFBundleURLTypes`. * **Android** — [App Links](https://developer.android.com/training/app-links) (intent filters with `autoVerify="true"` + [Digital Asset Links file (`assetlinks.json`)](https://developer.android.com/training/app-links/verify-applinks) hosted on your domain) and/or a custom URI scheme registered in your `AndroidManifest.xml`. * For email and SMS/RCS, only Universal Links / App Links work — custom URI schemes are not clickable from those clients. Once a link is working on the device for a regular tap (paste into Notes on iOS or run `adb shell am start -a android.intent.action.VIEW -d ""` on Android), OneSignal can deliver it through any supported channel. *** ## Choose a link type | Type | URL format | Supported in OneSignal | Fallback when app is not installed | | ---------------------------- | ----------------------------- | ---------------------------- | ---------------------------------- | | **Universal Links** (iOS 9+) | `https://yourdomain.com/path` | Push, in-app, email, SMS/RCS | Opens the URL in Safari | | **App Links** (Android 6+) | `https://yourdomain.com/path` | Push, in-app, email, SMS/RCS | Opens the URL in the browser | | **Custom URI schemes** | `myapp://path` | Push, in-app only | Fails silently or shows an error | **Recommendation:** Use Universal Links / App Links (`https://`) for anything sent via email or SMS — custom schemes do not work in those clients. For push and in-app messages, either works. On Android 12+, an `https://` link that is **not** a verified App Link opens in the user's default browser, not your app. If deep links from push or email are opening the browser on Android, the App Link verification is the first thing to check. *** ## Send a deep link in a push notification OneSignal supports two ways to attach a deep link to a push notification. Pick one based on how much control you want over navigation. ### Launch URL (`url` / `app_url`) Set the Launch URL in the dashboard, or pass `url` (all platforms) or `app_url` (mobile only) in the API. ```json theme={null} { "app_id": "YOUR_APP_ID", "contents": { "en": "Your order shipped" }, "url": "https://yourdomain.com/orders/12345" } ``` | Platform | Behavior when the notification is tapped | | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Android | Android resolves the URL. Verified App Link → opens directly in your app. Non-verified `https://` → opens the browser. Custom scheme → opens the registered app. | | iOS | OneSignal's iOS SDK calls `openURL`, which opens Safari first, then bounces to your app through the Universal Link handshake. See [iOS Launch URL behavior](#ios-launch-url-behavior). | ### Additional Data (`data`) — recommended for mobile Put the destination in Additional Data instead and do the navigation yourself in the [push click listener](#handle-deep-links-in-your-app). This avoids the iOS browser bounce entirely and lets you pass structured context. ```json theme={null} { "app_id": "YOUR_APP_ID", "contents": { "en": "Your order shipped" }, "data": { "deep_link": "https://yourdomain.com/orders/12345", "order_id": "12345" } } ``` For mobile push, prefer Additional Data + a click listener. You get full control over navigation, no browser flash on iOS, and you can pass extra fields (like `order_id`) that aren't part of the URL. ### iOS Launch URL behavior OneSignal's iOS SDK uses `openURL` to handle the `url` / `app_url` property. Even with Universal Links correctly configured, this causes the link to open in the browser first and then redirect back into the app — a noticeable "flash" for users. You have two ways to avoid it: 1. **Use `data` instead of `url`** and route from your [push click listener](#handle-deep-links-in-your-app). Recommended. 2. **Suppress Launch URLs globally.** Add a Boolean `OneSignal_suppress_launch_urls` with value `YES` to your app's `Info.plist`. OneSignal will stop calling `openURL`, and you must handle every deep link in the click listener. *** ## Send a deep link in an in-app message In-app messages use click actions instead of a URL property. For the full list of click action types, see [In-app click actions](./iam-click-actions). ### Drag-and-drop editor 1. Add a button, image, or background click target. 2. Open **Add click action**. 3. Choose one of: * **URL** — opens the link in the device's browser (or, if Universal Links / App Links are configured for that domain, opens your app). * **Custom action ID** — passes the value to your [in-app click listener](#handle-deep-links-in-your-app) without opening anything. Use this when you want to navigate inside the app you control. 4. Enter your deep link (e.g. `https://yourdomain.com/promo`) or action identifier (e.g. `open_promo`). ### HTML editor In sandboxed HTML in-app messages, use the [In-App Message JS API](./in-app-message-api): * `OneSignalIamApi.openUrl(e)` — opens the URL in the device browser using default URL handling. * `OneSignalIamApi.addClickName(e)` — sends a custom click name to your [in-app click listener](#handle-deep-links-in-your-app) without opening a browser. ```html theme={null} ``` *** ## Send a deep link in an email By default, OneSignal rewrites every link in an email for click tracking. The rewritten URL uses OneSignal's tracking domain, which does **not** match your app's Associated Domain or Digital Asset Links host — so iOS and Android treat the link as a regular web URL and open the browser instead of your app. To preserve deep linking in email, disable click tracking for the deep link using one of these options: | Scope | How | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Whole email (dashboard)** | Uncheck **Track link clicks** in the email editor. | | **Whole email (API)** | Set `disable_email_click_tracking: true` in the Create message request. | | **Single link (HTML)** | Add `disable-tracking=true` to the anchor tag: ``. Tracking stays enabled on every other link. | OneSignal email editor with Track link clicks checkbox unchecked Disabling tracking removes click metrics from [Email Message Reports](./email-message-reports). Use the per-link `disable-tracking=true` attribute to keep analytics on non–deep-link URLs. ### Expected behavior | Scenario | Result | | ----------------------------------------------------------------- | --------------------------------------------------------------------- | | iOS + Safari + Universal Link + tracking disabled | Opens app directly | | iOS + Safari + Universal Link + tracking enabled | Opens Safari, link doesn't match AASA, stays in browser | | iOS + non-Safari mail client + Universal Link + app not installed | Falls back to web; can redirect to App Store | | Android + App Link + tracking disabled | Opens app directly | | Android + App Link + tracking enabled | Opens browser, link doesn't match `assetlinks.json`, stays in browser | *** ## Send a deep link in SMS or RCS SMS and RCS messages support only `https://` links — custom schemes like `myapp://` won't open from the Messages app. Insert the link inline in the message body. If you use OneSignal's trackable link feature (`{{ "https://..." | track_link }}` or the **Insert Trackable Link** button), the URL is replaced with a `1sgnl.co` short link, which breaks Universal Link / App Link domain matching. Do not wrap deep-link URLs in `track_link` — send them raw so the OS can match them to your app. See [URLs, links, & deep links → SMS/RCS trackable links](./links#sms%2Frcs-trackable-links) for the tracking-link format. *** ## Handle deep links in your app The most reliable way to route a OneSignal deep link is to intercept it with a click listener and do the navigation yourself. This works regardless of platform quirks (iOS `openURL`, Android 12+ verification, email tracking rewrites) as long as your listener receives the event. ### Push notification click listener Register the listener as early as possible — in `Application.onCreate()` on Android or `application(_:didFinishLaunchingWithOptions:)` on iOS — so it is active before the user taps a cold-start notification. ```kotlin Kotlin theme={null} OneSignal.Notifications.addClickListener { event -> val url = event.notification.launchURL val data = event.notification.additionalData // Route to the correct screen based on url or data } ``` ```swift Swift theme={null} class AppDelegate: UIResponder, UIApplicationDelegate, OSNotificationClickListener { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { OneSignal.Notifications.addClickListener(self) return true } func onClick(event: OSNotificationClickEvent) { let url = event.notification.launchURL let data = event.notification.additionalData // Route to the correct screen based on url or data } } ``` ```javascript React Native theme={null} OneSignal.Notifications.addEventListener('click', (event) => { const url = event.notification.launchURL; const data = event.notification.additionalData; // Route to the correct screen based on url or data }); ``` ```dart Flutter theme={null} OneSignal.Notifications.addClickListener((event) { final url = event.notification.launchUrl; final data = event.notification.additionalData; // Route to the correct screen based on url or data }); ``` ```csharp Unity (C#) theme={null} OneSignal.Notifications.Clicked += (sender, e) => { var url = e.Notification.LaunchURL; var data = e.Notification.AdditionalData; // Route to the correct screen based on url or data }; ``` For the full API including Java, Objective-C, and Cordova/Ionic, see [`addClickListener()` Push](./mobile-sdk-reference#addclicklistener-push). ### In-app message click listener Fires when a user taps a button configured with a **Custom action ID** (or when `OneSignalIamApi.addClickName` is called in HTML). ```kotlin Kotlin theme={null} OneSignal.InAppMessages.addClickListener { event -> val actionId = event.result.actionId // Route based on the action ID (your deep link or internal route name) } ``` ```swift Swift theme={null} func onClick(event: OSInAppMessageClickEvent) { let actionId = event.result.actionId // Route based on the action ID (your deep link or internal route name) } ``` ```javascript React Native theme={null} OneSignal.InAppMessages.addEventListener('click', (event) => { const actionId = event.result.actionId; // Route based on the action ID (your deep link or internal route name) }); ``` ```dart Flutter theme={null} OneSignal.InAppMessages.addClickListener((event) { final actionId = event.result.actionId; // Route based on the action ID (your deep link or internal route name) }); ``` For the full API, see [`addClickListener()` In-App](./mobile-sdk-reference#addclicklistener-in-app). *** ## Personalize deep links per user Every URL field in OneSignal — Launch URL, Additional Data values, in-app URL actions, email body links — accepts [Liquid syntax](./using-liquid-syntax). Use it to inject the user's `external_id`, tags, or `custom_data` into the path or query string. ```text theme={null} https://yourdomain.com/profile/{{subscription.external_id}} https://yourdomain.com/orders/{{message.custom_data.order_id}} https://yourdomain.com/topic/{{ interest | default: 'home' }} ``` See [Dynamic URLs](./links#dynamic-urls) for the full list of data sources and fallback filters. *** ## Test deep links from OneSignal 1. Send a test message from the OneSignal dashboard with your deep link as the Launch URL, Additional Data value, or in-app click action. 2. Tap the notification or in-app button on a real device (Universal Links do not activate in the iOS Simulator from all sources). 3. Confirm the app opens directly to the right screen and your click listener logs the expected URL or action ID. For platform-level verification (before involving OneSignal), Apple provides the [Associated Domains validation tool](https://search.developer.apple.com/appsearch-validation-tool/) and Android offers `adb shell pm verify-app-links --re-verify `. On iOS, Universal Links do not activate from the Safari address bar. Test from Notes, Mail, Messages, or a OneSignal push/in-app message. *** ## Troubleshooting ### iOS push opens Safari for a second before my app OneSignal's SDK uses `openURL` for the Launch URL. Fix with one of the [two approaches above](#ios-launch-url-behavior): send the link in Additional Data and route from the click listener, or set `OneSignal_suppress_launch_urls=YES` in `Info.plist`. ### Android push opens the browser instead of the app The `https://` link is not being recognized as a verified App Link. Check that your intent filter uses `android:autoVerify="true"`, that `assetlinks.json` is reachable at `https://yourdomain.com/.well-known/assetlinks.json` (HTTP 200, `application/json`, no redirects), and that its SHA-256 fingerprint matches the certificate you actually shipped (Play App Signing uses a different fingerprint than your local keystore). See Google's [Troubleshoot App Links](https://developer.android.com/training/app-links/troubleshoot). ### Deep link works in push but not in email Email click tracking rewrites the URL and breaks domain matching. [Disable tracking](#send-a-deep-link-in-an-email) for that link or that email. ### Deep link is received but the app doesn't navigate * The click listener was registered **after** the click event fired. Register in `Application.onCreate()` (Android) or `application(_:didFinishLaunchingWithOptions:)` (iOS), before any other async work. * The `launchURL` or `actionId` doesn't match what your router expects. Log the raw value. * On iOS, the Launch URL (`url`) is being used without suppressing it — the browser may consume the link before your listener fires. Switch to Additional Data. ### I updated my AASA file and iOS still doesn't open the app Starting in iOS 14 / macOS 11, Apple's CDN fetches AASA files, not the device. The CDN requests a refresh within 24 hours, and installed devices re-check roughly once per week. Reinstall the app to force an immediate fetch during testing. *** ## FAQ ### Should I use Launch URL or Additional Data? Additional Data is recommended for mobile deep links. It bypasses iOS's `openURL` browser flash and gives your click listener structured data instead of a string to parse. Launch URL is simpler and fine for web push or when you accept the iOS behavior. ### Do deep links work with OneSignal Journeys? Yes. Set the Launch URL or Additional Data on any push or in-app step the same way you would for a one-off message. Email steps follow the same click-tracking rules — disable tracking on the deep link. ### Can I deep link into another app (not mine)? Yes, for push and in-app only. Set a custom URL scheme that the other app registers (e.g. `whatsapp://send?phone=15551234567`). Custom schemes do not work in email or SMS clients. ### What happens if the user doesn't have the app installed? With Universal Links (iOS) or App Links (Android), the `https://` URL loads as a normal web page — you can host a landing page that redirects to the App Store / Play Store. With a custom URI scheme (`myapp://`), the tap fails silently or shows an error; there is no fallback. ### Can I use a trackable SMS link as a deep link? No. OneSignal's SMS trackable links rewrite the URL to `1sgnl.co`, which isn't associated with your app. Send the original `https://` URL so the OS can match it to your Associated Domain or Digital Asset Links entry. ### Do I need the OneSignal SDK to handle a deep link? No, but it's recommended. Universal Links / App Links are handled by the OS regardless of which SDK sent the message. Using OneSignal's click listener gives you a consistent entry point across channels and avoids platform-specific quirks like the iOS `openURL` flash. *** Launch URLs, UTM parameters, dynamic URLs, and click tracking across every channel. Full API reference for push and in-app click listeners and notification events. Configure URL, custom action ID, and other click actions for in-app buttons. Platform-specific push notification setup for Android and iOS. # Design emails with drag-and-drop Source: https://documentation.onesignal.com/docs/en/design-emails-with-drag-and-drop Build responsive emails visually with OneSignal’s Drag and Drop Email Builder — settings, rows, content blocks, personalization, and compliance links. ## Overview The OneSignal Drag and Drop Email Builder lets you visually design responsive emails exactly as they appear in recipients’ inboxes — without writing full HTML. You build emails using three core components: * **Settings** – global styles and layout applied across the entire email * **Rows** – horizontal layout containers that control structure and responsiveness * **Content** – individual blocks such as text, images, buttons, and HTML The sections below cover each component and the recommended order for building an email. Use the Drag and Drop Email Builder if you want to: * Design emails visually without managing full HTML (HTML blocks are available) * Reuse rows or templates across campaigns * Allow non-technical teammates to safely edit content If you need full HTML control, custom fonts everywhere, or advanced dark mode logic, use the [HTML Editor](./design-emails-with-html) instead. ### Recommended build flow Follow this order for the best results and fewer rendering issues: 1. Configure global styles in **Settings** 2. Add layout structure using **Rows** 3. Insert **Content** blocks 4. Add personalization and links 5. Add an unsubscribe link (for marketing emails) 6. Save as a template or send When finished, your email should: * Be no wider than 600px * Render cleanly on mobile and desktop * Include required compliance links **Common mistakes that cause rendering or deliverability issues:** * **Missing unsubscribe link** — required for marketing emails. Without one, inbox providers are more likely to flag your email as spam. * **Email wider than 600px** — causes horizontal scrolling on mobile and breaks layouts in many clients. * **Background images in rows** — Outlook and several mobile clients do not render them. Use Image blocks instead. * **No fallback fonts** — custom web fonts only load in a few clients. Always declare system font fallbacks. * **Oversized images** — emails over 1 MB load slowly and may be blocked. Individual images should be under 200 KB. *** ## Settings Settings define the foundational layout and styles for your email. These values cascade down to rows and content blocks unless explicitly overridden. Global settings panel in the Drag and Drop Editor | Design Setting | Description | | ----------------------------- | ---------------------------------------------------------------------- | | Content area width | Width of the email in pixels. **Recommended:** `600px`. | | Content area alignment | Align content left or center. | | Background color | Color behind the content area. | | Content area background color | Color inside the content area. | | Default font | Applied to all text unless overridden. Custom fonts require HTML. | | Link color | Default color for all links. | | Language | Sets the HTML `lang` attribute for accessibility. Defaults to English. | **Recommended default:** Configure as much styling as possible in Settings to ensure consistency and reduce per-block overrides. *** ## Rows Rows define the horizontal layout of your email. Each row can contain one or more columns, and each column can contain content blocks. Drag and drop rows into the editor to build your structure. Adding rows to structure an email Use rows to control layout and spacing. Avoid relying on individual content blocks for major layout decisions. ### Saved rows Saved rows let you reuse headers, footers, or repeated sections across emails and templates. Click the **save icon** on a row to save it. Saving a row for reuse Access saved rows from **Rows > Saved rows**. Selecting a saved row Saved row categories: * **Empty** – blank row templates * **My Saved Rows** – rows created by you or your team * **Sample Rows** – OneSignal examples ### Row properties Click the outer edge of a row to edit row-level settings. If you see **Content** instead of **Row** when hovering, you’ve selected a content block. Click the outer container edge until the label reads **Row**. | Row Property | Description | | ---------------- | ------------------------------------------------------------------------------------------------- | | Backgrounds | Color or image behind the row. **Recommended:** set background color in Settings for consistency. | | Borders | Border color, width, and style. | | Layout | Show or hide rows on mobile or desktop. | | Columns | Add, remove, or resize columns and adjust column padding. | | Delete/Duplicate | Select a row and use the **delete** or **duplicate** icons to remove or copy it. | Avoid background images in rows. Email client support is inconsistent. If the row contains only an image, use an **Image block** instead. *** ## Content Content blocks are the elements recipients see — text, images, buttons, dividers, and HTML. Drag a content block into a row column. It automatically adapts to the column width. Available content blocks **Decision rules for content blocks:** * Use **Text blocks** for most copy. * Use **Image blocks** for visuals or custom typography. * Use **HTML blocks** only when you need advanced styling or behavior. ### Custom fonts The Drag and Drop Editor supports system fonts by default. To use custom fonts, you must use an HTML block. ```html HTML block theme={null}

Welcome!

Thanks for subscribing.

``` Many email clients (including Gmail and Outlook for Windows) do not load web fonts. Use system fonts for body text and reserve custom fonts for headlines only. Always include fallback fonts, or use images for guaranteed typography. ### Images & video Upload images directly to the OneSignal dashboard so your team can reuse them. The editor supports cropping, filtering, and other effects. You can also reference externally hosted images and videos by URL — make sure each URL is publicly accessible. #### Recommended image sizes for email * Header/Banner images: `3:1` or `4:1` (e.g., `600×200` or `600×150`) * Hero images: `16:9` or `2:1` (e.g., `600×338` or `600×300`) * Square images: `1:1` (e.g., `300×300`) — good for product grids * Thumbnails: `1:1` or `4:3` * Max width: `600–700px` is standard (most email clients) * Design at `2x` for retina displays (e.g., `1200px` wide, displayed at `600px`) * Keep individual images under `100–200 KB` * Total email size (including images) under `1 MB` * Smaller images mean faster load times and better deliverability * **JPG** — best for photos * **PNG** — best for graphics, logos, and images with transparency * **GIF** — for simple animations (keep file size small) * **WebP** — not widely supported in email yet; avoid * Always include alt text for accessibility and for when images do not load * Use inline CSS for styling (many clients strip ` ``` Replace any content blocks that do not render correctly in dark mode with HTML blocks that use the CSS classes defined above. ```html email-header theme={null} ``` ```html email-body theme={null} ``` ```html email-button theme={null} ``` Return to preview mode to verify the dark mode styles are applied correctly. *** ## Save your work Save your email design as a [**template**](./templates) for future use. Save email as a template *** ## FAQ ### How do I add a custom unsubscribe link? Replace the default `[unsubscribe_url]` with your own URL. You are responsible for updating the email Subscription status in OneSignal when a recipient unsubscribes. See [Create a custom unsubscribe page](./create-custom-unsubscribe-page) for setup details. ### Why does my email look different in Outlook? Outlook on Windows uses the Microsoft Word rendering engine, which does not support modern CSS. Common issues include ignored `max-width`, collapsed background images, and missing web fonts. Test in Outlook specifically and use inline CSS with explicit `width` and `height` attributes on images. ### What is the maximum email size? Keep total email size (HTML + images) under 1 MB. Emails over 102 KB of HTML are clipped by Gmail, which hides content below the fold behind a "View entire message" link. Optimize images and remove unused code to stay under this threshold. ### Can I reuse parts of an email across multiple campaigns? Yes. Save any row as a **Saved Row** by clicking the save icon on the row. Saved rows appear under **Rows > My Saved Rows** and can be dragged into any email. For full email reuse, save the entire design as a [template](./templates). ### How do I preview my email before sending? Use the **Preview** button in the top toolbar to see how your email renders on desktop and mobile. To send a live test, click **Send Test Email** and enter a recipient address. Test emails are delivered to the inbox so you can verify rendering, links, and personalization in actual email clients. ### Do emojis work in emails? Yes, but they render differently across email clients. Outlook on Windows is the most inconsistent — emojis may appear as black-and-white outlines or boxes. If emojis are part of your subject line or CTA text, test across clients before sending. ### How do I import an existing HTML email template? You can bring existing HTML templates into OneSignal three ways: 1. Forward the email to OneSignal using [Email Template forwarding](./email-template-forwarding) 2. Use the [Create Template API](/reference/create-template) to upload HTML programmatically 3. Copy-paste your HTML directly into the [HTML Editor](./design-emails-with-html) ## Related pages End-to-end guide to sending email with OneSignal. Full HTML control, dark mode overrides, and advanced styling. Save and reuse email designs across campaigns. Add default or custom unsubscribe links for compliance. Personalize emails with tags, Liquid syntax, and dynamic content. Test subject lines, content, and send times to optimize engagement. # Design emails with HTML Source: https://documentation.onesignal.com/docs/en/design-emails-with-html Send fully custom branded emails using OneSignal's HTML Editor. Covers supported HTML rules, inline CSS, dark mode, unsubscribe handling, and client limitations. ## Overview The HTML Editor lets you send fully custom, branded emails using your own HTML. You should use the HTML Editor when you: * Need complete control over layout, spacing, and styling * Already have production-ready HTML email templates * Are comfortable working within email client limitations HTML emails are not the same as web pages. Many HTML and CSS features are unsupported or inconsistently rendered across email clients. ### Prerequisites Before using the HTML Editor, make sure you: * Have experience building responsive HTML emails * Host all images on publicly accessible URLs (your site, CDN, S3, etc.) #### Expected outcome After setup, your email: * Renders consistently across major clients (Gmail, Outlook, Apple Mail) * Tracks link clicks correctly * Includes a working unsubscribe mechanism * Passes spam and deliverability checks ### Import your own templates If you already have HTML email templates, you can add them to OneSignal in any of the following ways: 1. Use [Email Template forwarding](./email-template-forwarding) 2. Create templates programmatically using the [Create Template API](/reference/create-template) 3. Copy-paste your HTML into the HTML Editor Start with a proven template rather than writing HTML from scratch. ### Design in Figma and export to OneSignal If your email designs live in Figma, there are two ways to get them into OneSignal as HTML: * **[Email Love](https://emaillove.com/figma-plugin)**: A Figma plugin that converts your design into production-ready HTML email and uploads it directly to your OneSignal templates. Enter your OneSignal App ID and API key in the plugin's export panel and click upload. * **[Figma MCP server](https://help.figma.com/hc/en-us/articles/32132100833559-Guide-to-the-Figma-MCP-server)**: Connect Figma to an AI coding tool via the MCP server. Point it at your email design frame and ask it to generate HTML email code, then paste the output into the HTML Editor. **Common mistakes that cause rendering or deliverability issues:** * **Missing unsubscribe link**: Required for marketing emails. Without one, inbox providers are more likely to flag your email as spam. * **Using `
``` #### Understanding autoplay behavior * The embed URL (/embed/VIDEO\_ID) is required for autoplay — normal watch?v= links won’t work. * Adding ?autoplay=1\&mute=1 ensures the video plays automatically and complies with browser autoplay rules. * The allow="autoplay; encrypted-media" attribute grants permission for autoplay. * The .video-box uses a fixed 560×315px size (YouTube’s default 16:9) so the video appears contained instead of stretching full screen. * The body is set as a flex container with justify-content: center and align-items: center to position the video box in the exact middle of the screen. *** ## Performance tips * Prefer inline critical CSS; defer heavy scripts. * Optimize images (dimensioned `` with `object-fit`), use modern formats when possible. * Keep HTML payloads concise; avoid large base64 blobs. * Use system fonts or host performant web fonts with `font-display: swap`. * Reduce file sizes and use proper resolution: * [Apple image guidelines](https://developer.apple.com/design/human-interface-guidelines/ios/icons-and-images/image-size-and-resolution/) * [Android image overview](https://developer.android.com/guide/topics/graphics) * OneSignal has no affiliation with [imagekit.io](https://imagekit.io/), though it’s a helpful tool for optimization. ### Test across devices Send test messages frequently to confirm behavior and layout across device types. See [Find & set Test Users](./test-users). *** ## FAQ ### Can I remove the gray background or drop shadow from banner-style in-apps? Yes. For top/bottom banners, update your SDKs and set: **iOS 5.1.5+** ```xml theme={null} OneSignal_in_app_message_hide_drop_shadow OneSignal_in_app_message_hide_gray_overlay ``` **Android 5.1.9+** ```xml theme={null} ``` ### Can I reuse in-app templates from other providers? Yes. Paste your HTML into the editor and replace analytics/actions with the OneSignal In‑App JS methods. ### I don’t have templates. How do I start? Use the provided starter template or [browse available templates](./in-app-html-templates). Some HTML/CSS experience is recommended. ### What SDK version is required? * **iOS:** 3.9.0+ * **Android:** 4.6.3+ Older SDKs will fall back to Center Modal rendering. ### Do in-app messages work offline? No. Messages require an active internet connection to fetch and render content. *** # Disabled apps & organizations Source: https://documentation.onesignal.com/docs/en/disabled-apps Reasons OneSignal disables Apps and Organizations, what disablement blocks, and how App Admins and Organization Admins can restore access. ## Overview Read this guide if your OneSignal App or Organization has been disabled, or if you want to understand what triggers disablement, what it affects, and how to restore access. OneSignal disables Apps and Organizations to protect platform stability, enforce billing and compliance requirements, and prevent unintended messaging behavior. This guide explains: * How Apps and Organizations become disabled * What happens when an App or Organization is disabled * How to re-enable an App or Organization Before continuing, review: * [Apps, Organizations, & Accounts](./apps-organizations) to understand how ownership and billing are structured. * [Team members](./manage-team-members) to understand how to manage team access. An **App** holds user and messaging data for a single project, across multiple platforms (web, iOS, Android, email, etc.). An **Organization** is a container for managing multiple Apps, billing, and team permissions. *** ## How Apps and Organizations become disabled ### App An App may be disabled: * Manually by an App or Organization Admin in the dashboard at **Settings > Manage App > Disable App** * Automatically due to: * [API rate limits](/reference/rate-limits) * Violations of the [Acceptable Use Policy](https://onesignal.com/aup) ### Email channel An App's Email channel can be disabled separately from the App itself due to violations of the [Acceptable Use Policy](https://onesignal.com/aup), most commonly high bounce and spam complaint rates. When this happens, the App cannot send emails until the channel is re-enabled. Follow the [Email reputation best practices](./email-reputation-best-practices) guide to resolve the underlying issues, then reach out to `support@onesignal.com` with details on which steps you followed including screenshots to verify the issue has been resolved. ### Organization An Organization may be disabled due to: * Overdue invoices on a paid plan. See [Billing FAQ](./billing-faq) for more details. * Violations of the [Acceptable Use Policy](https://onesignal.com/aup) When an Organization is disabled, all Apps within the Organization are disabled, and billing or compliance issues must be resolved first. *** ## What happens when an App or Organization is disabled When an **App** is disabled: **Blocked:** * New messages cannot be sent. * In-app messages are paused. * Scheduled messages are cancelled. * Journeys are stopped and archived. * Data cannot be deleted or exported. **Still active:** * Existing data remains intact within normal retention periods. * The SDK continues to collect new and update existing user data. * Billing continues if the App is in an enabled Organization on a paid plan. * The App can still be deleted or moved to another Organization. You will continue to be billed for the App if it is within an enabled Organization on a paid plan. To stop billing: * Delete the App, or * Move it to a Free Organization Contact `support@onesignal.com` for assistance. When an **Organization** is disabled: * All Apps in the Organization are disabled. * Messaging, exports, and deletions are blocked. * Apps cannot be removed or transferred. Organization-level disablement is more restrictive than App-level disablement. *** ## How to re-enable an App or Organization You must be an **App or Organization Admin** to re-enable an App and an **Organization Admin** to re-enable an Organization. See [Team members](./manage-team-members) for details. ### Re-enable an App * If you manually disabled the App, go back to **Settings > Manage App > Disable App** and click **Re-enable**. * If the App was disabled due to rate limits, follow the in-dashboard prompt to re-enable it. * If the App was disabled for any other reason, contact `support@onesignal.com` for assistance. ### Re-enable an Email channel * Contact `support@onesignal.com`. * Share which steps you followed from the [Email reputation best practices](./email-reputation-best-practices) guide to address the bounce and complaint rates. ### Re-enable an Organization * If the Organization was disabled due to an overdue invoice, pay the invoice and the Organization will be re-enabled. * If the Organization was disabled for any other reason, contact `support@onesignal.com` for assistance. *** ## FAQ ### Does disabling an App stop billing? No. Disabled Apps still count toward monthly active user (MAU) billing. ### Does disabling an App stop new users? No. You must remove the OneSignal SDK to prevent new subscriptions. ### Can I re-enable a disabled App? Yes, in some cases. App or Organization Admins can re-enable manually disabled Apps and Organizations from the dashboard. Apps or Organizations disabled for policy violations require contacting support. ### How do I know why my App or Organization was disabled? Check dashboard warnings, recent send activity, and billing status. If you are unsure, contact support with your App ID. ### How do I contact support about a disabled App? Email `support@onesignal.com` with your App ID (found in **Settings > [Keys & IDs](./keys-and-ids)**). Include a description of the issue and whether the disablement was manual or automatic. *** Understand how ownership, billing, and access are structured across apps and organizations. Invite users, assign roles, and manage access at the app or organization level. Find your App ID, REST API key, and other credentials. Manage plans, billing, and subscriptions across organizations. # Duplicated notifications Source: https://documentation.onesignal.com/docs/en/duplicated-notifications Troubleshoot duplicate push notifications caused by SDK conflicts, server-side retries, multi-app environments, and platform-specific bugs. Duplicate notifications occur when the same device receives the same message content more than once. This guide covers the most common causes and how to resolve them. If the same user sees the notification on multiple devices (phone, tablet, desktop), that is expected behavior based on your targeting configuration (segments, external IDs, etc.). For duplicate in-app messages, see the [in-app message troubleshooting](./in-app-message-troubleshooting#duplicated-in-app-messages) guide instead. Apple acknowledged a bug in iOS 17 that caused duplicates. This was fixed in iOS 17.3. [Read more](https://forums.developer.apple.com/forums/thread/747044). ## Start here Pick the section that best matches your situation: * Your server retries failed API calls or your backend pipeline may double-send → [Same message sent multiple times](#same-message-sent-multiple-times) * Your app uses Firebase or another push SDK alongside OneSignal → [Third-party notification handlers](#third-party-notification-handlers) * Your Android app customizes notifications in code (service extension or foreground listener) → [Android notification handlers](#android-notification-handlers) * Your iOS app implements its own `UNUserNotificationCenterDelegate` → [iOS foreground handler](#ios-foreground-handler) * You only see duplicates on dev builds or with multiple installs → [Multiple app instances](#multiple-app-instances) ## Same message sent multiple times The most common cause is sending the same notification payload more than once through the OneSignal API. Common reasons: * Your server retries requests without checking if the first succeeded. * Logic in your backend pipeline sends the same notification twice. * You're migrating to OneSignal but still sending from a previous provider. Avoid sending from both systems at the same time. Prevent duplicate messages by reusing idempotency keys on retries. ## Third-party notification handlers Duplicates can occur when other code in your app processes OneSignal's push payload and displays its own notification in addition to OneSignal's. This includes: * Other remote push SDKs (for example, Firebase Cloud Messaging) that receive the same payload. * Custom message handlers such as a `FirebaseMessagingService` on Android or a `UNUserNotificationCenterDelegate` on iOS that read the incoming payload and build a local notification from it. OneSignal's SDK filters out OneSignal payloads automatically when it is the only handler. Third-party code does not know to filter them out unless you tell it to. ### Identifying OneSignal notifications Every OneSignal notification includes a `custom` object with an `i` key containing the OneSignal notification UUID: ```json JSON theme={null} { "custom": { "i": "the-notification-id" } } ``` For the full payload reference, see [Custom OneSignal payload structure](./osnotification-payload#custom-onesignal-payload-structure). ### Filtering OneSignal notifications from third-party handlers In any non-OneSignal handler, check for the `custom.i` key and return early when it is present. OneSignal then handles its own payloads while your other code continues to process its own. In a custom `FirebaseMessagingService` or other remote message handler: ```kotlin Kotlin theme={null} override fun onMessageReceived(remoteMessage: RemoteMessage) { val customJson = remoteMessage.data["custom"] if (customJson != null) { try { val custom = JSONObject(customJson) if (custom.has("i")) { // OneSignal notification — let the OneSignal SDK handle it return } } catch (e: JSONException) { // Not a OneSignal payload, fall through } } // Handle your own notification here } ``` In your `UNUserNotificationCenterDelegate` or a custom notification handler: ```swift Swift theme={null} func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { let userInfo = notification.request.content.userInfo if let custom = userInfo["custom"] as? [String: Any], custom["i"] != nil { // OneSignal notification — let the OneSignal SDK handle it completionHandler([]) return } // Handle your own notification here } ``` If you control the payload structure sent from your other provider, add a distinct marker key (for example, `my_app_notification: true`) and filter in both directions — only handle notifications containing your marker in your own code, and let OneSignal handle notifications containing the `custom.i` key. ## Android notification handlers OneSignal exposes two Android callbacks that let you intercept and customize a notification before it displays: * `onNotificationReceived` in a [notification service extension](./service-extensions) — runs for every push, including when the app is in the background. * `onWillDisplay` in a [foreground lifecycle listener](./mobile-sdk-reference#addforegroundlifecyclelistener-push) — runs only when the app is in the foreground. Duplicates happen when these callbacks display the notification in addition to OneSignal's automatic display. ### The `preventDefault()` and `display()` rule OneSignal displays each notification automatically unless you suppress it with `event.preventDefault()`. Two common mistakes cause duplicates: * Calling `event.getNotification().display()` without first calling `event.preventDefault()` — shows the notification twice. * Posting a separate notification with [`NotificationManagerCompat.notify()`](https://developer.android.com/reference/androidx/core/app/NotificationManagerCompat) while OneSignal also displays the original. Only one display path should be active — either OneSignal's `display()` or your own `NotificationManagerCompat.notify()`, never both. ```kotlin Kotlin theme={null} override fun onNotificationReceived(event: INotificationReceivedEvent) { event.preventDefault() val notification = event.notification notification.setExtender { builder -> builder.setColor(0xFF0000FF.toInt()) } notification.display() } ``` ```java Java theme={null} @Override public void onNotificationReceived(INotificationReceivedEvent event) { event.preventDefault(); IDisplayableMutableNotification notification = event.getNotification(); notification.setExtender(builder -> builder.setColor(0xFF0000FF) ); notification.display(); } ``` The same rule applies inside `onWillDisplay` in a foreground lifecycle listener: ```kotlin Kotlin theme={null} OneSignal.Notifications.addForegroundLifecycleListener(object : INotificationLifecycleListener { override fun onWillDisplay(event: INotificationWillDisplayEvent) { event.preventDefault() // Render your own UI or call event.notification.display() to show the OneSignal notification } }) ``` If you call `preventDefault()` but never call `display()`, the notification is silently dropped and the user does not see it. Every code path should either call `display()` once or intentionally suppress the notification. If you use both a notification service extension and a foreground lifecycle listener, make sure only one handler displays the notification for a given app state. Calling `display()` from both produces duplicates. ### Async work inside callbacks When you do background work (network calls, database lookups) before displaying, call `preventDefault()` synchronously in the callback and call `display()` only after the async work completes. Returning from the callback without `preventDefault()` lets OneSignal display the notification, and a later `display()` call produces a duplicate. ## iOS foreground handler OneSignal sets itself as the `UNUserNotificationCenterDelegate` during SDK initialization. If your app also implements foreground notification handling, the notification can display twice — once from OneSignal and once from your code. Common patterns that cause duplicates: * Implementing your own `UNUserNotificationCenterDelegate` without forwarding calls to OneSignal, then calling `completionHandler([.banner, .sound])` and showing a custom in-app alert from the same method. * Scheduling a local notification (`UNUserNotificationCenter.current().add(...)`) in response to an incoming push that OneSignal is also displaying. To control foreground display without causing duplicates, use OneSignal's [foreground lifecycle listener](./mobile-sdk-reference#addforegroundlifecyclelistener-push) and call `event.preventDefault()` when you want to render the notification yourself: ```swift Swift theme={null} OneSignal.Notifications.addForegroundLifecycleListener(self) func onWillDisplay(event: OSNotificationWillDisplayEvent) { event.preventDefault() // Render your own UI or call event.notification.display() to show the OneSignal notification } ``` ## Multiple app instances Duplicates can happen when you have both production and development versions of your app installed. Each app has a unique package name and receives its own push token. Long-press the notification to confirm which app instance sent it. If two device records in OneSignal share the same push token, the device may receive two notifications. This can happen when the same token is imported or registered multiple times. OneSignal includes checks to prevent this, but edge cases may bypass them. Web push duplicates are caused by subscribing to multiple origins using the same OneSignal App ID. An [origin](https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy) is the combination of scheme, host, and port that the browser uses to scope push subscriptions. For example, a user who subscribes to `https://example.com` and `https://sub.example.com` counts as two subscriptions, and a send using the same App ID reaches both. To fix: 1. [Reset browser data and push permissions](./troubleshooting-web-push) per origin. 2. If many users are affected, create a new OneSignal App and use the new App ID in your site's init code. 3. Have users revisit the site and resubscribe. Subscribing on multiple browsers or browser profiles also leads to multiple notifications. ## Diagnostic tips To debug duplicates faster, collect and send the following to OneSignal support: * OneSignal Subscription ID of the device that received the duplicates * OneSignal Message ID or a link to the message in the dashboard * List of other libraries or plugins in your app * [Debug log reproducing the issue](./capturing-a-debug-log) * Detailed reproduction steps ## FAQ ### What happens if I have two notification SDKs in my app? Both SDKs may independently process and display the same notification. OneSignal filters its own notifications automatically, but other SDKs do not. Filter OneSignal payloads out of your other SDK's handlers by checking for the `custom.i` key. See [Third-party notification handlers](#third-party-notification-handlers) for code examples. ### How do I send push from a previous provider and OneSignal at the same time? You can transition gradually, but avoid sending the same message from both providers. * **Android**: Remove old SDK notification handling code when integrating OneSignal and releasing the app. As users update, they stop receiving push from the old provider. * **iOS**: You can continue sending from the old provider temporarily while users update. Once fully transitioned, send from OneSignal only to avoid duplicates. ### How do I prevent multiple notifications for rapidly-changing updates? Use a [`collapse_id`](/reference/push-notification#body-parameters) in the [Create notification](/reference/push-notification) API to replace previous notifications instead of stacking them. When multiple notifications share the same `collapse_id`, each new one replaces the previous notification in the tray. This is useful for stock price updates, live scores, delivery ETAs, and similar frequent updates. ## Related pages Resolve common issues with push notification delivery on iOS and Android. Debug web push subscription, service worker, and delivery issues. Fix issues with in-app message display, triggers, and duplicates. Intercept and customize push notifications before display on iOS and Android. Troubleshoot web push notifications that are sent but not displayed. # Dynamic Content with CSV Source: https://documentation.onesignal.com/docs/en/dynamic-content Personalize push, email, and SMS messages at scale using Dynamic Content with CSV uploads and Liquid syntax in OneSignal. OneSignal provides several ways to [personalize message content](./message-personalization) at scale. This guide focuses on using the Dynamic Content with CSV upload feature in the OneSignal dashboard for push, email, and SMS. **Key benefits:** * **Use a CSV to personalize at scale** – One message, custom experiences for everyone * **Dynamic personalization** – Content adapts to user properties (language, region, campaign ID) * **HTML injection (email)** – Include HTML markup in CSV cells to dynamically build rich email content * **Team collaboration** – Non-technical users edit content in CSV files * **Cross-channel compatibility** – Reuse CSV logic across channels * **Multi-language support** – Automatic language switching per user **Common use cases:** * Multi-language onboarding or marketing * Region-specific promotions * Event announcements per location * Campaign-based personalization *** ## Dynamic Content with CSV setup steps **Quick reference:** 1. Create a CSV file with your content variations. 2. Create a [template](./templates) 3. Map the CSV data to the message using the `dynamic_content` property in liquid syntax. 4. Select the **Dynamic Content** or **Personalization** button. 5. Upload the CSV file and send the message. ### CSV requirements & setup * **File size:** Under 200 KB * **Column headers:** * Reserve the first column header for the tag key or leave blank to reference sections * Alphanumeric characters and underscores only * Use underscores (`_`) instead of spaces * **Encoding:** UTF-8 Start with a blank CSV or use a provided template. Templates are provided when selecting the **Dynamic Content** or **Personalization** button in the message and template editors. Dynamic Content button in the OneSignal message editor Templates are available for: * **Multi-language** – Localize content by language * **Content personalization** – Customize content by Tags CSV template options showing multi-language and content personalization templates #### Example CSVs This guide will use the following example CSV data. * Map the column headers to your [supported language codes](./multi-language-messaging#supported-languages). * Add your translations to each row for each language code. * If you have multiple sections (like in an email), designate the first column as the section name. This example includes: * 3 languages: English, Spanish, and French. * 2 sections: "section\_1" and "section\_2". Multi-language CSV with language codes as column headers and translated content in rows * Map the first column header to the tag key and underlying rows to the values you want to reference. See [Tags](./add-user-data-tags) for more information on adding tags to users. * Map the remaining column headers to the sections or components you want to personalize. In this example: * The tag key is `campaign_id` with possible values of `A` and `B`. * There are 3 components: "title", "message", and "url". Content personalization CSV using campaign_id tag with title, message, and url columns **Important:** Remove spaces and non-alphanumeric characters from CSV headers or use hash notation in Liquid. Detailed CSV example showing Liquid syntax references for tag-based personalization ### Map CSV data to message content Using [Liquid syntax](./using-liquid-syntax), reference the CSV data in your message using the `dynamic_content` property: ```liquid theme={null} {{dynamic_content.file_name.message_component[user_property]}} {{dynamic_content.file_name[user_property].message_component}} ``` **Parameters:** * `dynamic_content` – The property name used to reference the CSV data * `file_name` – CSV file name (without `.csv` extension) * `message_component` – The specific message component you want to personalize. This is the static text in the CSV column header or first row. * `user_property` – The [user property](./message-personalization#properties) you want to reference. **Fallback content:** Always use hard-coded string `default` fallbacks to ensure messages render if the CSV lookup or Dynamic Content fails. ```liquid Liquid syntax for the fallback theme={null} {{ dynamic_content.my_template.header[user.language] | default: "Welcome to our latest update" }} ``` This means if the CSV lookup or Dynamic Content fails, the message will render the fallback text `"Welcome to our latest update"`. This ensures: * Dynamic Content is used when available * A hard-coded message appears if Dynamic Content fails * Users never receive blank content ```csv translations.csv theme={null} ,en,es,fr section_1,Hello,Hola,Bonjour section_2,Thanks for shopping with us...,Gracias por comprar con nosotros...,Merci pour votre achat avec nous... ``` * The `file_name` is `translations.csv`. * The `message_component` is in the rows of the first column `section_1` and `section_2`. * The `user_property` is the column header matching language code. We can reference this on the user with the `user.language` [property](./message-personalization#properties). ```liquid Basic Liquid syntax for the multi-language message theme={null} {{dynamic_content.translations.section_1[user.language]}} {{dynamic_content.translations.section_2[user.language]}} ``` ```liquid (Recommended) Liquid syntax with default fallback for the multi-language message theme={null} {% assign supported_langs = "en,es,fr,de" | split: "," %} {% assign lang = user.language | default: "en" %} {% unless supported_langs contains lang %} {% assign lang = "en" %} {% endunless %} {{ dynamic_content.translations.section_1[lang] | default: "Hello" }} {{ dynamic_content.translations.section_2[lang] | default: "Thanks for shopping with us..." }} ``` OneSignal email editor with Liquid syntax for multi-language Dynamic Content ```csv content_personalization_template.csv theme={null} campaign_id,title,message,url campaign_123,Flash deal for you,Tap to claim,https://example.com/flash campaign_456,Weekend picks are here,See what's trending,https://example.com/weekend default,Our latest offers,See what's new,https://example.com ``` * The `file_name` is `content_personalization_template.csv`. * The `user_property` maps to the tag key: `campaign_id`. * The `message_component` is the column header mapped to the section of the message you want to personalize (title, message, or url). ```liquid Liquid syntax for the content personalization message theme={null} {{ dynamic_content.content_personalization_template[campaign_id].title }} {{ dynamic_content.content_personalization_template[campaign_id].message }} {{ dynamic_content.content_personalization_template[campaign_id].url }} ``` ```liquid (Recommended) Liquid syntax with default fallback for the content personalization message theme={null} {% assign cid = campaign_id | default: "default" %} {% unless dynamic_content.content_personalization_template[cid] %} {% assign cid = "default" %} {% endunless %} {{ dynamic_content.content_personalization_template[cid].title | default: "Our latest offers" }} {{ dynamic_content.content_personalization_template[cid].message | default: "See what's new" }} {{ dynamic_content.content_personalization_template[cid].url | default: "https://example.com" }} ``` Dynamic Content personalization Liquid example Use Liquid with `default` fallback to update subject lines, preheaders, button labels, and URLs. ### Using HTML in CSV cells (email only) You can include HTML markup directly in CSV cells to inject rich content into emails. This is useful for swapping entire sections of an email — such as banners, CTAs, or styled blocks — based on user properties. ```csv promo_banners.csv theme={null} campaign_id,banner_html spring_sale, default, ``` Reference the HTML cell in your email template: ```liquid theme={null} {{ dynamic_content.promo_banners[campaign_id].banner_html | default: "

Check out our latest offers

" }} ``` The HTML renders directly in the email body, so you can use any inline styles and markup that email clients support. HTML from CSV cells is only supported in email. Push and SMS channels render content as plain text and do not interpret HTML markup. *** ## Reference ### Updating templates Re-upload CSVs via the dashboard or use the [Update Template API](/reference/update-template) `dynamic_content` property. ### Special characters in keys **Hash notation** (for non-alphanumeric keys): ```liquid theme={null} {{ dynamic_content.file_name["!the_row!"]["&the_column&"] }} ``` **Dot notation** (for standard keys): ```liquid theme={null} {{ dynamic_content.file_name.the_row.the_column }} ``` *** ## FAQ ### How can I test Dynamic Content with CSV? Use email to test multiple variations of the message. * Use `+` addressing in emails to test multiple variations: `username+test@example.com`. * Set tags following the multi-language and content personalization examples above. * See [Import](./import) for more on uploading multiple users and tags. ### When should I use Dynamic Content with CSV vs. other personalization options? Use **Dynamic Content with CSV** when sending messages from the dashboard and you have user data in a CSV file. For other ways to add dynamic content to messages, see [Message Personalization](./message-personalization) or [Multi-language Messaging](./multi-language-messaging). *** ## Related pages Overview of all personalization options available in OneSignal. Complete Liquid syntax reference for OneSignal messages. Upload user data and segments to OneSignal. Create and manage reusable message templates. # Email address validation Source: https://documentation.onesignal.com/docs/en/email-address-validation Validate email addresses during CSV import and in bulk to reduce bounces and protect your sender reputation. Email address validation detects common problems in email addresses before they reach your audience. It flags typos, invalid domains, role-based addresses, and disposable email services that could increase your bounce rate or hurt your sender reputation. It runs automatically during CSV import and is also available as a bulk validation tool for your existing audience. Email address validation checks addresses against OneSignal's validation criteria. It is not a live bounce verification check and does not guarantee deliverability. ## Validation checks Email address validation evaluates each address against the following checks: | Check | What it detects | | ---------------------- | --------------------------------------------------------------------------------------- | | **Syntax validation** | Malformed addresses missing required components (e.g. missing `@`, invalid TLD) | | **Typo detection** | Common domain misspellings like `gnail.com` or `gmail.con` | | **MX record check** | Domains with no valid mail exchange record; email sent here cannot be delivered | | **Role-based address** | Addresses like `sales@`, `info@`, `admin@` that are unlikely to belong to an individual | | **Disposable email** | Domains associated with temporary or throwaway email services | **Free plans** include syntax validation and typo detection. **Paid plans** include all of the above plus MX record checks, role-based address detection, and disposable email filtering. If an address fails a check, email address validation returns a human-readable error explaining the specific problem. If an address fails multiple checks, all failure reasons are returned together so you can address everything at once. *** ## Validate addresses during CSV import When importing a CSV, you can configure email address validation settings on the **Review** step before confirming the import. Go to **Audience > Import** and upload your file. Complete the **Upload** and **Map Fields** steps. If any invalid email addresses are detected, a warning will appear on the **Map Fields** step. Import CSV Map Fields step showing an invalid emails detected warning On the **Review** step, expand **Email address validation settings**. The following options are available: Import CSV Review step with Email address validation settings expanded * **Validate email domains (MX records)**: confirm the domain can receive email * **Exclude disposable email addresses**: prevent temporary or one-time addresses that may reduce engagement * **Exclude role-based email addresses**: prevent shared addresses like `info@` or `support@` that often have lower engagement All three are enabled by default. Uncheck any you don't want applied to this import. If you need to import invalid email addresses (for example, to update suppression or subscription status), check **Allow invalid email addresses**. This bypasses validation and accepts any string as an email address. Click **Confirm and Import**. When the import is complete, you'll receive an email summarizing the results: * Subscriptions added * Subscriptions modified * Rows not imported, with a link to download the rejected rows #### Result Valid addresses are added to your audience. You receive an email when the import is complete. Addresses that failed validation are excluded and available to download from the link in the results email. *** ## Validate your existing audience in bulk Use bulk validation to check email addresses already in your OneSignal audience. You receive results by email as a CSV export. Go to **Audience > Subscriptions** (or **Audience > Users**), click **Update/Import Users**, then select **Validate Email Addresses**. Update/Import Users dropdown with Validate Email Addresses option highlighted In the modal, click **Start validation**. Email address validation runs against all email addresses for your app. Validate Email Addresses modal showing Start validation button When validation is complete, you'll receive an email from OneSignal with a **Download CSV Export** link. The link is active for 72 hours. The email summarizes how many addresses failed, broken down by: OneSignal results email showing validation summary and Download CSV Export button * Typos * Role-based addresses * Disposable domains * Invalid MX records Download the CSV and review the flagged addresses. To clean up your audience, create a new CSV with the corrected or actioned addresses and re-import it using one of the following column formats: | Goal | Required columns | Value | | -------------------------------- | --------------------- | --------------------- | | Re-subscribe corrected addresses | `email`, `subscribed` | `subscribed` = `yes` | | Unsubscribe flagged addresses | `email`, `subscribed` | `subscribed` = `no` | | Suppress flagged addresses | `email`, `suppressed` | `suppressed` = `true` | #### Result You receive an email with a CSV export of flagged addresses and a summary of validation results broken down by error type. CSV export showing email, reason, suggested_email, created_at, last_opened, and last_clicked columns *** ## Validation error reference The following errors may appear in validation results: | Error | What it means | What to do | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | | **Invalid input** | The address is malformed and does not meet basic syntax requirements (e.g. missing `@`, invalid TLD). | Correct or remove the address. | | **Typo in email address** | A likely misspelling was detected in the domain. A suggested correction appears in the adjacent column. | Apply the suggested fix or remove the address. | | **MX record not found** | The domain has no valid mail exchange record. Email cannot be delivered to this address. | Remove or suppress the address. | | **Role-based email address** | The local part (before `@`) matches a known role-based pattern like `sales@` or `support@`. These are unlikely to belong to an individual. | Review whether the address is appropriate for your use case, then suppress or remove it. | | **Disposable email address** | The domain is associated with a temporary or throwaway email service. | Suppress or remove the address. | *** ## API error responses Email address validation also applies to addresses submitted through the API. The sections below describe the error responses you may encounter. API validation only checks that the email address is correctly formatted. It does not check for typos, MX records, role-based addresses, or disposable domains. Those checks are only available during CSV import and bulk validation. ### Send email: Create Message When you send to one or more invalid addresses via the [Create Message endpoint](/reference/create-message), invalid addresses are excluded from delivery and returned under `invalid_email_tokens`. ```json theme={null} { "id": "", "errors": { "invalid_email_tokens": ["invalid@email"] } } ``` ### Add a user or subscription: User Model API When you create a user or subscription with an invalid email via the [Create User](/reference/create-user) or [Create Subscription](/reference/create-subscription) endpoint: ```json theme={null} { "errors": [ { "title": "Invalid `token` format for device type email" } ] } ``` ### Add a device: Legacy Players API When you add or edit a player record with an invalid email via the [legacy Players API](/reference/add-a-device): ```json theme={null} { "success": false, "errors": ["[\"Identifier invalid format.\"]"] } ``` The Legacy Players API is deprecated. Migrate to the [User Model API](/reference/create-user) to receive more descriptive validation errors and access current features. # BCC emails Source: https://documentation.onesignal.com/docs/en/email-bcc Send a hidden copy of an outgoing email to additional recipients for archiving, compliance, or third-party integrations. Add BCC (blind carbon copy) recipients to a OneSignal email so the message is also delivered to one or more hidden addresses. BCC recipients receive the same content as the primary recipient and are not visible to anyone else on the send. Common use cases: * Archive customer-facing email to a shared inbox or compliance system. * Forward sends to third-party services like Trustpilot Automatic Feedback Service (AFS). * Keep internal stakeholders copied on transactional or high-value sends. Do not BCC password resets, magic links, two-factor codes, or any email containing sensitive data. The BCC recipient sees the full personalized message body. Treat BCC as a content disclosure and pick recipients deliberately. *** ## Prerequisites * Complete [Email setup](./email-setup) and verify your sending domain. * Confirm your billing plan accommodates additional volume. For every email sent, an additional email is sent to each BCC address. See [Billing and analytics](#billing-and-analytics). *** ## How BCC works in OneSignal * BCC recipients are set per message at send time. There is no account-level default BCC. * BCC sends count toward your billable email volume. * Each BCC address adds one send per recipient to your email volume: OneSignal sends one email to your recipient and one email to each BCC address. * BCC sends are excluded from engagement metrics (open rate, click rate, conversions) and included in your total send count so billing remains accurate. * BCC addresses are validated for syntax only (the same check OneSignal applies to the **From** address). Deliverability, suppression status, and domain ownership are not verified. Because BCC addresses are not validated for deliverability, a typo or bounced address fails silently with no retry and no notification. Send a test message to each BCC address before relying on it for compliance workflows. *** ## Send a BCC email from the dashboard 1. In your OneSignal dashboard, go to **Messages > New Message > Email**. 2. Compose your email. Set the audience, subject, and content as usual. 3. In the **Sender** section, locate the **BCC** field. 4. Enter one or more email addresses, separated by commas: ``` archive@yourdomain.com, compliance@yourdomain.com ``` 5. Click **Review and Send** to deliver immediately, or **Save** to keep the message as a draft. **Expected outcome:** Each BCC address receives a copy of the email at the same time as the primary recipient. The primary recipient cannot see the BCC list, and BCC recipients cannot see one another. After sending, open **Delivery > Sent Messages** and select the message to confirm the BCC recipients appear on the message detail page. *** ## Use BCC with a template BCC addresses are configured directly on an email template and apply to every send that uses it. 1. Go to **Messages > Email > Templates** and open or create a template. 2. In the **Sender** section, locate the **BCC** field. 3. Enter up to 5 addresses, separated by commas. 4. Save the template. *** ## Use BCC in a Journey When you select a template in a Journey's **Send Email** step, the BCC addresses from that template appear in the **Sender Settings** section of the step. They are read-only from here. To update them, select **Edit** next to the template name to modify the template directly. Changes to a template's BCC addresses apply to all future sends using that template, including active Journeys. *** ## Send a BCC email via the API Pass an `email_bcc` array on the [Create message](/reference/create-message) endpoint. Each entry must be a syntactically valid email address. ```bash cURL theme={null} curl -X POST 'https://api.onesignal.com/notifications' \ -H 'Authorization: Key YOUR_REST_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "app_id": "YOUR_APP_ID", "include_aliases": { "external_id": ["user-123"] }, "target_channel": "email", "email_subject": "Your receipt", "email_body": "

Thanks for your purchase.

", "email_bcc": ["archive@yourdomain.com", "compliance@yourdomain.com"] }' ``` ```javascript JavaScript theme={null} const response = await fetch('https://api.onesignal.com/notifications', { method: 'POST', headers: { Authorization: `Key ${process.env.ONESIGNAL_REST_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ app_id: process.env.ONESIGNAL_APP_ID, include_aliases: { external_id: ['user-123'] }, target_channel: 'email', email_subject: 'Your receipt', email_body: '

Thanks for your purchase.

', email_bcc: ['archive@yourdomain.com', 'compliance@yourdomain.com'], }), }); if (!response.ok) { const error = await response.json(); throw new Error(`Send failed: ${error.errors?.join(', ')}`); } ``` ```python Python theme={null} import os import requests response = requests.post( "https://api.onesignal.com/notifications", headers={ "Authorization": f"Key {os.environ['ONESIGNAL_REST_API_KEY']}", "Content-Type": "application/json", }, json={ "app_id": os.environ["ONESIGNAL_APP_ID"], "include_aliases": {"external_id": ["user-123"]}, "target_channel": "email", "email_subject": "Your receipt", "email_body": "

Thanks for your purchase.

", "email_bcc": ["archive@yourdomain.com", "compliance@yourdomain.com"], }, ) response.raise_for_status() ```
### Request fields An array of email addresses to receive a hidden copy of the message. Each address must pass basic syntax validation (the same check OneSignal applies to the **From** address). Deliverability, suppression status, and domain ownership are not verified. Invalid addresses fail silently and are not retried. ### Errors If any entry in `email_bcc` fails syntax validation, the API returns `400 Bad Request` with an `Invalid BCC address` error and the message is not sent. Fix or remove the offending entry and retry. *** ## Billing and analytics | Metric | Behavior | | -------------------------------------- | ----------------------------------------------------------------------------------------------- | | Total email sends | Includes BCC recipients. For every email sent, an additional email is sent to each BCC address. | | Open rate, click rate, conversions | Excludes BCC recipients. | | Delivery, bounce, and complaint events | Excludes BCC recipients. | | Suppression list | Not enforced for BCC addresses. A suppressed address still receives the BCC copy. | Plan for the volume increase before enabling BCC at scale. Each BCC address adds one additional email per recipient to your billable volume. For high-volume archive use cases, such as mirroring every send to a compliance store, evaluate whether [Event Streams](./event-streams) meets your requirements. Event Streams capture send and engagement data without adding to your email volume. *** ## Best practices * **Send a test before going live.** Add the BCC address to a one-off message and confirm receipt in the destination inbox before adding it to production sends. * **Use a dedicated inbox with sufficient capacity.** Route BCC traffic to a purpose-built inbox like `archive@mail.yourdomain.com`, not a shared or personal account. Free-tier and shared mailboxes fill up quickly at scale: a 1 GB inbox can reach capacity after as few as 60,000 emails depending on message size. When the BCC inbox is full, all remaining BCC deliveries fail silently with no retry. * **Keep the list short.** Each BCC recipient adds billable volume. A single archive address is usually sufficient for compliance. * **Exclude transactional and security email.** Password resets, magic links, OTPs, and 2FA codes should never include a BCC. * **Match BCC scope to the use case.** For Trustpilot AFS, only BCC the Trustpilot address on transactional sends that should generate review invitations, not on every marketing email. *** ## Common failure points OneSignal does not retry BCC delivery failures. If a BCC address is misspelled, mailbox-full, or rejected by the receiving server, the message is dropped silently. Send a manual test to each address before relying on it. BCC sends count toward your total email volume but are excluded from open and click rates. If your total sends rose without a matching rise in engagement, the new volume is likely BCC traffic. Compare engagement against the primary recipient count, not the total send count. Each entry in the `email_bcc` array must be a bare, syntactically valid email address. Remove extra whitespace, stray punctuation, and display-name formatting like `Name `. When sending to a large audience, a small or shared BCC inbox can reach capacity before the send completes. Once the inbox is full, remaining BCC deliveries fail silently with no retry or alert. Primary recipients are not affected. Use a dedicated inbox with adequate storage, or configure auto-archiving or auto-delete rules to keep the mailbox clear. Suppression rules apply only to the primary audience. BCC bypasses the suppression list. Remove the address from your BCC list manually if it should no longer receive copies. *** ## FAQ ### Can I set a default BCC address for every email? No. BCC must be set per message in the dashboard or per request in the API. There is no account- or app-level default. ### Are BCC recipients shown to the primary recipient? No. BCC stands for "blind carbon copy." Each BCC recipient sees only their own address, and BCC recipients cannot see one another. ### Do BCC addresses respect my suppression list? No. The suppression list applies to your primary audience. Do not add a suppressed address to BCC for compliance; the message will still be delivered. ### Are BCC sends included in analytics? BCC sends count toward your total email volume so billing reflects the true volume. They are excluded from engagement metrics like open and click rates because BCC inboxes are typically archives, not engaged readers. ### What happens if a BCC address bounces? The BCC delivery is dropped. OneSignal does not retry, does not surface a bounce event for the BCC recipient, and does not affect the primary recipient's delivery. Test BCC addresses before relying on them. *** ## Related pages Full guide to sending, scheduling, and tracking email. Reference for the email send endpoint and all available parameters. Manage blocked and suppressed email addresses. Stream email events to your data warehouse for compliance archiving without adding billable sends. # Email deliverability Source: https://documentation.onesignal.com/docs/en/email-deliverability Improve inbox placement by managing sender reputation, reducing bounces and spam complaints, and following email hygiene best practices. Email deliverability is the ability of your messages to reach recipients' inboxes instead of being blocked, sent to spam, or lost in transit. Deliverability depends on your sender reputation, email content, [domain alignment](./email-dns-configuration) (DKIM and SPF matching your From: domain), and recipient engagement. Internet service providers (ISPs) and mailbox providers (Gmail, Outlook, etc.) assess every incoming email to filter out unwanted or harmful messages.