# Home

Welcome to our product documentation. Choose from the following getting started options to start learning about our platform:

## Integration

{% content-ref url="/pages/t3SHa3nQ0vJwisxqwqy2" %}
[Web Push](/integration/web-push)
{% endcontent-ref %}

{% content-ref url="/pages/-LryAZh5XNeH6hyMrkcG" %}
[Native App Push](/integration/implementation-steps)
{% endcontent-ref %}

## Platform

{% content-ref url="/pages/-Lrz2qvieshhsVi3n-sd" %}
[Dashboard](/platform/dashboard)
{% endcontent-ref %}

{% content-ref url="/pages/-Lrz4CxkiijxnNIHplxE" %}
[Notifications](/platform/notifications)
{% endcontent-ref %}

{% content-ref url="/pages/-Lrz6I5jOTms1wBIg2xs" %}
[Segments](/platform/segments)
{% endcontent-ref %}

{% content-ref url="/pages/-Lrz6JDcu3qPGXp6Q9U5" %}
[Journeys](/platform/journeys)
{% endcontent-ref %}

{% content-ref url="/pages/-Lrz6Kgz2Pwn3dx5kQ-M" %}
[Insights](/platform/insights)
{% endcontent-ref %}

{% content-ref url="/pages/-Lrz6Lz4qOB6fuYhbdds" %}
[User Management](/platform/user-management)
{% endcontent-ref %}

{% content-ref url="/pages/-MgH4QhmxcYHJbpHgrl8" %}
[Organizations](/platform/organizations)
{% endcontent-ref %}

## Info Center


# Web Push

{% content-ref url="/pages/exfjqFW7w0lAgDpqaWKf" %}
[Quick Start](/integration/web-push/quick-start)
{% endcontent-ref %}

{% content-ref url="/pages/-MX2hcFU1cZgUEjUobnM" %}
[Installation & Setup](/integration/web-push/wordpress-plugin/wordpress-plugin)
{% endcontent-ref %}

{% content-ref url="/pages/7qUNbT6azewkcPDzxZ21" %}
[Wix Integration Steps](/integration/web-push/wix-integration-steps)
{% endcontent-ref %}

{% content-ref url="/pages/C9dKtylzxybHCFX9LnrJ" %}
[SDK](/integration/web-push/sdk)
{% endcontent-ref %}


# Quick Start

## Requirements

Support for web push notifications requires:

* Your website to be SSL-enabled (https\://)
* One publicly-accessible file to be placed on your site
* A snippet of Integration HTML to be added to each page on your site.
  * This can also be done with a tag manager.

## Steps

{% hint style="info" %}
If your site uses Wordpress you can skip these steps and follow the [<mark style="color:blue;">Wordpress Plugin</mark>](/integration/web-push/wordpress-plugin/wordpress-plugin) steps for an easier implementation.
{% endhint %}

### Step 1: Place the Service Worker File

Download the Service Worker from [<mark style="color:blue;">our CDN</mark>](https://cdn.p-n.io/pushly-sdk-worker.js) ([<mark style="color:blue;">https://cdn.p-n.io/pushly-sdk-worker.js</mark>](https://cdn.p-n.io/pushly-sdk-worker.js)) and place it in a publicly accessible location on the same domain that serves your site. For example: `https://www.example.com/assets/pushly/pushly-sdk-worker.js`

{% hint style="danger" %}
This file **must** be served from the same origin the potential subscriber is on when they visit your site. It cannot be served from a separate domain or subdomain.\
\
For example, if your visitor is on `www.example.com` the service worker must be located on `www.example.com.` It cannot be served from `assets.example.com` or `examplecdn.com`.&#x20;

Also note that it **cannot** be served from behind a 301/redirect.
{% endhint %}

### Step 2: Add the Integration HTML

If you placed the Service Worker in a subdirectory on your domain follow the steps in the *Service Worker Placed in Subdirectory* tab otherwise select the *Service Worker Placed in Public Root* tab below.

{% tabs %}
{% tab title="Service Worker Placed in Subdirectory" %}
Add the following HTML snippet to the head section of your site and replace **both** instances of `SDK_KEY` with the value provided by your account manager.

Also, replace the `/absolute/path/to/pushly-sdk-worker.js` text in the snippet with the full path to the Service Worker.

```javascript
<script src="https://cdn.p-n.io/sdk/web/latest/pushly-sdk.min.js?domain_key=SDK_KEY" async></script>
<script>
  window.PushlySDK = window.PushlySDK || [];
  function pushly() { window.PushlySDK.push(arguments) }
  pushly('load', {
    domainKey: 'SDK_KEY',
    sw: '/absolute/path/to/pushly-sdk-worker.js',
  });
</script>
```

{% hint style="danger" %}
The `sw` property should not include the origin/domain. It should be a path only; I.e., do not include `https:///www.example.com` in this value.
{% endhint %}
{% endtab %}

{% tab title="Service Worker Placed in Public Root" %}
Add the following HTML snippet to the head section of your site and replace **both** instances of `SDK_KEY` with the value provided by your account manager.

```javascript
<script src="https://cdn.p-n.io/sdk/web/latest/pushly-sdk.min.js?domain_key=SDK_KEY" async></script>
<script>
  window.PushlySDK = window.PushlySDK || [];
  function pushly() { window.PushlySDK.push(arguments) }
  pushly('load', {
    domainKey: 'SDK_KEY'
  });
</script>
```

{% endtab %}
{% endtabs %}

Need strict CSP or Subresource Integrity (SRI)? See [Delivery & Security (SRI)](/integration/web-push/sdk/delivery-and-security-sri).


# WordPress Plugin

If your website uses WordPress, you can install Pushly directly from the WordPress plugin directory — no manual code changes required.

The Pushly WordPress plugin enables you to:

* Automatically embed the Pushly SDK on your site for subscriber opt-in
* Send push notifications directly from the WordPress post editor
* Configure notification settings, auto-send behavior, and target post types
* Troubleshoot notification delivery with a built-in Activity Log

### Getting Started

Follow the [Installation & Setup guide](/integration/web-push/wordpress-plugin/wordpress-plugin) to install the plugin and configure your SDK Key and API Key.

{% content-ref url="/pages/-MX2hcFU1cZgUEjUobnM" %}
[Installation & Setup](/integration/web-push/wordpress-plugin/wordpress-plugin)
{% endcontent-ref %}

### Having Issues?

See the [Troubleshooting guide](/integration/web-push/wordpress-plugin/wordpress-plugin#troubleshooting) for help with common issues including notification delivery problems and server configuration.


# Installation & Setup

Instructions for installing Pushly via the Wordpress plugin

If your website utilizes Wordpress you can skip the manual steps and install Pushly via the Wordpress plugin directory.

## Step 1: Create Your WordPress API Key

If you want to send notifications directly from the WordPress plugin you will need to create a WordPress API Key within the platform. You can skip this section if you do not want to create notifications directly from WordPress.

Navigate to the *Domain Settings* section of the Pushly platform and click the *Integrations* tab. Within this tab you will see a *Content Management Systems* section that contains a WordPress card.

<figure><img src="/files/UBL0wYXn3Ty9SurNHPf5" alt=""><figcaption></figcaption></figure>

Click the **New API Key** button and wait for the key to be displayed on the screen. Once displayed, click the key to copy it your clipboard and store it for use in Step 3.

## Step 2: Install the Plugin

Log in to your Wordpress Admin with a user that has access to install new plugins and navigate to the plugins directory. Click the `Add New` button to be taken to a list of available plugins to install.&#x20;

![](/files/-MX2jCrftg2-ZUc4VKFf)

On the `Add Plugin` page search for `Pushly` in the search box and then click the `Install Now` button on the Pushly plugin card.

![](/files/-MX2jZeAVJseW31E1N_s)

Once the plugin is installed the `Install Now` button will change to an `Activate` button. Click this button again to complete the installation process and then move to Step 2 to complete set up of the plugin.

## Step 3: Configure the Plugin

After completing Step 1 and activating the plugin a new menu item labeled `Pushly` will be available in the sidebar menu. Click this menu item to visit the Pushly configuration page where you can configure the behavior of the plugin.

<figure><img src="/files/whCdUHazthFe6z83UyUl" alt=""><figcaption></figcaption></figure>

The following configuration options are available:

* **SDK Key**: Retrieve the SDK Key from the *Domain Settings* section of the platform and enter it in this text box. Your account manager can also provide you with your SDK Key.
* **Send Notifications for New Posts:** Turn on this switch to allow notifications to be created directly from WordPress when a new post is created. After enabling this switch, a new *Pushly* section will show in the Post Editor sidebar that contains options related to sending notifications.
* **Automatic Sending**: Turn on this switch to automatically check the *Send Notifications* checkbox when creating a new post in Wordpress. When the *Send Notifications* box is checked a notification will be created for the post when it is published.
* **API Key:** Enter the WordPress API Key generated in Step 1 in this text box.
* **Advanced:**
  * **Disable SDK Initialization:** When this box is checked the Pushly SDK **will not** automatically run on your site; Existing subscribers can still receive notifications.

After setting your configuration options Save the form. Once the form has been saved, visitors to your site will begin seeing the notification permission dialog. If you configured notification sending then your WordPress users will also see the notification sending options when creating a WordPress post.

{% hint style="info" %}
If you are not being prompted to opt-in to notifications after completing these steps ensure you have at least 1 active prompt within the `Prompts` section of the Pushly platform.
{% endhint %}

## Troubleshooting

### Service Worker Blocked (403 Forbidden)

Some WordPress hosting environments use NGINX in a way that blocks PHP execution inside the `wp-content/plugins/` directory. When this happens, the Pushly service worker returns a **403 Forbidden** error and push notifications will not work.

#### How to Check

Visit the following URL in your browser (replace `yourdomain.com` with your site's domain):

```
https://yourdomain.com/wp-content/plugins/pushly/assets/js/pushly-sdk-worker.js.php
```

If you see a 403 error or a blank page instead of JavaScript content, your server is blocking PHP execution for this file.

#### Why This Happens

WordPress sites commonly run behind NGINX in several configurations:

* **NGINX as a reverse proxy** in front of Apache — NGINX handles static files and may block PHP in plugin directories
* **NGINX with PHP-FPM** (no Apache) — common on managed hosts like Kinsta, Flywheel, WP Engine, and Cloudways. Security rules often restrict PHP execution to specific paths
* **NGINX caching layers or CDNs** — may block requests to `.php` files in unexpected paths

#### Fix: Add an NGINX Exception

If you have access to your NGINX configuration, add the following inside your site's `server` block:

```nginx
location = /wp-content/plugins/pushly/assets/js/pushly-sdk-worker.js.php {
    include fastcgi_params;
    fastcgi_pass unix:/var/run/php/php-fpm.sock;
    fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
```

The `fastcgi_pass` path may differ depending on your PHP version and host. Common alternatives:

* `unix:/var/run/php/php8.1-fpm.sock`
* `127.0.0.1:9000`

After adding the rule, reload NGINX:

```bash
sudo nginx -t && sudo systemctl reload nginx
```

#### Fix: Contact Your Hosting Provider

If you don't have direct access to NGINX configuration (common with managed WordPress hosts), contact your hosting provider and ask them to:

> Allow PHP execution for the file at `/wp-content/plugins/pushly/assets/js/pushly-sdk-worker.js.php`. This file serves the Pushly push notification service worker and needs to return JavaScript content with the correct headers.

Most managed hosts can add a specific exception for this path without compromising their security rules.

#### Verifying the Fix

After applying the fix, visit the service worker URL again. You should see a single line of JavaScript starting with `importScripts(`. If you see this, the service worker is working correctly.


# Activity Log

The Pushly plugin includes a built-in Activity Log that records every step of the notification send process. When a notification doesn't send as expected, the Activity Log shows exactly what happened and why — making it easy to diagnose issues without contacting support.

### Enabling Debug Logging

1. Navigate to the **Pushly** settings page in your WordPress admin sidebar
2. Open the **Debugging** section at the bottom of the Settings tab
3. Toggle **Debug Logging** to on
4. Click **Save**

<figure><img src="/files/CsXOC1xDOXjFcFPAT9u9" alt=""><figcaption></figcaption></figure>

Once enabled, the plugin will record detailed log entries every time a post is published or updated. An environment snapshot is automatically captured when logging is first enabled, providing useful context about your WordPress and plugin configuration.

{% hint style="info" %}
Log entries are written in a single batch at the end of each request and are automatically cleaned up after 14 days or when the log reaches 5,000 entries.
{% endhint %}

### Viewing the Activity Log

Click the **Activity Log** tab on the Pushly settings page to view recorded log entries. Each entry includes:

* **Timestamp** — when the event occurred, displayed in your local timezone
* **Severity** — the importance level of the event (debug, info, warning, or error)
* **Event Type** — a machine-readable identifier for the event (e.g., `send_flow_start`, `api_response`)
* **Message** — a human-readable description of what happened

Log entries are displayed in reverse chronological order (newest first), with 50 entries per page.

<figure><img src="/files/7Uo0kjvmSGdKvb5tj3lX" alt=""><figcaption></figcaption></figure>

### Exporting the Log for Support

If you need to contact Pushly support about a notification issue:

1. Go to the **Activity Log** tab
2. Click **Download Log** to save all log entries as a text file
3. Attach the downloaded file to your support ticket

The exported file includes all recorded log entries with timestamps, severity levels, and full message details. This gives the support team everything they need to diagnose the issue without requiring access to your WordPress admin.

You can also click **Copy to Clipboard** to copy the entries currently visible on the page.

### Understanding Log Entries

When a post is published, the plugin logs each decision point in the notification send flow. Here are the most common entries you'll see:

| Event Type           | What It Means                                                      |
| -------------------- | ------------------------------------------------------------------ |
| `send_flow_start`    | The notification send process has started for a post               |
| `meta_check`         | Whether the "Send Notification" checkbox was checked               |
| `duplicate_check`    | A notification was already sent for this post                      |
| `notification_build` | The notification payload was assembled                             |
| `api_request`        | The notification was sent to the Pushly API                        |
| `api_response`       | The API responded — check the message for success or error details |

If a notification fails to send, look for entries with **error** severity. The message will include specific details like HTTP status codes and error messages from the API.

### Clearing the Log

Click **Clear Log** to remove all log entries and start fresh. This is useful when you want to isolate a specific issue — clear the log, reproduce the problem, then review or export the new entries.

{% hint style="warning" %}
Clearing the log is permanent and cannot be undone.
{% endhint %}


# Wix Integration Steps

Instructions for installing Pushly when using Wix

If your website utilizes Wix follow these steps for implementation

## Step 1: Install the Service Worker

First, enable Wix Dev Mode and add a file to expose the Service Worker:

1. Open your Wix admin dashboard
2. Click the "Edit Site" button in the top right of the dashboard
3. Click the "Dev Mode" tab at the top of the site and click the “Turn On Dev Mode” button
4. Click the “Public & Backend” `{}` sidebar menu option
5. In the "Backend" section click “Add web module” and enter the filename: `http-functions.js`
6. Replace the entire contents of the `http-functions.js` file with the following code:

```javascript
import { ok } from 'wix-http-functions';

export function get_ServiceWorker(request) {
  let options = {
    "headers": {
      "Content-Type": "application/javascript",
      "Service-Worker-Allowed": "/"
    },
    "body": "importScripts('https://cdn.p-n.io/pushly-sw.min.js' + (self.location || {}).search || '');"
  };
  
  return ok(options);
}
```

After clicking the “Publish” button make sure the service worker is accessible at your site. For example: `https://www.mysite.com/_functions/ServiceWorker`

<figure><img src="/files/KSNZVHNrw0ncSOH8oHCd" alt=""><figcaption></figcaption></figure>

## Step 2: Add the Integration Code Snippet

1. Navigate back to the main Wix Dashboard
2. Click the "Settings" sidebar link and then "Custom code" near the bottom of the page
3. Click the “Add Custom Code” button and place the following code in the code text area. Leave the “All Pages” radio button selected.

{% hint style="warning" %}
Ensure you replace **both** instances of`SDK_KEY` in the snippet with the value provided by your account manager.
{% endhint %}

```
<script src="https://cdn.p-n.io/pushly-sdk.min.js?domain_key=SDK_KEY" async></script>
<script>
  window.PushlySDK = window.PushlySDK || [];
  function pushly() { window.PushlySDK.push(arguments) }
  pushly('load', {
    domainKey: 'SDK_KEY',
    sw: '/_functions/ServiceWorker',
  });
</script>
```

Save the snippet to complete the integration steps.

<figure><img src="/files/41ae30OE0RwZcDR8qbEj" alt=""><figcaption></figcaption></figure>


# Safari

Instructions on how to implement web push on Safari browsers

{% content-ref url="/pages/-LryVbeWwIXXnwDia23f" %}
[Safari on Desktop](/integration/web-push/safari/safari-support)
{% endcontent-ref %}

{% content-ref url="/pages/7W1UedpAqun0nncoSYEl" %}
[Safari on Mobile (iOS / iPadOS)](/integration/web-push/safari/safari-on-mobile-ios-ipados)
{% endcontent-ref %}


# Safari on Desktop

Step-by-step guide to enabling Web Push for Safari web browser on Desktop devices

{% hint style="info" %}
Safari on Mac before version 13 (Ventura) does not implement the Web Push standard for which all other browsers do. Because of this, in order to set up your website to prompt for notification permissions and deliver notifications to these Safari visitors you must perform additional integration steps.
{% endhint %}

## Introduction

Before you start ensure you have the following:

* Have access to a Mac computer
* Have an Apple Developer Account

The below steps walk you through generating the required Web Push security certificates and uploading them to the platform.

{% hint style="warning" %}
You must create a separate web push certificate for **each** site/domain that opts in subscribers. Re-using certificates across multiple domains will result in fewer notifications delivered and higher unsubscribe rates.
{% endhint %}

## Step 1: Create a Certificate Request (CSR)

1. On a Mac computer Open the `Keychain Access` application and click on `Certificates` under the `Category` left navigation section.
2. Within the `Keychain Access` drop down menu, select `Keychain Access` > `Certificate Assistant` > `Request a Certificate from a Certificate Authority`.
3. In the Certificate Information window, enter the following information:
   * In the `User Email Address` field, enter your email address.
   * In the `Common Name` field, create a name for your private key (e.g. Company Name Web Push).
   * The `CA Email Address` field should be left empty.
   * In the `Request` group, select the `Saved to disk` option.
4. Click `Continue` within Keychain Access and save the CSR to disk to complete this step.

## Step 2: Add a Website Push ID

{% hint style="danger" %}
If you are renewing an existing certificate you can skip this step and move to Step 3 since the Website Push ID should already exist within your Apple developer account.
{% endhint %}

1. [Login](https://developer.apple.com/account/ios/certificate/) to your company's Apple Developer Console account.
2. Click on `Certificates, IDs, and Profiles` in the left navigation bar.
3. Choose `Identifiers` in the left navigation bar.
4. In the top-right dropdown choose `Website Push ID` and then click the `+` button next to the `Identifiers` header.
5. Choose `Website Push IDs` from the list of options and press the `Continue` button.
6. Enter a description. Eg: `Pushy Web Push`
7. Enter an identifier. Eg:  `web.com.YOUR_COMPANY_NAME.webpush`
8. Click the `Continue` and then `Register` button.

## Step 3: Generate a Web Push Certificate

* Click on `Certificates` in the left nav and press the `+` sign next to the `Certificates` header..
* Choose `Website Push ID Certificate` from the list of Production certificate options and then click `Continue`.
* Select the Website Push ID that was created in the previous step and then click `Continue`.
* Click the `Choose File` button, browse for the CSR file that we created in the first step, and click `Continue`.
* The next screen informs you that your certificate is ready. Click `Download` to store the certificate on your computer.

## Step 4: Generate the .p12 Certificate

* Navigate to the certificate you saved on your computer and double click it to install it to the `Keychain Access` application.
* Open the `Keychain Access` application and click on `Certificates` under the `Category` left navigation section.
* Right click on the installed Website Push ID certificate and choose `Export`.
* In the save dialog, choose a filename (or leave the default), choose the `.p12` file format, and click the `Save` button.
* You will be prompted to enter a password - leave both boxes empty and click `OK`.
* Click `Allow` to save the p12 certificate to your computer.

## Step 5: Upload the Certificate via the Platform

* Log in to the platform.
* In the left navigation choose `Settings` and then `Overview`.&#x20;
* Ensure all subdomains that users will be prompted on are in the `SDK Allowed Domains` list.
* Scroll down to the `Send Integrations` section and click the `Edit` button.&#x20;
* Enable the `WEB` switch and then enter the `Website Push ID` you created in Step 2. (Eg: "web.com.example")
* Upload the `p12` file from your computer and only enable the `Pass Phrase` switch if you set a password when creating the certificate.
* Click the `Submit` button at the bottom of the page to finish setting up Safari Support.
  * Note: These changes may take up to 1 hour to take effect.

<figure><img src="/files/X2yuxiNj3cUNIBLeQIzV" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
NOTE: The Safari Certificate will need to be updated on an annual basis. Your Pushly Account Manager will reach out when the Safari Certificate has expired and needs to be updated.
{% endhint %}


# Safari on Mobile (iOS / iPadOS)

Step-by-step guide to enabling Web Push for Safari web browser on Mobile iOS/iPadOS devices

## Requirements

In order for your website to support Web Push on iOS devices it must be installable as a Progressive Web App (PWA).

Once your site is installable as a PWA, any visitor using iOS 16.4+ that adds the site to their home screen is eligible to subscribe to and receive web push notifications.

Ensure you have also completed the [<mark style="color:blue;">Web Push Implementation Steps</mark>](/integration/web-push).

{% hint style="info" %}
Safari Web Push requires a 2-step permission prompt. If you do not have at least one 2-step prompt enabled in the platform visitors will not be prompted to subscribe to notifications.
{% endhint %}

## Step 1: Create & Expose the Web App Manifest

The web app manifest is a JSON file that tells browsers how you want your site to display as an app in the visitor's operating system.

This file is typically hosted in the public root folder and linked to on all HTML pages. Its recommended extension is `.webmanifest` but any filename will work as long as it's delivered with the `application/manifest+json` content type or any other valid JSON content type such as `text/json`. Historically sites have used `manifest.json` as the filename.

Create a file, for example, `site.webmanifest` with the following content, replacing the example `name` and icon `src` values with relevant values for your site:

```json
{
    "$schema": "https://json.schemastore.org/web-manifest-combined.json",
    "name": "Example",
    "display": "standalone",
    "icons": [
        {
            "src": "https://example.com/images/logo-512x512.png",
            "sizes": "512x512",
            "type": "image/png"
        }
    ]
}
```

{% hint style="info" %}
For a full listing of fields that can be provided in the manifest see the [<mark style="color:blue;">MDN article on Web App Manifests</mark>](https://developer.mozilla.org/en-US/docs/Web/Manifest).
{% endhint %}

The manifest is typically served from the public root of your site; for example: `https://www.example.com/site.webmanifest` but it is also possible to serve the manifest from a subdirectory. Note that serving from a subdirectory may impact your ability to implement additional PWA features in the future.

In addition to making the manifest publicly available, you must also add a line to each page of your site that informs the browser of the location of your manifest file. For example, place the following line of code within the `<head>` section on each page of your site:

```
<link rel="manifest" href="/site.webmanifest">
```

Once the file is placed and the above line added to pages on your site you can test the implementation on an iOS device running iOS version 16.4+.

## Step 2: Test

Visit your site in Safari on a device running iOS/iPadOS version 16.4+, click on the `Share` icon, and tap `Add to Home Screen`. Navigate to your home screen, find the icon for your site, and open it. If the manifest was properly implemented, your site should open as a standalone app without showing any of the browser controls.

## Step 3 (Optional): Direct Visitors to Add Your Website to Their Home Screen

Since Web Push on Safari requires the visitor to install your site as a PWA you may want to add elements to your site that direct the visitor to do so.&#x20;

You may implement this as a button on your site, a popup with instructions that is displayed directly above the share button, a banner, or any other element that will help direct the visitor to perform the necessary steps to add your site as a PWA.&#x20;

There are several open source projects that can help you implement this functionality or if you use Wordpress many PWA plugins provide options for this.&#x20;

<figure><img src="/files/aeg2phIFIsH9pJKX2Zh0" alt=""><figcaption></figcaption></figure>

## FAQ / Debugging

#### Site Opening in Browser Instead of Standalone

If your site is still opening in the browser instead of as a standalone app open your site's debug console and look for a message saying that the browser detected no manifest. If you see this message check to make sure you can load the manifest directly in the browser and that its content type is correctly set.

#### No 2-Step Prompt Being Shown

If your app is opening in PWA/standalone mode but you are not seeing the permission prompt make sure that:

1. You have at least one 2-step prompt enabled within the platform Prompts settings
2. You are on an iOS device that supports web push (iOS 16.4+)


# SDK

{% content-ref url="/pages/-LryWYJhQyAzVKyVsNyy" %}
[SDK Methods](/integration/web-push/sdk/methods)
{% endcontent-ref %}

{% content-ref url="/pages/-LryYDJofNV3scTR3vaK" %}
[SDK Events](/integration/web-push/sdk/events)
{% endcontent-ref %}

{% content-ref url="/pages/-LryX963PGlAOirifj8N" %}
[Customizing Prompt CSS](/integration/web-push/sdk/customizing-css)
{% endcontent-ref %}

{% content-ref url="/pages/-LryWMkdOMNL0BWoVyK-" %}
[AMP Support](/integration/web-push/sdk/amp-support)
{% endcontent-ref %}


# SDK Methods

## Manually Triggering the Opt-In Prompt

You may choose to disable automatic triggering of the opt-in prompt via the platform. In this scenario you would choose to trigger the prompt based on your own criteria (eg: after a visitor clicks a specific link, visits a specific page, etc).

Triggering the prompt is as easy as calling the `show_prompt` event when a visitor performs the desired action.

```javascript
pushly('show_prompt', { checkEligibility: true })
```

If you want the prompt to show regardless of current subscription status or frequency capping (i.e., manually displaying a bell or custom prompt) this can be simplified to the following code.

```javascript
pushly('show_prompt')
```

## Adding Attributes to a Subscriber's Profile

You can add attributes to a subscriber's profile and later perform segmentation based on those attributes. For example, you may want to tag visitors who are interested in a specific type of news (eg: politics, sports) so that you can target them with specific notifications.

```javascript
pushly('profile', {
  'is_paying_subscriber': true,
  'interests': ['poltics', 'news']
});
```

{% hint style="info" %}
Property values can be scalar or an array of scalar values.
{% endhint %}

If you're storing the value of a property as an array you can use the `profile_append` and `profile_remove` methods to add or remove values.

```javascript
// add 'sports' to the subscriber's interests
pushly('profile_append', {
  'interests': ['sports']
});

// remove 'fashion' and 'news' from the subscriber's interests
pushly('profile_remove', {
  'interests': ['fashion', 'news']
});
```

{% hint style="warning" %}
Properties using `profile_append` will only store the most recent 20 values provided.
{% endhint %}

## Adding Page Keywords / Tags to a Subscriber's Profile

You can add page keywords / tags to a subscribers profile for every page they visit. These tags can then be used in segmentation to create cohorts of subscribers that have visited tags based on number of times and recency. For example: "Subscribers who have visited a page tagged with "Astrology" at least 4 times in the last 30 days".

```javascript
pushly('page_tag_visit', ['Tag1', 'Tag2', 'Tag3']);
```

## Retrieving the Subscriber's User ID

{% hint style="warning" %}
This method is only available after the SDK is fully loaded. This SDK method should be wrapped inside of the `on_ready` event.
{% endhint %}

Run the following code to the get the user's Pushly ID. This is a transient ID that may change often so it should not be used for long-term visitor identification.

```javascript
pushly('on_ready', function() {
    PushlySDK.getUser().getId();
});
```

## Setting the Subscriber's External ID

Providing a unique User ID for your subscriber allows you to later interact with that subscriber via the API.

There are two ways to provide the external ID:

#### On SDK Init

Preferably you may provide the external ID when the SDK is initialized. This requires adding one additional line of code to the `load` function. Note the new `externalId` line in the below script:

```javascript
<script src="https://cdn.p-n.io/pushly-sdk.min.js?domain_key=DOMAIN_KEY" async></script>
<script>
  var PushlySDK = window.PushlySDK || [];
  function pushly() { PushlySDK.push(arguments) }
  pushly('load', {
    domainKey: 'DOMAIN_KEY',
    externalId: 'REPLACE_WITH_USER_ID'
  });
</script>
```

#### Via a separate method call

Alternatively you may provide the external ID any time after the SDK has loaded via the following code:

```javascript
pushly('external_id', 'h7bwKwuE3');
```

Once a subscriber's external ID has been set you may work with our team to set up bi-directional automated data syncs. The following use cases are common ways that publishers use the external ID via server-to-server requests:

* Sending notifications to individual subscribers programmatically
* Processing unsubscribe requests
* Adding and removing profile attributes and events to subscribers

## Removing a Subscriber's External ID

If you want to disassociate an External ID from a subscriber run the following javascript:

```javascript
pushly('deregister_external_id');
```

## Determining if a Visitor is Already Subscribed

{% hint style="warning" %}
This method is only available after the SDK is fully loaded. This SDK method should be wrapped inside of the `on_ready` event.
{% endhint %}

If you need to know if a visitor is already subscribed to push notifications the following javascript can be used:

```javascript
pushly('on_ready', function() {
    await PushlySDK.isUserSubscribed();
});
```

The result of this call will be a boolean `true` or `false` representing the subscription status of the visitor.

{% hint style="info" %}
Note the use of the `on_ready` event to ensure that the SDK is ready for interaction.
{% endhint %}

## Determining if a Visitor is Eligible to Prompt

{% hint style="warning" %}
This method is only available after the SDK is fully loaded. This SDK method should be wrapped inside of the `on_ready` event.
{% endhint %}

If you need to determine if the visitor meets all requirements to be prompted for push permission run the following JavaScript:

```javascript
pushly('on_ready', function() {
    await PushlySDK.isUserEligibleToPrompt();
});
```

The result of this call will be a boolean `true` or `false` representing if the visitor is eligible to be shown a permission dialog.

{% hint style="info" %}
Note the use of the `on_ready` event to ensure that the SDK is ready for interaction.
{% endhint %}

## Suppressing & Allowing Prompts

If you use automatic prompting but need to temporarily block prompts at runtime — for example while a checkout, paywall, or other modal flow is active — you can suppress prompting on demand and re-enable it afterward. Automatic prompting stays configured as normal; suppression only pauses it at runtime.

Suppress prompting when your flow begins (eg: a checkout modal opens):

```javascript
pushly('suppress_prompts')
```

Allow prompting again when the flow ends (eg: the modal closes or completes):

```javascript
pushly('allow_prompts')
```

To check whether prompting is currently suppressed:

```javascript
pushly('get_prompting_suppressed_state', (isSuppressed) => { ... })
```

While suppressed, no prompt is shown — neither automatic prompts nor a manual `show_prompt` call. A prompt already visible on screen is not hidden; suppression only prevents new prompts from appearing.

{% hint style="info" %}
The suppressed state is held in memory. It persists across single-page-app (SPA) navigation and is cleared automatically on a full page reload, so a missed `allow_prompts` call cannot leave a visitor permanently unable to be prompted.
{% endhint %}

When you call `allow_prompts`, if the prompt's configured display delay has already elapsed the eligible prompt appears shortly after; otherwise it appears once the delay is met. Time spent suppressed still counts toward the delay.

{% hint style="warning" %}
`suppress_prompts` / `allow_prompts` are not the same as `pause_notifications` / `resume_notifications`. Prompt suppression controls whether the opt-in **prompt** is shown; pausing notifications is a soft unsubscribe that controls push **delivery** for a visitor who is already subscribed.
{% endhint %}

## Pausing / Resuming a User's Notifications (Soft Unsubscribe)

A user's notifications can be paused by calling the following method:

```javascript
pushly('pause_notifications')
```

If the user's notifications should be resumed call the following method:

```javascript
pushly('resume_notifications')
```

To check if the user's notifications are currently paused:

```javascript
pushly('get_notifications_paused_state', (isPaused) => { ... })
```

## Permanently Delete a User

If the user should be excluded from all notifications and tracking run the following method:

```javascript
pushly('request_user_deletion');
```

{% hint style="warning" %}
This method works only as a soft unsubscribe. If the subscriber clears their cookies they may get re-opted into notifications.
{% endhint %}


# SDK Events

The SDK emits events for all important events. These events can be listened for in order to execute custom logic like tracking, visual changes, or any other action that should occur after an event.

| Event                      | Emit Trigger                                                               |
| -------------------------- | -------------------------------------------------------------------------- |
| on\_loaded                 | The SDK has been successfully loaded in to the visitor's browser           |
| on\_load\_error            | There was an unexpected error while attempting to load the SDK             |
| on\_web\_push\_supported   | The visitor's web browser supports web push                                |
| on\_web\_push\_unsupported | The visitor's web browser does not support web push                        |
| on\_ready                  | The SDK has gather all required configuration and is ready for interaction |
| on\_prompt\_eligible       | The visitor is eligible to be shown the prompt                             |
| on\_prompt\_ineligible     | The visitor should not be shown the prompt                                 |
| on\_prompt\_shown          | After a prompt is successfully shown to the visitor                        |
| on\_prompt\_dismissed      | After a visitor dismisses the prompt                                       |
| on\_prompt\_allowed        | After a visitor allows the prompt                                          |
| on\_permission\_shown      | After the visitor is shown the browser's native permission dialog          |
| on\_permission\_allowed    | After the visitor allows the browser's native permission dialog            |
| on\_permission\_denied     | After the visitor declines the browser's native permission dialog          |
| on\_permission\_dismissed  | After the visitor dismisses the browser's native permission dialog         |

## Example: on\_ready

For example, to run your own custom code once the SDK is ready for interaction:

```javascript
pushly('on_ready', function() {
  console.log('example');
});
```


# Customizing Prompt CSS

You may override any of the CSS for the prompt styles available within the platform using the following CSS classes.

| Property                                                                         | Description                                                                                                                                                                                                        |
| -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| .pushly-prompt-slide                                                             | Controls the general slide prompt border, shadow, size, and positioning.                                                                                                                                           |
| .pushly-prompt-slide .pushly-prompt-icon                                         | Defines max-width, height, and positioning for the prompt icon container: position top when using stacked layout, position left when using standard layout.                                                        |
| .pushly-prompt-slide .pushly-prompt-icon img                                     | Defines style for the prompt icon.                                                                                                                                                                                 |
| .pushly-prompt-slide .pushly-prompt-content                                      | Controls the container for the prompt message and actions. Defines max-width, height, and positioning for the prompt content: position lower when using stacked layout, position right when using standard layout. |
| .pushly-prompt-slide .pushly-prompt-content .pushly-prompt-message               | Controls the container for the prompt message.                                                                                                                                                                     |
| pushly-prompt-slide .pushly-prompt-content .pushly-prompt-message-headline       | Defines the style for the prompt headline.                                                                                                                                                                         |
| .pushly-prompt-slide .pushly-prompt-content .pushly-prompt-message-subheadline   | Defines the style for the prompt sub-headline.                                                                                                                                                                     |
| .pushly-prompt-slide .pushly-prompt-content .pushly-prompt-actions               | Defines the style for the prompt sub-headline.                                                                                                                                                                     |
| .pushly-prompt-slide .pushly-prompt-content button.pushly-prompt-buttons-dismiss | Defines style for the prompt dismiss button.                                                                                                                                                                       |
| .pushly-prompt-slide .pushly-prompt-content button.pushly-prompt-buttons-allow   | Defines style for the prompt allow button.                                                                                                                                                                         |


# AMP Support

You may also prompt your visitors for Web Push permissions via AMP pages.

{% hint style="warning" %}
Web push AMP support is not recommended due to poor user experience and low percentage of opt-ins.
{% endhint %}

## Integration Steps

### Step 1: Download & Place Required Files

Download the following files:

* [pushly-amp-helper-frame.html](https://cdn.p-n.io/amp/examples/pushly-amp-helper-frame.html)
* [pushly-permission-dialog.html](https://s3.amazonaws.com/cdn.p-n.io/amp/examples/pushly-amp-permission-dialog.html)
* [pushly-amp-styles.html](https://cdn.p-n.io/amp/examples/pushly-amp-styles.html)

In the `pushly-amp-helper-frame.html` and `pushly-amp-permission-dialog.html` files replace the `REPLACE_WITH_DOMAIN_KEY` placeholder text with the Domain Key from the platform.

Place all 3 files in a publicly accessible directory on the domain for which you are going to opt-in visitors.

### Step 2: Include the AMP Project Javascript

Include the following script tag on your AMP pages in the HTML head section. Ensure that the AMP page does not include the main Pushly SDK snippet.

```
<script async custom-element="amp-web-push" src="https://cdn.ampproject.org/v0/amp-web-push-0.1.js"></script>
```

### Step 3: Include the \`amp-web-push\` component

Include the following \`amp-web-push\` tag within the HTML body on your AMP pages.

{% hint style="warning" %}
Ensure the URLs to each of the files placed in Step 1 are accurate in the final snippet.&#x20;
{% endhint %}

```
<amp-web-push
   id="amp-web-push"
   layout="nodisplay"
   helper-iframe-url="https://www.example.com/pushly-amp-helper-frame.html"
   permission-dialog-url="https://www.example.com/pushly-amp-permission-dialog.html"
   service-worker-url="https://www.example.com/pushly-sdk-worker.js"
></amp-web-push>
```

### �Step 4: Add Widgets

Add the following code blocks to the page wherever you want the subscribe and unsubscribe widgets to show.

{% hint style="info" %}
We recommend placing these widgets between article paragraphs and/or at the bottom of each article.
{% endhint %}

#### Subscribe Button

```
<amp-web-push-widget visibility="unsubscribed" layout="fixed" width="245" height="45">
  <button class="subscribe" on="tap:amp-web-push.subscribe">
    <amp-img
      class="subscribe-icon"
      width="24"
      height="24"
      layout="fixed"
      src="data:image/svg+xml;base64,PHN2ZyBjbGFzcz0ic3Vic2NyaWJlLWljb24iIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXRoIGQ9Ik0xMS44NCAxOS44ODdIMS4yMnMtLjk0Ny0uMDk0LS45NDctLjk5NWMwLS45LjgwNi0uOTQ4LjgwNi0uOTQ4czMuMTctMS41MTcgMy4xNy0yLjYwOGMwLTEuMDktLjUyLTEuODUtLjUyLTYuMzA1czIuODUtNy44NyA2LjI2LTcuODdjMCAwIC40NzMtMS4xMzQgMS44NS0xLjEzNCAxLjMyNSAwIDEuOCAxLjEzNyAxLjggMS4xMzcgMy40MTMgMCA2LjI2IDMuNDE4IDYuMjYgNy44NyAwIDQuNDYtLjQ3NyA1LjIyLS40NzcgNi4zMSAwIDEuMDkgMy4xNzYgMi42MDcgMy4xNzYgMi42MDdzLjgxLjA0Ni44MS45NDdjMCAuODUzLS45OTYuOTk1LS45OTYuOTk1SDExLjg0ek04IDIwLjk3N2g3LjExcy0uNDkgMi45ODctMy41MyAyLjk4N1M4IDIwLjk3OCA4IDIwLjk3OHoiIGZpbGw9IiNGRkYiLz48L3N2Zz4=">
    </amp-img>
    Subscribe to updates
  </button>
</amp-web-push-widget>
```

#### �Unsubscribe Button (Optional)

```
<amp-web-push-widget visibility="subscribed" layout="fixed" width="250" height="80">
  <button class="unsubscribe" on="tap:amp-web-push.unsubscribe">Unsubscribe from updates</button>
</amp-web-push-widget>
```

## Customization

You may customize the CSS and widgets as long as you ensure the `amp-web-push-widget` and `subscribe/unsubscribe` buttons exist.


# Commerce & Catalog Item Events

How to set up activity tracking and commerce interactions via the SDK.

This guide explains how to send commerce and catalog-related user interactions to Pushly using the Web SDK. These events power features like abandoned cart notifications, saved item reminders, revenue attribution, and catalog-driven recommendation campaigns.

The Pushly Web SDK must be loaded and initialized before sending any of the events described below.

{% hint style="info" %}
The following steps assume that you are providing an item catalog/feed to our team. Please contact your account manager for more information on this process.
{% endhint %}

### Supported Interaction Types

Pushly supports the following commerce and catalog interactions:

* **view\_item** – A user views an item detail page
* **save\_item** – A user saves or favorites an item
* **unsave\_item** – A user removes saved or unfavorites an item
* **complete\_item** – A user completes an item
* **uncomplete\_item** – A user removes completed item
* **rate\_item** – A user rates an item
* **unrate\_item** – A user removes rating from an item
* **add\_to\_cart** – A user adds an item to their cart
* **update\_cart** – A user modifies cart contents or quantities
* **purchase** – A user completes a transaction

Each interaction can reference items using one of three identifier types, depending on your catalog configuration.

### Item Identifier Types

Every interaction includes one of the following top‑level arrays:

* **products** – Standard e‑commerce product catalog items
* **events** – Event‑based catalog items (e.g., ticketed content)
* **recipes** – Recipe or content‑based catalog items

Only one identifier type should be included per interaction call.

Each item requires an `id`. Quantity is optional unless otherwise noted.

{% hint style="danger" %}
All of the following code snippets must be ran **after** the SDK has been initialized.
{% endhint %}

### View Item

Send this event when a user views an item detail page.

{% tabs %}
{% tab title="Products" %}

```
pushly('view_item', {
    "products": [
        {
            "id": "PRODUCT_ID"
        }
    ]
});
```

{% endtab %}

{% tab title="Events" %}

```
pushly('view_item', {
    "events": [
        {
            "id": "EVENT_ID"
        }
    ]
});
```

{% endtab %}

{% tab title="Recipes" %}

```
pushly('view_item', {
    "recipes": [
        {
            "id": "RECIPE_ID"
        }
    ]
});
```

{% endtab %}
{% endtabs %}

### Save Item

Use this event when a user saves, favorites, or bookmarks an item. This enables saved‑item reminder campaigns.

{% tabs %}
{% tab title="Products" %}

```
pushly('save_item', {
    "products": [
        {
            "id": "PRODUCT_ID"
        }
    ]
});
```

{% endtab %}

{% tab title="Events" %}

```
pushly('save_item', {
    "events": [
        {
            "id": "EVENT_ID"
        }
    ]
});
```

{% endtab %}

{% tab title="Recipes" %}

```
pushly('save_item', {
    "recipes": [
        {
            "id": "RECIPE_ID"
        }
    ]
});
```

{% endtab %}
{% endtabs %}

### Unsave Item

Use this event when a user removes an item from the saved list, favorites, or bookmarks.&#x20;

{% tabs %}
{% tab title="Products" %}

```
pushly('unsave_item', {
    "products": [
        {
            "id": "PRODUCT_ID"
        }
    ]
});
```

{% endtab %}

{% tab title="Events" %}

```
pushly('unsave_item', {
    "events": [
        {
            "id": "EVENT_ID"
        }
    ]
});
```

{% endtab %}

{% tab title="Recipes" %}

```
pushly('unsave_item', {
    "recipes": [
        {
            "id": "RECIPE_ID"
        }
    ]
});
```

{% endtab %}
{% endtabs %}

### Complete Item

A user marks an item as completed after finishing its intended experience.

{% tabs %}
{% tab title="Products" %}

```
pushly('complete_item', {
    "products": [
        {
            "id": "PRODUCT_ID"
        }
    ]
});
```

{% endtab %}

{% tab title="Events" %}

```
pushly('complete_item', {
    "events": [
        {
            "id": "EVENT_ID"
        }
    ]
});
```

{% endtab %}

{% tab title="Recipes" %}

```
pushly('complete_item', {
    "recipes": [
        {
            "id": "RECIPE_ID"
        }
    ]
});
```

{% endtab %}
{% endtabs %}

### Uncomplete Item

Use this event to revert the completion of an item.

{% tabs %}
{% tab title="Products" %}

```
pushly('uncomplete_item', {
    "products": [
        {
            "id": "PRODUCT_ID"
        }
    ]
});
```

{% endtab %}

{% tab title="Events" %}

```
pushly('uncomplete_item', {
    "events": [
        {
            "id": "EVENT_ID"
        }
    ]
});
```

{% endtab %}

{% tab title="Recipes" %}

```
pushly('uncomplete_item', {
    "recipes": [
        {
            "id": "RECIPE_ID"
        }
    ]
});
```

{% endtab %}
{% endtabs %}

### Rate Item

The `rate_item` event includes an optional `rating` property.\
If provided, `rating` must be a numeric value between **0 and 100** inclusive, with **up to 1 decimal place**. Integer values are valid.\
If `rating` is omitted, the event is valid.\
Invalid `rating` values causes the SDK to **reject the event and not send it**.

{% tabs %}
{% tab title="Products" %}

```
pushly('rate_item', {
    "products": [
        {
            "id": "PRODUCT_ID",
            "rating": 5,
        }
    ]
});
```

{% endtab %}

{% tab title="Events" %}

```
pushly('rate_item', {
    "events": [
        {
            "id": "EVENT_ID",
            "rating": 5,
        }
    ]
});
```

{% endtab %}

{% tab title="Recipes" %}

```
pushly('rate_item', {
    "recipes": [
        {
            "id": "RECIPE_ID",
            "rating": 5,
        }
    ]
});
```

{% endtab %}
{% endtabs %}

### Unrate Item

Use this event when a user removes the rating from an item.

{% tabs %}
{% tab title="Products" %}

```
pushly('unrate_item', {
    "products": [
        {
            "id": "PRODUCT_ID"
        }
    ]
});
```

{% endtab %}

{% tab title="Events" %}

```
pushly('unrate_item', {
    "events": [
        {
            "id": "EVENT_ID"
        }
    ]
});
```

{% endtab %}

{% tab title="Recipes" %}

```
pushly('unrate_item', {
    "recipes": [
        {
            "id": "RECIPE_ID"
        }
    ]
});
```

{% endtab %}
{% endtabs %}

### Add To Cart

Send this event whenever a user adds an item to their cart. Pushly will accumulate cart state across multiple calls.

{% tabs %}
{% tab title="Products" %}

```
pushly('add_to_cart', {
    "products": [
        {
            "id": "PRODUCT_ID",
            "quantity": 1
        }
    ]
});
```

{% endtab %}

{% tab title="Events" %}

```
pushly('add_to_cart', {
    "events": [
        {
            "id": "EVENT_ID",
            "quantity": 1
        }
    ]
});
```

{% endtab %}
{% endtabs %}

You may call `add_to_cart` as many times as necessary to keep track of all items in a user's cart. Abandoned Cart notifications may be sent for any item in a customer's cart that has not been purchased.

After a purchase is made the user's cart will be emptied and no notifications for a purchased item will be sent to the user.&#x20;

### Update Cart

Use this event when a user changes their cart without completing a purchase, such as adjusting quantities or removing items.

To remove an item from the cart call the `update_cart` method with the full current cart information (omitting the removed item):

{% tabs %}
{% tab title="Products" %}

```
pushly('update_cart', {
    "products": [
        {
            "id": "PRODUCT_ID",
            "quantity": 3
        }
    ]
});
```

{% endtab %}

{% tab title="Events" %}

```
pushly('update_cart', {
    "events": [
        {
            "id": "EVENT_ID",
            "quantity": 3
        }
    ]
});
```

{% endtab %}
{% endtabs %}

Or if the cart has been completely emptied provide an empty array:

{% tabs %}
{% tab title="Products" %}

```
pushly('update_cart', {
    "products": []
});
```

{% endtab %}

{% tab title="Events" %}

```
pushly('update_cart', {
    "events": []
});
```

{% endtab %}
{% endtabs %}

### Purchase

Send this event after a successful checkout. This clears abandoned cart state and enables revenue attribution.

{% tabs %}
{% tab title="Products" %}

```
pushly('purchase', {
    "products": [
        {
            "id": "PRODUCT_ID",
            "quantity": 1
        }
    ],
    "price_value": "344.33",
    "purchase_id": "ABC123"
});
```

{% endtab %}

{% tab title="Events" %}

```
pushly('purchase', {
    "events": [
        {
            "id": "EVENT_ID",
            "quantity": 1
        }
    ],
    "price_value": "344.33"
   "purchase_id": "ABC123"
});
```

{% endtab %}
{% endtabs %}

#### Purchase Fields

* **price\_value** – Total purchase amount, in the currency configured for the domain
* **purchase\_id** – Unique order identifier

If a `purchase` event is sent without item data, Pushly will still clear the user’s cart state.

#### Currency handling

`price_value` must be provided in the currency configured for the domain in the platform’s domain settings.

Use a period (`.`) as the decimal separator for all currencies, regardless of locale. **Do not use commas.**

Examples:

* USD: `"344.33"`
* JPY: `"5000"`

### Required Fields Summary

* `id` is required for all items
* `quantity` is required only when tracking cart or purchase quantities
* `rating` is required only when rating an item
* Only one of `products`, `events`, or `recipes` should be included per call

### Implementation Best Practices

Initialize the SDK early in the page lifecycle to avoid missed events.

Use consistent item IDs across all interaction types so Pushly can correctly associate behavior.

Trigger `purchase` events from a reliable confirmation step whenever possible to avoid false abandoned cart notifications.

##


# Activity Tracking

{% hint style="danger" %}

## Not Required for Most Partners

This feature is only needed for partners who either:

* use SRI (Subresource Integrity) and cannot allow the Pushly loader to fetch updated SDK assets automatically, or
* self-host the Pushly SDK (serve it from their own servers/CDN)
  {% endhint %}

### Overview

The Pushly SDK supports sending “content context” signals from your site to Pushly. These signals let Pushly build cohorts based on what a subscriber has viewed, how often they viewed it, and how recently.

This powers segmentation like:

* Subscribers who visited any page tagged “Astrology” at least 4 times in the last 30 days
* Subscribers who visited content tagged “Elections” in the last 7 days
* Subscribers who visited multiple categories (ex: “sports” AND “politics”) over the last 14 days

These tags are defined by you at runtime and should map cleanly to your content taxonomy (categories, sections, keywords, authors, topics, etc.).

### Prerequisites

1. The standard Pushly integration snippet must already be installed and running on the page.
2. `window.PushlySDK` must be available before you call these methods.

If you call before the SDK is ready, queueing still works (see usage below), but you must ensure the integration snippet has executed first.

### When to call

Call this after you’ve determined the page’s tags (usually once per page view):

* Traditional multi-page sites: call once on page load after tags are known.
* SPAs / client-side routing: call on every route change when the viewed content changes.

Avoid calling repeatedly for the same view (ex: on scroll, on timers, or in re-render loops).

***

### Method: `page_tag_visit`

Records that the current subscriber viewed a page associated with one or more tags.

#### Signature

```js
window.PushlySDK.push(['page_tag_visit', [tags]]);
```

#### Parameters

* `tags` (Array of strings) – required\
  A list of 1+ tags to associate with this page view.

#### Tag rules / recommendations

* Use stable, human-readable slugs (recommended): `["astrology", "sports", "politics"]`
* Keep tags reasonably short (avoid dumping entire keyword lists)
* Maximum of 20 tags per page view. Any tags beyond the first 20 will be ignored.
* Do not include PII (no emails, names of private individuals, user IDs, etc.)

### Examples

#### Basic example

```js
window.PushlySDK.push(['page_tag_visit', ['sports', 'politics', 'custom_tag_1']]);
```

#### Example with runtime tags from the page

```js
var tags = window.pageTags || []; // however your site exposes tags
if (tags.length) {
  window.PushlySDK.push(['page_tag_visit', tags]);
}
```

#### SPA example (route changes)

```js
function trackCurrentRouteTags() {
  var tags = getTagsForCurrentRoute(); // your implementation
  if (tags && tags.length) {
    window.PushlySDK.push(['page_tag_visit', tags]);
  }
}

// Call on initial load
trackCurrentRouteTags();

// Call on route changes
window.addEventListener('popstate', trackCurrentRouteTags);
```

(Use your router’s hook/event if you have one; `popstate` is just the generic baseline.)

### Notes and common pitfalls

* Order doesn’t matter: `['sports','politics']` is the same as `['politics','sports']`
* Dedupe your tags before sending if your CMS can emit duplicates.
* Don’t call it before the Pushly snippet runs. If you inline this in the `<head>`, it can fire too early. Place it after the Pushly snippet or after your tag data is available.

***

### Troubleshooting

If cohorts don’t populate as expected:

1. Confirm the Pushly base snippet is present and loading successfully (no blocked requests, no JS errors).
2. Confirm `window.PushlySDK` exists and that calls are being queued/executed.
3. Confirm the tags being passed are strings (not objects) and look like what you expect.
4. Confirm you’re sending tags on every relevant page view (especially for SPAs).


# Delivery & Security (SRI)

Pushly supports loading a pinned, immutable version of the Web SDK using Subresource Integrity (SRI).

This option is intended for customers with strict security requirements, such as enforced CSP policies. Most customers should continue using the Latest SDK.

***

#### When should I use SRI?

Use SRI only if you require:

* Cryptographic verification of third-party scripts
* Enforcement of `require-sri-for script` in Content Security Policy
* Explicit control over when SDK updates occur

If you do not have these requirements, use the [Latest SDK](/integration/web-push/quick-start) instead.

***

#### Pinned SDK snippet example (SRI)

```html
<script
  src="https://cdn.p-n.io/sdk/web/pushly@3.1.0.min.js"
  integrity="sha384-REPLACE_WITH_HASH"
  crossorigin="anonymous"
  async
  data-domain-key="DOMAIN_KEY">
</script>
```

This loads a specific SDK version that will never change.

***

#### Get the integrity hash

The integrity hash for each SDK release is published in a release manifest.

For version 3.1.0:

```
https://cdn.p-n.io/sdk/web/releases/3.1.0/manifest.json
```

Look for the `files[].integrity` field and copy its value into the `integrity` attribute.

***

#### Upgrading a pinned SDK

Pinned SDKs do not auto-update.

To upgrade:

1. Update the SDK version in the script `src`
2. Update the corresponding `integrity` hash
3. Deploy the updated snippet

```diff
- src="https://cdn.p-n.io/sdk/web/pushly@3.1.0.min.js"
- integrity="sha384-OLD_HASH"
+ src="https://cdn.p-n.io/sdk/web/pushly@3.2.0.min.js"
+ integrity="sha384-NEW_HASH"
```

If you do not update the snippet, your site will continue running the older SDK indefinitely.


# Web SDK Changelog

This changelog highlights customer-visible changes to the Pushly Web SDK.

{% updates format="full" %}
{% update date="2026-08-03" %}

## v3.4.1

#### Fixed

* Resolves issue with evaluating prompt conditions with anchored regexes.
  {% endupdate %}

{% update date="2026-07-22" %}

## v3.4.0

#### Added

* Added ability to [Suppress](/integration/web-push/sdk/methods#suppressing-and-allowing-prompts-dynamic-blocking) opt-in prompt/dialog during page load.
  {% endupdate %}

{% update date="2026-07-15" %}

## v3.3.1

#### Fixed

* SDK will properly emit page URLs for debug events.
  {% endupdate %}

{% update date="2026-07-14" %}

## v3.3.0

#### Added

* All SDK events now include an ephemeral session identifier.
  {% endupdate %}

{% update date="2026-06-15" %}

## v3.2.4

#### Fixed

* On event-only domains (e.g. a separate checkout or cart subdomain), subscription-gated events such as add\_to\_cart could be silently dropped even for users with an active subscription on the primary domain.
  {% endupdate %}

{% update date="2026-06-10" %}

## v3.2.3

#### Fixed

* Two-step prompts were not shown to visitors in iOS standalone (PWA) mode due to incorrect Safari detection when window\.safari is absent in that context.
  {% endupdate %}

{% update date="2026-06-08" %}

## v3.2.2

#### Fixed

* Stale pre-subscription events that had accumulated in the queue no longer fire in a burst at subscribe time. Events like page views and tag visits are now discarded if they predate the subscription rather than sending with skewed timestamps.
* A race condition between permission observation and the prompt grant flow that could emit a duplicate subscribed event has been resolved.

## v3.2.1

#### Fixed

* Allow and Dismiss buttons on Slide prompts now properly respond to user interaction.
  {% endupdate %}

{% update date="2026-06-08" %}

## v3.2.0

#### Added

* Service-worker push handling is hardened against malformed payloads, with clearer diagnostics.
* Prompt outcomes now flush when a visitor leaves the page, so they're no longer lost when someone leaves immediately after responding to a prompt.
* Page views are now event-driven with live permission-change detection.

#### Fixed

* Eliminated intermittent duplicate "This site has been updated in the background" notification that could appear on Chrome for macOS.
* Duplicate service-worker registrations and duplicate subscriptions that could orphan a device's live push endpoint are now prevented and cleaned up.
  {% endupdate %}

{% update date="2026-03-18" %}

## v3.1.1

#### Fixed

* Sanitize multiple leading slashes when provided in `sw` and `swScope` load.
  {% endupdate %}

{% update date="2026-02-02" %}

## v3.1.0

#### Added

* [Added Subresource Integrity (SRI)](/integration/web-push/sdk/delivery-and-security-sri) support for pinned SDK assets
  {% endupdate %}
  {% endupdates %}


# Native App Push

Choose a Platform/Channel to view implementation steps

{% content-ref url="/pages/jeRemeIR69xGEEDV1daj" %}
[Apple / iOS](/integration/implementation-steps/apple-ios)
{% endcontent-ref %}

{% content-ref url="/pages/NJP4mfr4BQnCU6xJIJ7C" %}
[Android](/integration/implementation-steps/android)
{% endcontent-ref %}

{% content-ref url="/pages/8V197NuJkTKCIk3qjzLC" %}
[React Native](/integration/implementation-steps/react-native)
{% endcontent-ref %}

{% content-ref url="/pages/yewLFm3emng3NkOHaLKq" %}
[Flutter](/integration/implementation-steps/flutter)
{% endcontent-ref %}


# Apple / iOS

## Step 1: P8 Key or P12 Cert Setup

All apps must have a P8 Key or P12 Certificate created via the Apple Developer portal to enable subscriptions and sending. Follow the link below to create the key or certificate for your app and add it to our platform.

{% content-ref url="/pages/X7O1fIuw88yK1nGscUlP" %}
[P8 Key or P12 Cert Setup](/integration/implementation-steps/apple-ios/p8-key-or-p12-cert-setup)
{% endcontent-ref %}

## Step 2: Client SDKs

Once the P8 Key or P12 Cert has been uploaded to the platform you can integrate the Push SDK into your app. Choose the appropriate SDK from the list below for implementation instructions.

{% content-ref url="/pages/BhMsJvQP8NywNLZ9zqzm" %}
[SDK: Swift / Obj-C](/integration/implementation-steps/apple-ios/sdk-swift-obj-c)
{% endcontent-ref %}


# P8 Key or P12 Cert Setup

Instructions for creating the required credentials to enable iOS notifications

## Prerequisites

In order to begin subscribing users to notifications you must have either a P8 Key (recommended) or a P12 Certificate; Either of these can be generated via the Apple Developers portal.

* An [Apple Developer Account](https://developer.apple.com/account/) that has Admin privileges
* Your application's App Bundle ID (e.g., `com.pushly.example`)
* An iOS application or XCode project with the Push Notifications capability added

{% hint style="info" %}
**Which work happens where.** Setting up iOS notifications spans two systems, and the split is easy to conflate:

* **Apple Developer portal** — register the App ID and enable its capabilities, register the devices you will test on, and generate the APNs auth key (`.p8`) or certificate (`.p12`).
* **Pushly dashboard** — upload the `.p8` key along with its `Key ID` and your `Team ID` (or the `.p12` and `Bundle ID`) so that Pushly can send to APNs on your behalf.

The sending credentials are server-side configuration. They are never added to your app's code, `Info.plist`, or entitlements — the app itself only needs its SDK Key.
{% endhint %}

## Register Your App ID

Every app that receives notifications needs an App ID in the Apple Developer portal with the right capabilities enabled. Complete this before generating a key or certificate.

1. [Login](https://developer.apple.com/account/) to your company's Apple Developer account.
2. Click on the `Identifiers` link under `Certificates, Identifiers & Profiles`
3. In the top-right dropdown ensure `App IDs` is selected and click the `+` button next to the `Identifiers` header.
4. Choose `App IDs` from the list of options and press the `Continue` button.
5. Choose `App` as the identifier type and click the `Continue` button.
6. Enter a description. Eg: `YOUR_APP_NAME Push Notifications`
7. Enter the Bundle ID for your app, matching your Xcode project exactly. Eg: `com.pushly.example`
8. Under the `Capabilities` tab check both:
   * `Push Notifications` — required for the app to register with APNs at all.
   * `App Groups` — required for the shared container the PushSDK uses to pass data between your app and its Notification Service Extension. See [<mark style="color:blue;">Configure the Shared App Group</mark>](/integration/implementation-steps/apple-ios/sdk-swift-obj-c#step-4-configure-the-shared-app-group).
9. Click the `Continue` and then `Register` button.

{% hint style="warning" %}
Enabling a capability on the App ID is not sufficient on its own — the provisioning profile has to be regenerated afterwards before the entitlement is actually granted to a build. Xcode does this for you when `Automatically manage signing` is enabled. If you manage profiles manually, regenerate and download the profile after changing any capability.
{% endhint %}

## Register a Test Device

Development builds only install on devices that are registered to your team.

* With `Automatically manage signing` enabled, Xcode registers a connected device for you the first time you build to it.
* To register a device manually, go to `Devices` in the Apple Developer portal, click `+`, and enter the device name and its identifier (UDID).

{% hint style="info" %}
Simulators do not need to be registered. A simulator can register with APNs and receive real remote notifications when it meets the requirements in [<mark style="color:blue;">Testing on a simulator</mark>](/integration/implementation-steps/apple-ios/sdk-swift-obj-c#testing-on-a-simulator); on setups that do not meet them, token registration has to be exercised on a physical device.
{% endhint %}

## P8 Key Setup (Recommended)

### Step 1: Generate the Key

If you are not renewing an existing P12 certificate we recommend using the newer P8 Key to provide authorization to send notifications. The P8 key will work across all of your applications and environments. You do not need a separate P8 file for developer vs production or for each of your applications.

To create a new P8 key navigate to your [Apple Developer Account](https://developer.apple.com/account/) and click the `Keys` link.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2Fhr66nmrcH5k9oL8e59TU%2Fkeys.png?alt=media&#x26;token=4fff2cf6-6abd-44e0-88d5-18c8ed2976bd" alt=""><figcaption></figcaption></figure>

On the next page click the `+` button next to the `Keys` header

Enter a name for your P8 key; This can be your company name, the name of your application, or any text that will help you identify it later.

Check the `Apple Push Notification service (APNs)` box and click the `Continue` button.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2F1MikHTSIx0icOiCAaWJx%2Fcreate.png?alt=media&#x26;token=d57ce1af-94e1-445b-bc1b-bbb0a32eef50" alt=""><figcaption></figcaption></figure>

On the next page, copy the value provided for `Key ID` and then click the `Download` button to download the P8 Key.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FlZQkYbTrhXRRZERcCaec%2FScreenshot%202022-12-12%20at%204.10.09%20PM.png?alt=media&#x26;token=3ba39f9c-e7b6-473d-808a-b289075eb454" alt=""><figcaption></figcaption></figure>

The last piece of information we need is the `Team ID` associated to your membership and can be found in the top right of the screen underneath your name.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FXFo3595GgoygzfAbErcN%2FScreenshot%202022-12-12%20at%204.19.50%20PM.png?alt=media&#x26;token=24947664-83c3-4817-976e-33cbe23f9963" alt=""><figcaption></figcaption></figure>

### Step 2: Enable Integration in Pushly

Now that you have the `Team ID`, `Key ID` , and the P8 Key you can log into the Pushly platform, navigate to the `Settings` page, and locate the `Send Integrations` section.

For here, click the `Edit` button in the `Send Integration` header, enable the `Native` switch under the `Apple` heading and:

1. Choose `P8 Cert` from the pill selection
2. Click the certificate area to upload your P8 Key
3. Enter the `Team ID`
4. Enter the `Key ID`
5. Click the `Submit` button

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FzISEeeHVthIJ2AVhffo5%2FScreenshot%202023-02-01%20at%209.29.20%20AM.png?alt=media&#x26;token=2a1e22e1-3c51-435c-8f39-8e20a9a8a65e" alt=""><figcaption></figcaption></figure>

If everything was entered correctly you should now see a section underneath `Send Integrations` that shows an active Apple Native integration.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2Fnwbudz0lLpKSXyWiK5Eo%2FScreenshot%202023-02-01%20at%209.27.22%20AM.png?alt=media&#x26;token=b86668f2-c739-4630-a063-629a7c78bc7f" alt=""><figcaption></figcaption></figure>

## P12 Certificate Setup

We do not recommend creating new P12 Certificates in favor of using P8 Keys. If you have an existing P12 Certificate follow the steps below to renew it and add it to the platform.

### Step 1: Create a Certificate Request (CSR)

1. On a Mac computer Open the `Keychain Access` application
2. Within the `Keychain Access` primary application menu, select `Keychain Access` > `Certificate Assistant` > `Request a Certificate from a Certificate Authority`
3. In the Certificate Information window, enter the following information:
   * In the `User Email Address` field, enter your email address.
   * In the `Common Name` field, create a name for your private key (e.g. Company Name Web Push).
   * The `CA Email Address` field should be left empty.
   * In the `Request` group, select the `Saved to disk` option.
4. Click `Continue` within Keychain Access and save the CSR to disk to complete this step.

### Step 2: Create an Identifier for Your App

{% hint style="danger" %}
If you already completed [<mark style="color:blue;">Register Your App ID</mark>](#register-your-app-id) above, or you are renewing an existing certificate, skip this step and move to Step 3 — the App Identifier already exists within your Apple developer account.
{% endhint %}

1. [Login](https://developer.apple.com/account/ios/certificate/) to your company's Apple Developer Console account.
2. Click on the `Identifiers` link under `Certificates, Identifiers & Profiles`
3. In the top-right dropdown ensure `App IDs` is selected and click the `+` button next to the `Identifiers` header.
4. Choose `App IDs` from the list of options and press the `Continue` button.
5. Choose `App` as the identifier type and click the `Continue` button
6. Enter a description. Eg: `YOUR_APP_NAME Push Notifications`
7. Enter the Bundle ID for your app. Eg: `com.YOUR_APP_NAME.app-push`
8. Under the `Capabilities` tab scroll down and check the `Push Notifications` capability
9. Click the `Continue` and then `Register` button.

### Step 3: Generate a Web Push Certificate

1. Click on `Certificates` in the left nav and press the `+` sign next to the `Certificates` header
2. Choose `Apple Push Notification service SSL (Sandbox & Production)` from the list of Services and then click `Continue`.
3. Select the App Identifier that corresponds to the application for which you are generating the certificate and click the `Continue` button.
4. Click the `Choose File` button, browse for the CSR file that we created in the first step, and click `Continue`.
5. The next screen informs you that your certificate is ready. Click `Download` to store the certificate on your computer.

### Step 4: Generate the .p12 Certificate

* Navigate to the certificate you saved on your computer and double click it to install it to the `Keychain Access` application.
* Open the `Keychain Access` application and click on `My Certificates` tab with the `Login` keychain selected in the left navigation section.
* Right click on the installed Apple Push Services certificate and choose `Export`.
* In the save dialog, choose a filename (or leave the default), choose the `.p12` file format, and click the `Save` button.
* You will be prompted to enter a password - leave both boxes empty and click `OK`.
* Click `Allow` to save the p12 certificate to your computer.

### Step 5: Upload the Certificate via the Platform

Now that you have the `Bundle ID` and the P12 Certificate you can log into the Pushly platform, navigate to the `Settings` page, and locate the `Send Integrations` section.

For here, click the `Edit` button in the `Send Integration` header, enable the `Native` switch under the `Apple` heading and:

1. Enter the application's `Bundle ID`
2. Choose `P12 Cert` from the pill selection
3. Click the area to upload your P12 Certificate
4. If you entered a passphrase when generating the certificate enable the switch and add it
5. Click the `Submit` button


# SDK: Swift / Obj-C

## Prerequisites

Before you can integrate the SDK with your app the following requirements must be met:

* An iOS 11+ device (iPhone, iPad, or iPod Touch) to develop and test on, or a simulator that meets the requirements in [<mark style="color:blue;">Testing on a simulator</mark>](#testing-on-a-simulator).
* A Mac OS device with XCode 14+ (Swift 5.7, if using Swift) installed.
* An iOS P12 Push Certificate or P8 Key. If you do not already have these follow [<mark style="color:blue;">our documentation</mark>](/integration/implementation-steps/apple-ios/p8-key-or-p12-cert-setup) to generate them.
* An App ID registered for your bundle id with the `Push Notifications` and `App Groups` capabilities enabled. See [<mark style="color:blue;">Register Your App ID</mark>](/integration/implementation-steps/apple-ios/p8-key-or-p12-cert-setup#register-your-app-id).
* SDK Key: Obtained from the Pushly Platform in the `Domain Settings` page under the `Overview` > `Details` > `Domain Information` section.

## Step 1: Import the PushSDK Framework

{% tabs %}
{% tab title="Swift Package Manager" %}
The PushSDK Framework is compatible with both Swift and Objective-C.

* Select the root project and then your primary application target > `General`
* Click `+` > `Add Other` > `Add Package Dependency` under `Frameworks, Libraries, and Embedded Content`
* Search using the Package URL: [<mark style="color:blue;">https://github.com/pushly/push-sdk-ios</mark>](https://github.com/pushly/push-sdk-ios)
* Set the Dependency Rule to: `Range of Versions` and `1.0.0` < `2.0.0`
* Click `Add Package`

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2Fur8SV1YwZVDqBNRbLvYW%2FScreen%20Shot%202022-12-16%20at%2012.50.27%20PM.png?alt=media&#x26;token=11c0170c-4375-4ce7-9f72-0077fd73e82f" alt=""><figcaption></figcaption></figure>

* Ensure the `Pushly` option is checked and click `Add Package`

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FEXYitjbWrTVuLTouDMPQ%2FScreen%20Shot%202022-12-16%20at%2012.50.52%20PM.png?alt=media&#x26;token=f8eb701a-3b7c-47bc-9229-1259f38dd2bf" alt=""><figcaption></figcaption></figure>

* Finally, add the `Pushly` library to the Primary Application Target

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FsuHbbXOjXGKNlZobezvD%2FScreen%20Shot%202022-12-16%20at%2012.51.13%20PM.png?alt=media&#x26;token=c593ae8d-c92b-4dd7-9bc3-102bcd18fe10" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="CocoaPods" %}
The PushSDK Framework is compatible with both Swift and Objective-C.

CocoaPods Link: <https://cocoapods.org/pods/Pushly>

Add the following to your Podfile:

```
target 'MyApp' do
  pod 'Pushly', '~> 1.1'
end
```

Then run a `pod install` inside your terminal, or from CocoaPods.app.
{% endtab %}
{% endtabs %}

## Step 2: Add the Notification Service Extension

The Notification Service Extension is required to enable rich notifications that support images and custom actions.

Within your app's XCode project select `File > New > Target`. Select `Notification Service Extension` inside the iOS templates tab and click `Next`.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FIKOtBgXyBx78VJBxuNs9%2Fimage.png?alt=media&#x26;token=b0055514-7e75-4ce8-a0b3-bbba8f2543c6" alt=""><figcaption></figcaption></figure>

Enter "NotificationServiceExtension" for the `Product Name` and any other configuration details for your app extension and then click `Finish` but **do not click activate** on the subsequent dialog.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2F96yVKAthjykdmnXGXlem%2Fimage.png?alt=media&#x26;token=b8ed3424-44e0-4259-b97b-bb1457411164" alt=""><figcaption></figcaption></figure>

Click `Cancel` on the dialoging prompting you to activate the service extension so that you can set the deployment target once the modal is closed.

<img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FcGXECnSyGjGGEUBg5KDM%2Fimage.png?alt=media&#x26;token=bc91b955-baa5-490f-a32b-dd7367657f9f" alt="" data-size="original">

Make sure to set your deployment target to the **same as your primary application target**. Unless you have a specific reason not to, you should set the `Deployment Target` to be iOS 11 which is the lowest supported version of iOS in the PushSDK Framework and in the most recent releases of XCode.

Next, add the `Pushly` library to your Notification Service Extension:

Click `+` under `Frameworks and Libraries` and select the already imported `Pushly` library.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FK7zOKyVoMmLfbFB5SFif%2FScreen%20Shot%202022-12-16%20at%2012.51.32%20PM.png?alt=media&#x26;token=bf917f13-825a-4905-b13d-404d953a7c4a" alt=""><figcaption></figcaption></figure>

Now, open the newly created Notification Service Extension and replace the code with the following:

{% tabs %}
{% tab title="Swift" %}

```swift
import Pushly

class NotificationService: PNNotificationServiceExtension {
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
// Make sure to keep the header file import
#import "NotificationService.h"

@import Pushly;

@interface NotificationService ()

@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;

@end

@implementation NotificationService

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
    self.contentHandler = contentHandler;
    self.bestAttemptContent = [request.content mutableCopy];
    
    [PNNotificationServiceExtensionHandler didReceiveExtensionRequest:request content:self.bestAttemptContent withContentHandler:contentHandler];
}

- (void)serviceExtensionTimeWillExpire {
    // Called just before the extension will be terminated by the system.
    // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
    [PNNotificationServiceExtensionHandler didRecieveExtensionTimeWillExpire:self.bestAttemptContent withContentHandler:self.contentHandler];

    self.contentHandler(self.bestAttemptContent);
}

@end
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
You can ignore dependency errors, such as `Module Pushly not found`, at this point as they will be resolved during the next build.
{% endhint %}

## Step 3: Add Capabilities to Primary Application Target

{% hint style="warning" %}
The Push Notifications capability should only be added to the **primary application target**.
{% endhint %}

Select the root project and then your primary application target > `Signing & Capabilities`

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FPEKQiayxyjIphpjeqxB1%2Fimage.png?alt=media&#x26;token=84e69c08-d120-4a48-a193-a272b9e4f109" alt=""><figcaption></figcaption></figure>

Click the `+ Capability` and add `Push Notifications`.

Click the `+ Capability` and add `Background Modes`.

After adding the `Background Modes` capability ensure that `Remote Notifications` are enabled.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FMAfJ7lisFKGMxQvjpIaB%2Fimage.png?alt=media&#x26;token=7b5b4afb-0a24-4b0a-8ef6-594f763231ea" alt=""><figcaption></figcaption></figure>

Adding `Push Notifications` writes the `aps-environment` entitlement into the target's entitlements file — `development` for debug builds, and `production` for release, TestFlight, and App Store builds. Xcode manages this value; you should not set it by hand.

{% hint style="warning" %}
If the entitlement is missing, the app cannot register with APNs and registration fails at launch with:

```
no valid 'aps-environment' entitlement string found for application
```

That error means either the `Push Notifications` capability is not present on this target, or the provisioning profile predates the capability being enabled on your App ID. Confirm the capability is listed, then let Xcode regenerate the profile.
{% endhint %}

## Step 4: Configure the Shared App Group

Your app and its Notification Service Extension run as **two separate processes**. The shared App Group container is the only storage both can reach, and the PushSDK relies on it for everything that begins in one process and completes in the other.

The same App Group must be declared on **both** targets. If only one carries it, each process silently falls back to its own private storage and the sharing simply does not happen — with no error raised at the point of failure.

### Naming

The SDK derives the identifier from your app's bundle id rather than reading it from configuration, so the two targets cannot disagree about which container to use. It must be exactly:

```
group.{app-bundle-id}.push
```

For an app whose bundle id is `com.pushly.example`, the App Group is `group.com.pushly.example.push`. The Notification Service Extension uses the **app's** bundle id here, not the extension's own.

### Add it to the primary application target

With your primary application target selected under `Signing & Capabilities`, click the `+ Capability` and add `App Groups`.

Click the `+` symbol located inside the `App Groups` section to add a new named container, and name it as above.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FOR42sYi1kH4o1227pQpY%2Fimage.png?alt=media&#x26;token=c504305e-c703-4e7e-afff-834d9de68aac" alt=""><figcaption></figcaption></figure>

Check the box next to the newly created App Group in the primary application target.

### Add it to the Notification Service Extension

Select the root project and then the `NotificationServiceExtension > Signing & Capabilities`

Click the `+ Capability` and add `App Groups`.

Check the box next to the **same** App Group in the `NotificationServiceExtension`.

Adding the capability writes `com.apple.security.application-groups` into each target's entitlements file, containing the group identifier. Both files must list it.

{% hint style="info" %}
With `Automatically manage signing` enabled, Xcode creates the App Group identifier, enables the `App Groups` capability on your App ID, and regenerates the provisioning profiles for both targets. If Xcode complains about the new App Group, it is usually still provisioning — refresh the list once it has finished.

If you manage signing manually, creating the identifier is not enough on its own: enable `App Groups` on the App ID as well, then regenerate and download both profiles.
{% endhint %}

### What breaks without it

The SDK keeps running, but the features that depend on cross-process state stop working:

* **Notification impressions** — recorded by the extension and completed by the app, so a severed container costs every one of them.
* **Badge counts** — the shared count is no longer visible to both processes.
* **Service-extension attribution** — notifications handled by the extension are no longer attributed correctly.

The per-installation event sequence counter is also guarded by a cross-process lock that lives in the container. Without the container the SDK falls back to a best-effort in-process increment, so events still carry a sequence value, but it is no longer coordinated between the two processes.

### Confirming it is configured

The SDK reports the state of the shared container once per launch. When it is missing you will see:

```
[PNAppGroup] Shared App Group container is unavailable. Notification impressions, badge counts and service-extension attribution will not work. Add the App Group "group.com.pushly.example.push" to the entitlements of BOTH the app target and the Notification Service Extension target.
```

When it is configured correctly, the same check reports the resolved identifier at debug level:

```
[PNAppGroup] Shared App Group container available: group.com.pushly.example.push
```

## Step 5: SDK Initialization

First import the `Pushly` library in your AppDelegate

{% tabs %}
{% tab title="Swift" %}

```swift
import Pushly
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
@import Pushly;
```

{% endtab %}
{% endtabs %}

Add the following code to the AppDelegate's `didFinishLaunchingWithOptions` method of your application.

Replace the `REPLACE_WITH_SDK_KEY` in the `setConfiguration` method with the SDK Key from the platform settings page.

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.logLevel = .info
PushSDK.setConfiguration(appKey: "REPLACE_WITH_SDK_KEY", withLaunchOptions: launchOptions)

PushSDK.PushNotifications.showPermissionPrompt() { granted, settings, error in
    // optional callback
    print("User accepted permissions: \(granted)")
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
PushSDK.logLevel = PNLogLevelInfo;
[PushSDK setConfigurationAppKey:@"REPLACE_WITH_SDK_KEY" withLaunchOptions:launchOptions];

[PushSDKPushNotifications showPermissionPrompt:^(BOOL granted, UNNotificationSettings * _Nonnull settings, NSError * _Nullable error) {
    NSLog(@"User accepted permissions: %@", granted ? @"YES" : @"NO")
}];
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
**SwiftUI apps.** If your app supplies its delegate with `@UIApplicationDelegateAdaptor`, place the `setConfiguration` call in that delegate's `didFinishLaunchingWithOptions` exactly as shown above — the integration works normally.

Be aware when debugging that SwiftUI installs its own `SwiftUI.AppDelegate` as `UIApplication.shared.delegate` and forwards callbacks on to your adapted class. Inspecting `UIApplication.shared.delegate` will therefore not show your own delegate type. This is expected and does not indicate a broken integration.
{% endhint %}

## Step 6: Test the Implementation

Run your app on a physical iOS device, or on a simulator that meets the requirements in [<mark style="color:blue;">Testing on a simulator</mark>](#testing-on-a-simulator), to make sure it builds correctly.

The code you added in the previous step will show the push permission dialog upon app open. This can be customized using [<mark style="color:blue;">SDK methods</mark>](/integration/implementation-steps/apple-ios/sdk-swift-obj-c/sdk-methods#manually-triggering-the-permission-dialog) to control when the dialog shows.

After accepting the dialog, log into the platform and navigate to `Notifications > Create Notification` and send your first notification, targeting the **Native: iOS** channel, to your device.

### Testing on a simulator

A simulator can register with APNs and receive real remote notifications, so it can be used to test subscription and end-to-end delivery. This requires **all** of the following:

* A Mac with Apple silicon or a T2 chip
* macOS 13 or later
* An iOS 16 or later simulator
* Xcode 14 or later

When those conditions are met the app registers normally and iOS issues a genuine device token. Two things differ from a physical device:

* **The token is sandbox-only.** Notifications must be sent through the APNs sandbox environment. A debug build carries `aps-environment: development`, which is what routes sends to sandbox, so this is handled for you.
* **The token is tied to that simulator on that Mac.** It is specific to the combination of the simulator and the Mac hardware it runs on, so it is not portable, and erasing the simulator issues a new one.

{% hint style="warning" %}
If any of the requirements above are not met — an Intel Mac without a T2 chip, macOS 12 or earlier, or a simulator older than iOS 16 — registration fails with APNs error `3010` and the SDK logs:

```
iOS Simulator does not support push notifications
```

On those setups token registration has to be exercised on a physical device.
{% endhint %}

### Delivering a notification without APNs

`xcrun simctl push` delivers a notification straight to a booted simulator with no APNs setup at all, and works on any simulator regardless of the requirements above. It is the quickest way to check notification **presentation and tap handling**.

Save a payload as `payload.apns`:

```json
{
  "aps": {
    "alert": {
      "title": "Test notification",
      "body": "Delivered locally to the simulator."
    },
    "sound": "default"
  },
  "piid": "00000000-0000-0000-0000-000000000000",
  "notification_id": 1,
  "landing_url": "https://example.com"
}
```

Then deliver it to a booted simulator, replacing the bundle id with your own:

```bash
xcrun simctl push booted com.pushly.example payload.apns
```

For the SDK to recognise the payload as a Pushly notification and process it, the payload must carry all of the following:

* `aps.alert` as a **dictionary** — a plain string alert is not recognised
* `piid` — string
* `notification_id` — **integer**
* `landing_url` — string

If any of these are missing or the wrong type, iOS still displays the notification but the SDK ignores it, and no SDK callbacks or events fire.

{% hint style="warning" %}
Notifications delivered with `simctl push` never travel through APNs or Pushly, so they produce no delivery, impression, or click reporting in the platform. Use them to verify presentation and tap handling only.
{% endhint %}

## Next Steps

Once you have confirmed the SDK is working properly you may continue to add additional optional functionality like:

* [<mark style="color:blue;">Handling notification opens / app links / deep linking</mark>](/integration/implementation-steps/apple-ios/sdk-swift-obj-c/deep-linking)
* [<mark style="color:blue;">Attaching attributes to a subscriber's profile</mark>](/integration/implementation-steps/apple-ios/sdk-swift-obj-c/sdk-methods#adding-attributes-to-a-subscribers-profile)
* [<mark style="color:blue;">Sending information about what content a subscriber interacts with</mark>](/integration/implementation-steps/apple-ios/sdk-swift-obj-c/sdk-methods#activity-tracking-url-screen-visits)


# Activity Tracking

How to implement activity tracking within your application to enable segmentation based on content views

## Overview

The PushSDK provides methods that allow you to track the type of content a subscriber is viewing within your application. This information can then be used in the Pushly platform to create cohorts of subscribers that have visited specific URLs  or screens with tags based on the number of visits and visit recency.

For example: "Subscribers who have visited a page/screen tagged with "Astrology" at least 4 times in the last 30 days".

## Methods

### UserProfile.trackActivity

To track a URL or screen visited along with its keyword/tag metadata:

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.UserProfile.trackActivity(name: "myapp://dashboard", withTags: ["my-tag"])
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[PushSDKUserProfile trackActivityWithName:@"myapp://dashboard" withTags:@[@"my-tag"]];
```

{% endtab %}
{% endtabs %}

Or if there are no tags to be provided omit the `tags` parameter:

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.UserProfile.trackActivity(name: "myapp://sports/article-1")
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[PushSDKUserProfile trackActivityWithName:@"myapp://dashboard"];
```

{% endtab %}
{% endtabs %}


# Deep Linking

Overview of how the SDK handles deep links and how to perform custom navigation within your application

The PushSDK handles opening of a notification in the following ways:

* By default, the SDK will automatically call the `open` method using the landing URL attached to the notification.
* If you implement the `didReceiveNotificationDestination` lifecycle delegate you can perform any custom logic needed when the notification is opened.

## Implementing URL Handling Delegates

The PushSDK provides delegates that will be called when a subscriber interacts with a notification. These delegates should be implemented so that the subscriber is automatically navigated to the proper controller, page, or other view after opening the notification.

{% hint style="danger" %}
It is recommend to put all SDK delegates within your application's `AppDelegate` class to ensure proper handling of all notification events.
{% endhint %}

When the SDK receives a notification open event it will attempt to call one of the below delegates with parameters that you can use to navigate the subscriber to the appropriate view within your app. In order to implement these delegates you must have a class that implements `PNNotificationLifecycleDelegate`.

You must inform the SDK what class is going to implement this protocol. This can be done using the `setNotificationLifecycleDelegate` method on the SDK:

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.setNotificationLifecycleDelegate(self)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[PushSDK setNotificationLifecycleDelegate:self];
```

{% endtab %}
{% endtabs %}

For example, if a landing URL was provided within the notification and you implemented the delegate to handle subscriber navigation within your `AppDelegate` the code may look like this:

{% tabs %}
{% tab title="Swift" %}

```swift
import Pushly

@main
class AppDelegate: UIResponder, UIApplicationDelegate, PNNotificationLifecycleDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        PushSDK.setConfiguration(appKey: "YOUR_APP_KEY", withLaunchOptions: launchOptions)
        PushSDK.PushNotifications.showPermissionPrompt() { granted, settings, error in
            // optional callback
            print("User accepted permissions: \(granted)")
        }
        PushSDK.setNotificationLifecycleDelegate(self)

        return true
    }

    func pushSDK(didReceiveNotificationDestination destination: String, withInteraction interaction: PNNotificationInteraction) -> Bool {
        // Navigate to a view within your application using the destination (the Landing URL)
        
        // Return true to inform the SDK that it should not navigate
        return true
    }
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
@import Pushly;

@interface AppDelegate () <PNNotificationLifecycleDelegate>

@end

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.

    PushSDK.logLevel = PNLogLevelInfo;
    [PushSDK setConfigurationAppKey:@"YOUR_APP_KEY" withLaunchOptions:launchOptions];

    [PushSDKPushNotificationsSDK showPermissionPrompt:^(BOOL granted, UNNotificationSettings * _Nonnull settings, NSError * _Nullable error) {
        NSLog(@"User accepted permissions: %d", granted);
    }];
    
    [PushSDK setNotificationLifecycleDelegate:self];

    return YES;
}

- (BOOL)pushSDKDidReceiveNotificationDestination:(NSString *)destination withInteraction:(PNNotificationInteraction *)interaction {
    // Navigate to a view within your application using the destination (the Landing URL)
    
    // Return YES to inform the SDK that it should not navigate
    return YES;
}
```

{% endtab %}
{% endtabs %}

## Notification Interaction Delegates

### Notification Opened With URI/URL Destination

This delegate is called when a notification is opened that has a landing URL attached. The subscriber should be navigated to the view that represents the URL.

This method expects a Boolean response. If the response is `true` then the SDK will perform no navigation action for this notification open. If the response is `false` then the SDK will attempt to navigate the subscriber to the destination URL by using the `open` method.

{% tabs %}
{% tab title="Swift" %}

```swift
func pushSDK(didReceiveNotificationDestination destination: String, withInteraction interaction: PNNotificationInteraction) -> Bool {
    // Navigate to a view within your application using the destination (the Landing URL)
        
    // Return true to inform the SDK that it should not navigate
    return true
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
- (BOOL)pushSDKDidReceiveNotificationDestination:(NSString *)destination withInteraction:(PNNotificationInteraction *)interaction {
    // Redirect to a view within your application using the destination (the Landing URL)
    
    // Return YES to inform the SDK that it should not redirect
    return YES;
}
```

{% endtab %}
{% endtabs %}

x


# Live Activities

A quick start guide to adding Live Activity support to your iOS application with the Pushly PushSDK

[Live Activities](https://developer.apple.com/design/human-interface-guidelines/live-activities), introduced by Apple in October 2022, allow app developers to display dynamic, live-updating, content via widgets on device lock screens and in the Dynamic Island (for capable devices).

The PushSDK helps you streamline Live Activity registration and updates by allowing developers to register and manage the short-lived push tokens associated with your Live Activities which can then be updated via our server API.

## Prerequisites

Before you can integrate Live Activities with the SDK the following requirements must be met:

* An iOS Send Integration configured with a .p8 key.
* An iOS app (Live Activities are only available for iOS and iPadOS).
* A device or emulator with iOS 16.1 or newer installed
* PushSDK release supporting Live Activities (See version support chart below)

## Step 1: iOS SDK Setup

For applications **not** already using the PushSDK please see our Native App Push setup guide for [Apple / iOS](/integration/implementation-steps/apple-ios).

For applications already using the PushSDK you might need to upgrade to a later version that includes Live Activity support. Use the following tables to determine if you need to upgrade your installation.

<table><thead><tr><th width="190">SDK</th><th>Earliest Version with Live Activities</th></tr></thead><tbody><tr><td><a href="/pages/BhMsJvQP8NywNLZ9zqzm">Swift</a></td><td>1.2.0+</td></tr><tr><td><a href="/pages/8V197NuJkTKCIk3qjzLC">React Native SDK</a></td><td>1.1.0+</td></tr><tr><td><a href="/pages/yewLFm3emng3NkOHaLKq">Flutter SDK</a></td><td>1.1.0+</td></tr></tbody></table>

<table><thead><tr><th width="190">SDK</th><th>Earliest Version with Push-to-Start Live Activities Support</th></tr></thead><tbody><tr><td><a href="/pages/BhMsJvQP8NywNLZ9zqzm">Swift</a></td><td>1.3.9+ (Requires iOS 17.2+)</td></tr></tbody></table>

{% tabs %}
{% tab title="Swift Package Manager" %}
Within the Xcode project navigator panel locate `Package Dependencies`. Locate and right-click on `Pushly` and select `Update Package`.

<figure><img src="/files/PxLMYHBWpMR2MPfA7BnW" alt=""><figcaption></figcaption></figure>

If after an update the package is not >= version 1.2.0 select your Project in the project navigator, select your project, and then navigate to `Package Dependencies`. Select `Pushly` and ensure the `Dependency Rule` is set to `Up to Next Major Version` with 1.0.0, or greater, as the target.

<figure><img src="/files/ffEUbTCTYDW3mAotQqmk" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="CocoaPods" %}

* Ensure you are targeting `'Pushly', '>= 1.0', '< 2.0'` in your Podfile.
* Run `pod update Pushly`
* Confirm the latest version has been downloaded by looking in your Podfile.lock.
  {% endtab %}
  {% endtabs %}

## Step 2: Add Live Activity Support to Info.plist

Applications implementing Live Activities must set the `Supports Live Activities` key to `YES` in their primary target info.plist.

<figure><img src="/files/1wVHV2bTXNh27IJcYqcH" alt=""><figcaption></figcaption></figure>

If one of your use cases utilizes frequent updates you should also consider setting `Supports Live Activities Frequent Updates` to `YES` in the info.plist.

## Step 3: Add a Live Activity Widget Extension

Within your app's Xcode project select `File > New > Target`. Select `Widget Extension` inside the iOS templates tab and click `Next`.

<figure><img src="/files/nxQ2HNxP8U0Ryvv0Rca5" alt=""><figcaption></figcaption></figure>

Enter your desired Live Activity widget name for the `Product Name` and any other configuration details for your widget extension, making sure that `Include Live Activity` is selected, and then click `Finish`. On the subsequent dialog click `Cancel` to continue developing without activating the new widget extension as the current target.

<figure><img src="/files/BdFKn2BuEp9papSzIHh3" alt=""><figcaption></figcaption></figure>

## Step 4: Setup a Live Activity

### User Interface

Before starting your new Live Activity we recommend reading through Apple's guide to [Displaying live data with Live Activities](https://developer.apple.com/documentation/activitykit/displaying-live-data-with-live-activities).

### Starting Your Live Activity

Once your Live Activity has been designed and setup you are now ready to start and register it via the PushSDK.

To aid in tracking and updating multiple Live Activity instances across millions of devices via a single API call we use an activity ID. Activity IDs are string values provided by you, the developer.

In some scenarios, such as sporting events, you may want to send a single update to all devices registered to a single event. In these cases you should use a unique ID that identifies the event rather than an ID per individual user.

In other scenarios, such as meal orders, Live Activities are user specific. In these cases you should use a unique ID that identifies the individual user.

## Step 5: Starting a Live Activity

### Starting from Within the App

The following code snippet requests to start a Live Activity using the `MyLiveActivityAttributes` struct with an `eventId` attribute that will uniquely identify this instance of the activity and then registers the activity with the PushSDK to observe and collect any token updates.

{% tabs %}
{% tab title="Swift" %}
{% code fullWidth="false" %}

```swift
import UIKit
import ActivityKit
import Pushly

class ViewController: UIViewController {
    public func startLiveActivity() {
        if #available(iOS 16.1, *) {
            let attributes = MyLiveActivityAttributes(eventId: "my_activity_id")
            let contentState = MyLiveActivityAttributes.ContentState(
                home_team: "Chiefs",
                home_team_score: 0,
                away_team: "Chargers",
                away_team_score: 0)
            let activityContent = ActivityContent(
                state: contentState,
                staleDate: nil)
        
            do {
                let activity = try Activity<MyLiveActivityAttributes>.request(
                    attributes: attributes,
                    contentState: activityContent,
                    pushType: .token)
            
                // Register the activity with Pushly using the eventId attribute
                PushSDK.LiveActivities.register(
                    activity,
                    withId: activity.attributes.eventId)
            } catch (let error) {
                print(error.localizedDescription)
            }
        }
    }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

### Registering an Activity Type for Starting via Push

The following code snippet will start observing an Activity type and register and push-to-start tokens as well as ensure any started or already running activities are registered for push token collection as well. We will once again be using the `MyLiveActivityAttributes` class with an `eventId` attribute that will uniquely identify this instance of the activity.

{% tabs %}
{% tab title="Swift" %}
{% code fullWidth="false" %}

```swift
import UIKit
import ActivityKit
import Pushly

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        ...

        if #available(iOS 16.1, *) {
            // Register the activity type with Pushly
            PushSDK.LiveActivities.register(Activity<MyLiveActivityAttributes>.self) { activity in
                // Register the specific activity with Pushly
                PushSDK.LiveActivities.register(activity, withId: activity.attributes.eventId)
            }
        }
    }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

### Sending the Start Event

Once you have registered the activity type with the PushSDK you can use our [Live Activity API documentation](https://developers.pushly.com/reference/management-update-live-activity) to send a start event by specifying the corresponding attributes struct name in the `activity.ios.data.attributes_type` property and passing the associated unique activity identifier in both the `activity.ios.data.attributes` object and the request path - in this scenario we used the `eventId` attribute set to `my_activity_id` which we can then use in subsequent update events.

{% tabs %}
{% tab title="Example Start Event" %}
{% code fullWidth="false" %}

```json
POST Request to:
https://api.pushly.com/domains/{domain_id}/live-activities/my_activity_id
{
    "name": "my-request-event-name",
    "event": "start",
    "activity": {
        "ios": {
            "data": {
                "relevance_score": 100,
                "priority": 10,
                "attributes_type": "MyLiveActivityAttributes",
                "attributes": {
                    "eventId": "my_activity_id"
                },
                "content_state": {
                    "home_team": "Chiefs",
                    "home_team_score": 0,
                    "away_team": "Chargers",
                    "away_team_score": 0
                },
                "notification": {
                    "title": "A Live Event Has Started",
                    "body": "Stay tuned for more updates!"
                }
            }
        }
    }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

### PNActivityAttributes Protocol

The PushSDK also provides a simple protocol, `PNActivityAttributes`, that requires an attribute of `pnActivityId` which will be used to uniquely identify the activity. When using this protocol the method signatures are simplified as the ID is auto collected for token registrations in activities started both locally and via push.

{% tabs %}
{% tab title="Swift" %}
{% code fullWidth="false" %}

```swift
import UIKit
import ActivityKit
import Pushly

struct MyLiveActivityAttributes: PNActivityAttributes {
    public struct ContentState: Codable, Hashable {
        var home_team: String
        var home_team_score: Int
        var away_team: String
        var away_team_score: Int
    }

    var pnActivityId: String
}

class ViewController: UIViewController {
    public func startLiveActivity() {
        if #available(iOS 16.1, *) {
            ...

            // Register the activity with Pushly
            PushSDK.LiveActivities.register(activity)
        }
    }
}

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        ...

        if #available(iOS 16.1, *) {
            // Register the activity type with Pushly
            PushSDK.LiveActivities.register(Activity<MyLiveActivityAttributes>.self)
        }
    }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Step 5: Send Your Live Activity an Update

Once the short-lived push tokens have been registered with an activity ID you can use our [Live Activity API documentation](https://documentation.pushly-dev.com/#live-activities) to send updates to all registered Live Activities via a single request.

{% tabs %}
{% tab title="Example Update Event" %}
{% code fullWidth="false" %}

```json
POST Request to:
https://api.pushly.com/domains/{domain_id}/live-activities/my_activity_id
{
    "name": "my-request-event-name",
    "event": "update",
    "activity": {
        "ios": {
            "data": {
                "relevance_score": 100,
                "priority": 10,
                "content_state": {
                    "home_team": "Chiefs",
                    "home_team_score": 7,
                    "away_team": "Chargers",
                    "away_team_score": 0
                }
            }
        }
    }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

For more information on [Starting and updating Live Activities with ActivityKit push notifications](https://developer.apple.com/documentation/activitykit/displaying-live-data-with-live-activities) see Apple's developer documentation.


# App Messages

A quick start guide to adding App Message support to your iOS application with the Pushly PushSDK

App Messages, unlike Push Notifications and Live Activities, allow you to interact with and engage your user base without the need for a subscription token. App Message can be configured to automatically display based on a number of criteria including custom conditions that can be provided programmatically.

For App Messages with custom conditions the PushSDK provides convenience methods for you to signal when these conditions have been met as well as the ability to listen to certain lifecycle events.

## Prerequisites

Before you can integrate App Messages with the SDK the following requirements must be met:

* An iOS app
* A device or emulator with iOS 11.0 or newer installed
* PushSDK release supporting App Messages (See version support chart below)

## Step 1: iOS SDK Setup

For applications **not** already using the PushSDK please see our Native App Push setup guide for [Apple / iOS](/integration/implementation-steps/apple-ios).

For applications already using the PushSDK you might need to upgrade to a later version that includes App Message support. Use the following table to determine if you need to upgrade your installation.

<table><thead><tr><th width="191">SDK</th><th>Earliest Version with App Messages</th></tr></thead><tbody><tr><td><a href="/pages/BhMsJvQP8NywNLZ9zqzm">Objective-C / Swift</a></td><td>1.3.0+</td></tr><tr><td><a href="/pages/8V197NuJkTKCIk3qjzLC">React Native SDK</a></td><td>1.2.0+</td></tr><tr><td><a href="/pages/yewLFm3emng3NkOHaLKq">Flutter SDK</a></td><td>1.2.0+</td></tr></tbody></table>

{% tabs %}
{% tab title="Swift Package Manager" %}
Within the Xcode project navigator panel locate `Package Dependencies`. Locate and right-click on `Pushly` and select `Update Package`.

<figure><img src="/files/PxLMYHBWpMR2MPfA7BnW" alt=""><figcaption></figcaption></figure>

If after an update the package is not >= version 1.3.0 select your Project in the project navigator, select your project, and then navigate to `Package Dependencies`. Select `Pushly` and ensure the `Dependency Rule` is set to `Up to Next Major Version` with 1.3.0, or greater, as the target.

<figure><img src="/files/ffEUbTCTYDW3mAotQqmk" alt=""><figcaption></figcaption></figure>
{% endtab %}

{% tab title="CocoaPods" %}

* Ensure you are targeting `'Pushly', '>= 1.3', '< 2.0'` in your Podfile.
* Run `pod update Pushly`
* Confirm the latest version has been downloaded by looking in your Podfile.lock.
  {% endtab %}
  {% endtabs %}

## Step 2: Create and Schedule an App Message

Use the Pushly Platform to build and schedule your first [App Message](/platform/app-messages).

## Step 3: (Optional) Trigger Your App Message's Custom Condition

When using custom conditions the PushSDK is used to provide these conditions programmatically which then triggers an evaluation of which App Messages are currently active and match the provided conditions.

In most cases a single condition is triggered and evaluated at a time. For these situations you would use the `trigger(condition: String, withValue: String)` method signature.

{% tabs %}
{% tab title="Swift" %}
{% code fullWidth="false" %}

```swift
PushSDK.AppMessages.trigger(condition: "enabled_subscription", withValue: "true")
```

{% endcode %}
{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[PushSDKAppMessages triggerCondition:@"enabled_subscription" withValue:@"true"];
```

{% endtab %}
{% endtabs %}

However, there might be situations where it is more convenient to trigger multiple conditions at once for evaluation. In these cases you can pass a dictionary containing any number of conditions to the `trigger(conditions: [String : String])` method signature.

{% tabs %}
{% tab title="Swift" %}
{% code fullWidth="false" %}

```swift
PushSDK.AppMessages.trigger(conditions: [
    "enabled_subscription": "true",
    "subscriber_tier": "premium"
])
```

{% endcode %}
{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[PushSDKAppMessages triggerConditions:@{
    @"enabled_subscription" : @"true",
    @"subscriber_tier": @"premium"
}];
```

{% endtab %}
{% endtabs %}

## Step 4: (Optional) Listen to App Message Lifecycle Events

See our [SDK Delegates](/integration/implementation-steps/apple-ios/sdk-swift-obj-c/sdk-delegates#app-message-lifecycle-delegates) documentation for information on handling lifecycle events of your App Messages.


# Identity Synchronization

Information on how to link your identifier for the subscriber to our subscriber profile.

## Setting the Subscriber's External ID

Providing a unique User ID for your subscriber allows you to later interact with that subscriber via the API.

Once a subscriber's external ID has been set you may work with our team to set up bi-directional automated data syncs. The following use cases are common ways that publishers use the external ID via server-to-server requests:

* Sending notifications to individual subscribers programmatically
* Processing unsubscribe requests
* Adding and removing profile attributes and events to subscribers

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.UserProfile.externalId = "external-id"
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
PushSDKUserProfile.externalId = @"external-id";
```

{% endtab %}
{% endtabs %}

You can also check to see if the subscriber is already tagged with an External ID:

{% tabs %}
{% tab title="Swift" %}

```swift
let currentExternalId = PushSDK.UserProfile.externalId
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
NSString* currentExternalId = PushSDKUserProfile.externalId;
```

{% endtab %}
{% endtabs %}

### External ID Restrictions

To prevent subscribers from being accidentally merged via External ID and to ensure integrity in  the subscriber documents, Pushly enforces the following restrictions upon External IDs:

* No empty or strings that contain white-space
* No identifiers that contain placeholder text: `[test, testing, example, guest, anon, anonymous, user, guest, visitor, n/a]`
* No common JavaScript fall-through words or keywords: \[`[Object ... ], undefined, NaN, Infinity, true, false]`
* Cannot be only punctuation/symbols, must contain alphanumeric characters: `[---, ***, -*-, -._+/=:@~]` &#x20;
* Identifiers should be between 3 and 256 characters in length

A [regex playground](https://regex101.com/r/2wEVEc/1) has been set up for testing the validity of External ID schema.


# SDK Methods

## Manually Triggering the Permission Dialog

You may choose to disable automatic triggering of the permission dialog via the platform. In this scenario you would choose to trigger the dialog based on your own criteria (eg: after a visitor interacts with a modal / soft prompt, visits a specific screen/page, etc).

The following code will manually trigger the dialog:

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.PushNotifications.showPermissionPrompt() { granted, settings, error in
    // optional callback
    print("User accepted permissions: \(granted)")
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[PushSDKPushNotifications showPermissionPrompt:^(BOOL granted, UNNotificationSettings * _Nonnull settings, NSError * _Nullable error) {
    // optional callback
    NSLog(@"User accepted permissions: %d", granted);
}];
```

{% endtab %}
{% endtabs %}

## Adding Attributes to a Subscriber's Profile

You can add attributes to a subscriber's profile and later perform segmentation based on those attributes. For example, you may want to tag visitors who are interested in a specific type of news (eg: politics, sports) so that you can target them with specific notifications.

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.UserProfile.set(["politics", "news"], forKey: "interests")
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[PushSDKUserProfile set:@YES forKey:@"is_paying_subscriber"];
```

{% endtab %}
{% endtabs %}

If you want to set more than one attribute at a time you can also send a map of values. The following example sets both an `is_paying_subscriber` and `interests` attribute on the subscriber.

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.UserProfile.set([
  "is_paying_subscriber": true,
  "interests": ["politics", "news"]
])
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[PushSDKUserProfile set:@{
    @"is_paying_subscriber": @YES,
    @"interests": @[@"politics", @"news"]
}];
```

{% endtab %}
{% endtabs %}

If you're storing the value of a property as an array you can use the `append` and `remove` methods to add or remove values:

{% tabs %}
{% tab title="Swift" %}

```swift
// add 'sports' to the subscriber's existing interests
PushSDK.UserProfile.append(["sports"], to: "interests")

// remove 'fashion' and 'news' from the subscriber's interests
PushSDK.UserProfile.remove(["fashion", "news"], from: "interests")
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
// add 'sports' to the subscriber's existing interests
[PushSDKUserProfile append:@[@"sports"] to:@"interests"];

// remove 'fashion' and 'news' from the subscriber's interests
[PushSDKUserProfile remove:@[@"fashion", @"news"] from:@"interests"];
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Properties using `append` will only store the most recent 20 values provided.
{% endhint %}

## Activity Tracking (URL / Screen Visits)

To track the subscriber flow through your application you can use activity tracking. You can track via screen or URL and pass associated metadata/tags if desired.

This information can then be used in segmentation to create cohorts of subscribers that have visited specific URLs or screens with tags based on number of visits and recency. For example: "Subscribers who have visited a page/screen tagged with "Astrology" at least 4 times in the last 30 days".

{% tabs %}
{% tab title="Swift" %}
To track a URL or screen visited along with the keyword/tag metadata:

```swift
PushSDK.UserProfile.trackActivity(name: "myapp://dashboard", withTags: ["my-tag"])
```

Or if there are no tags to be provided omit the `tags` parameter:

```swift
PushSDK.UserProfile.trackActivity(name: "https://www.pushly.com/article-1")
```

{% endtab %}

{% tab title="Objective-C" %}
To track a URL or screen visited along with the keyword/tag metadata:

```objectivec
[PushSDKUserProfile trackActivityWithName:@"myapp://dashboard" withTags:@[@"my-tag"]];
```

Or if there are no tags to be provided omit the `2nd` parameter:

```objectivec
[PushSDKUserProfile trackActivityWithName:@"https://www.pushly.com/article-1"];
```

{% endtab %}
{% endtabs %}

## Retrieving Your Anonymous Push ID

The PushSDK automatically assigns an anonymous Push ID for event tracking and debugging purposes.

Run the following code to return the Push ID:

{% tabs %}
{% tab title="Swift" %}

```swift
let pushId = PushSDK.UserProfile.anonymousId
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
NSString* pushId = PushSDKUserProfile.anonymousId;
```

{% endtab %}
{% endtabs %}

## Setting the Subscriber's External ID

Providing a unique User ID for your subscriber allows you to later interact with that subscriber via the API.

Once a subscriber's external ID has been set you may work with our team to set up bi-directional automated data syncs. The following use cases are common ways that publishers use the external ID via server-to-server requests:

* Sending notifications to individual subscribers programmatically
* Processing unsubscribe requests
* Adding and removing profile attributes and events to subscribers

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.UserProfile.externalId = "external-id"
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
PushSDKUserProfile.externalId = @"external-id";
```

{% endtab %}
{% endtabs %}

You can also check to see if the subscriber is already tagged with an External ID:

{% tabs %}
{% tab title="Swift" %}

```swift
let currentExternalId = PushSDK.UserProfile.externalId
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
NSString* currentExternalId = PushSDKUserProfile.externalId;
```

{% endtab %}
{% endtabs %}

## Determining if a Visitor is Already Subscribed

If you need to know if a user is already subscribed to notifications the following snippet can be used:

{% tabs %}
{% tab title="Swift" %}

```swift
let subscribed = PushSDK.PushNotifications.isSubscribed
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
BOOL subscribed = PushSDKPushNotifications.isSubscribed;
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
If your application needs to react to a user's subscription status changing we recommend implementing [Permission Lifecycle Delegates](/integration/implementation-steps/apple-ios/sdk-swift-obj-c/sdk-delegates#permission-lifecycle-delegates) to ensure you are immediately notified of any change.
{% endhint %}

## Determining if a Visitor is Eligible to Prompt

{% tabs %}
{% tab title="Swift" %}

```swift
let isEligible = PushSDK.PushNotifications.isEligibleToPrompt
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
BOOL isEligible = PushSDKPushNotifications.isEligibleToPrompt;
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
If your application needs to react to a user's subscription status changing we recommend implementing [Permission Lifecycle Delegates](/integration/implementation-steps/apple-ios/sdk-swift-obj-c/sdk-delegates#permission-lifecycle-delegates) to ensure you are immediately notified of any change.
{% endhint %}

## Pausing / Resuming a User's Notifications (Soft Unsubscribe)

A user's notifications can be paused by calling the following method:

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.PushNotifications.pause()
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[PushSDKPushNotifications pause];
```

{% endtab %}
{% endtabs %}

If the user's notifications should be resumed call the following method:

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.PushNotifications.resume()
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[PushSDKPushNotifications resume];
```

{% endtab %}
{% endtabs %}

To check if the user's notifications are currently paused:

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.PushNotifications.isPaused
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
BOOL isPaused = PushSDKPushNotifications.isPaused;
```

{% endtab %}
{% endtabs %}

## Permanently Delete a User

If the user should be excluded from all notifications and tracking run the following method:

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.UserProfile.requestUserDeletion()
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[PushSDKUserProfile requestUserDeletion];
```

{% endtab %}
{% endtabs %}

## Setting the SDK Log Level

The PushSDK log level is set to `none` by default. Supported log levels are: `verbose`, `debug`, `info`, `warn`, `error`, `critical`, and `none`.

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.logLevel = .info
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
PushSDK.logLevel = PNLogLevelInfo
```

{% endtab %}
{% endtabs %}


# SDK Delegates

{% hint style="danger" %}
We recommend including all SDK delegates within your application's `AppDelegate` class to ensure proper handling of all events.
{% endhint %}

## SDK Lifecycle Delegates

These delegates can be implemented to observe SDK loading and exiting.

You must inform the SDK what class is going to implement this protocol. This can be done using the `setSDKLifecycleDelegate` method on the SDK:

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.setSDKLifecycleDelegate(self)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[PushSDK setSDKLifecycleDelegate:self];
```

{% endtab %}
{% endtabs %}

The following SDK Lifecycle delegates are available:

### Finished Loading

This delegate is called when the SDK finishes its initialization and is ready for interaction.

{% tabs %}
{% tab title="Swift" %}

```swift
func pushSDK(didFinishLoading configuration: PNApplicationConfig, withNotificationSettings settings: UNNotificationSettings) {
    // Add code to execute after SDK finishes loading
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
- (void)pushSDKDidFinishLoading:(PNApplicationConfig *)configuration withNotificationSettings:(UNNotificationSettings *)settings {
    // Add code to execute after SDK finishes loading
}
```

{% endtab %}
{% endtabs %}

## Permission Lifecycle Delegates

These delegates can be implemented to observe a user's permission authorization changes.

You must inform the SDK what class is going to implement this protocol. This can be done using the `setPermissionLifecycleDelegate` method on the SDK:

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.setPermissionLifecycleDelegate(self)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[PushSDK setPermissionLifecycleDelegate:self];
```

{% endtab %}
{% endtabs %}

The following Permission Lifecycle delegates are available:

### Permission Status Changed

This delegate is called when a user's permission status/authorization has changed.

The `status` variable represents the user's **new** permission status using the [UNAuthorizationStatus](https://developer.apple.com/documentation/usernotifications/unauthorizationstatus) enum.

{% tabs %}
{% tab title="Swift" %}

```swift
func pushSDK(didReceivePermissionStatusChange status: UNAuthorizationStatus, withSettings settings: UNNotificationSettings) {
    // Add code to execute after a user's notification permission has changed
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
- (void)pushSDKDidReceivePermissionStatusChange:(UNAuthorizationStatus)status withSettings:(UNNotificationSettings *)settings {
    // Add code to execute after a user's notification permission has changed
}

```

{% endtab %}
{% endtabs %}

## Notification Lifecycle Delegates

These delegates can be implemented to observe events like impressions, opens, and other interactions with notifications.

You must inform the SDK what class is going to implement this protocol. This can be done using the `setNotificationLifecycleDelegate` method on the SDK:

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.setNotificationLifecycleDelegate(self)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[PushSDK setPermissionLifecycleDelegate:self];
```

{% endtab %}
{% endtabs %}

The following Notification Lifecycle delegates are available:

### Notification Received

This delegate is called when a user receives a notification

{% tabs %}
{% tab title="Swift" %}

```swift
func pushSDK(didReceiveNotification notification: PNNotification)
    // Add code to execute after the subscriber's device receives a notification
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
- (void)pushSDKDidReceiveNotification:(PNNotification *)notification {
    // Add code to execute after the subscriber's device receives a notification
}
```

{% endtab %}
{% endtabs %}

### Notification Opened With URI/URL Destination

This delegate is called when a notification is opened that has a landing URL attached. The subscriber should be navigated to the view that represents the URL.

This method expects a Boolean response. If the response is `true` then the SDK will perform no navigation action for this notification open. If the response is `false` then the SDK will attempt to navigate the subscriber to the destination URL by using the `open` method.

{% tabs %}
{% tab title="Swift" %}

```swift
func pushSDK(didReceiveNotificationDestination destination: String, withInteraction interaction: PNNotificationInteraction) -> Bool {
    // Navigate to a view within your application using the destination (the Landing URL)
        
    // Return true to inform the SDK that it should not navigate
    return true
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
- (BOOL)pushSDKDidReceiveNotificationDestination:(NSString *)destination withInteraction:(PNNotificationInteraction *)interaction {
    // Redirect to a view within your application using the destination (the Landing URL)
    
    // Return YES to inform the SDK that it should not redirect
    return YES;
}
```

{% endtab %}
{% endtabs %}

See the [Deep Linking](/integration/implementation-steps/apple-ios/sdk-swift-obj-c/deep-linking) documentation for a complete example.

## App Message Lifecycle Delegates

These delegates can be implemented to observe events like impressions, opens, and other interactions with app messages.

You must inform the SDK what class is going to implement this protocol. This can be done using the `setAppMessageLifecycleDelegate` method on the SDK:

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.setAppMessageLifecycleDelegate(self)
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[PushSDK setAppMessageLifecycleDelegate:self];
```

{% endtab %}
{% endtabs %}

The following App Message Lifecycle delegates are available:

### App Message Will Present

This delegate is called when an App Message is presented to a user.

{% tabs %}
{% tab title="Swift" %}

```swift
func pushSDK(willPresentAppMessage appMessage: PNAppMessage) {
    // Add code to execute when a user's device presents an app message
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
- (void)pushSDKWillPresentAppMessage:(PNAppMessage *)appMessage {
    // Add code to execute when a user's device presents an app message
}
```

{% endtab %}
{% endtabs %}

### App Message Received User Interaction

This delegate is called when a user interacts with an App Message.

This method expects a Boolean response. If the response is `true` then the SDK will not handle the interaction or attempt to perform navigation when the interaction is an Open URL action. If the response is `false` then the SDK will process the interaction and if the interaction is an Open URL action will attempt to navigate the user to the destination URL by using the `open` method.

{% tabs %}
{% tab title="Swift" %}

```swift
func pushSDK(didReceiveAppMessageInteraction interaction: PNAppMessageInteraction, fromAppMessage appMessage: PNAppMessage) -> Bool {
    // Add code to execute when a user interacts with an app message
    
    // Return true to inform the SDK that it should not handle the interaction
    return true
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
- (BOOL)pushSDKDidReceiveAppMessageInteraction:(PNAppMessageInteraction *)interaction fromAppMessage:(PNAppMessage *)appMessage {
    // Add code to execute when a user interacts with an app message

    // Return YES to inform the SDK that it should not handle the interaction
    return YES;
}
```

{% endtab %}
{% endtabs %}


# Commerce & Catalog Item Events

How to set up activity tracking and commerce interactions via the SDK.

This guide explains how to send commerce and catalog-related user interactions to Pushly using the iOS SDK. These events power features like abandoned cart notifications, saved item reminders, revenue attribution, and catalog-driven recommendation campaigns.

The Pushly iOS SDK must be loaded and initialized before sending any of the events described below.

{% hint style="info" %}
The following steps assume that you are providing an item catalog/feed to our team. Please contact your account manager for more information on this process.
{% endhint %}

### Supported Interaction Types

Pushly supports the following commerce and catalog interactions:

* **view\_item** – A user views an item detail page
* **save\_item** – A user saves or favorites an item
* **unsave\_item** – A user removes saved or unfavorites an item
* **complete\_item** – A user completes an item
* **uncomplete\_item** – A user removes completed item
* **rate\_item** – A user rates an item
* **unrate\_item** – A user removes rating from an item
* **add\_to\_cart** – A user adds an item to their cart
* **update\_cart** – A user modifies cart contents or quantities
* **purchase** – A user completes a transaction

Each interaction can reference items using one of three identifier types, depending on your catalog configuration.

Each item requires an `id`. Quantity and rating are optional unless otherwise noted.

{% hint style="danger" %}
All of the following code snippets must be ran **after** the SDK has been initialized.
{% endhint %}

### View Item

Send this event when a user views an item detail page.

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.UserProfile.viewItem(item: CatalogItem(id: "ITEM_ID"))
```

{% endtab %}

{% tab title="Objective-C" %}

```objective-c
CatalogItem *item = [[CatalogItem alloc] initWithId:@"ITEM_ID"];
[PushSDKUserProfile viewItemWithItem:item];
```

{% endtab %}
{% endtabs %}

### Save Item

Use this event when a user saves, favorites, or bookmarks an item. This enables saved‑item reminder campaigns.

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.UserProfile.saveItem(item: CatalogItem(id: "ITEM_ID"))
```

{% endtab %}

{% tab title="Objective-C" %}

```objective-c
[PushSDKUserProfile saveItemWithItem:
    [[CatalogItem alloc] initWithId:@"ITEM_ID"]
];
```

{% endtab %}
{% endtabs %}

### Unsave Item

Use this event when a user removes an item from the saved list, favorites, or bookmarks.

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.UserProfile.unsaveItem(
    item: CatalogItem(id: "ITEM_ID")
)
```

{% endtab %}

{% tab title="Objective-C" %}

```objective-c
[PushSDKUserProfile unsaveItemWithItem:
    [[CatalogItem alloc] initWithId:@"ITEM_ID"]
];
```

{% endtab %}
{% endtabs %}

### Complete Item

A user marks an item as completed after finishing its intended experience.

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.UserProfile.completeItem(
    item: CatalogItem(id: "ITEM_ID")
)
```

{% endtab %}

{% tab title="Objective-C" %}

```objective-c
[PushSDKUserProfile completeItemWithItem:
    [[CatalogItem alloc] initWithId:@"ITEM_ID"]
];
```

{% endtab %}
{% endtabs %}

### Uncomplete Item

Use this event to revert the completion of an item.

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.UserProfile.uncompleteItem(
    item: CatalogItem(id: "ITEM_ID")
)
```

{% endtab %}

{% tab title="Objective-C" %}

```objective-c
[PushSDKUserProfile uncompleteItemWithItem:
    [[CatalogItem alloc] initWithId:@"ITEM_ID"]
];
```

{% endtab %}
{% endtabs %}

### Rate Item

The `rate_item` event includes an optional `rating` property. If provided, `rating` must be a numeric value between **0 and 100** inclusive, with **up to 1 decimal place**.

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.UserProfile.rateItem(
    item: CatalogItem(id: "ITEM_ID", rating: 95)
)
```

{% endtab %}

{% tab title="Objective-C" %}

```objective-c
CatalogItem *item = [[CatalogItem alloc] initWithId:@"ITEM_ID" rating:@95];
[PushSDKUserProfile rateItemWithItem:item];
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
The `rating` value should reflect your product's native scale — use whatever range makes sense for your use case (e.g., 1–5 for a star rating, 0–10 for a score, 1–100 for a percentage-style rating). The only requirement is that the value falls within 0–100 and has no more than 1 decimal place.
{% endhint %}

### Unrate Item

Use this event when a user removes the rating from an item.

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.UserProfile.unrateItem(
    item: CatalogItem(id: "ITEM_ID")
)
```

{% endtab %}

{% tab title="Objective-C" %}

```objective-c
[PushSDKUserProfile unrateItemWithItem:
    [[CatalogItem alloc] initWithId:@"ITEM_ID"]
];
```

{% endtab %}
{% endtabs %}

### Add To Cart

Send this event whenever a user adds one or more items to their cart. Pushly will accumulate cart state across multiple calls.

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.UserProfile.addToCart(items: [
    CatalogItem(id: "ITEM_ID", quantity: 2)
])
```

{% endtab %}

{% tab title="Objective-C" %}

```objective-c
NSArray *items = @[
    [[CatalogItem alloc] initWithId:@"ITEM_ID" quantity:2]
];
[PushSDKUserProfile addToCartWithItems:items];
```

{% endtab %}
{% endtabs %}

You may call `add_to_cart` as many times as necessary to keep track of all items in a visitor's cart. Abandoned Cart notifications may be sent for any item in a customer's cart that has not been purchased.

After a purchase is made the visitor's cart will be emptied and no notifications for a purchased item will be sent to the visitor.

### Update Cart

Use this event when a user changes their cart without completing a purchase, such as adjusting quantities or removing items.

To remove an item from the cart call the `update_cart` method with the full current cart information (omitting the removed item):

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.UserProfile.updateCart(withItems: [
    CatalogItem(id: "ITEM_1", quantity: 1),
    CatalogItem(id: "ITEM_2", quantity: 2)
])
```

{% endtab %}

{% tab title="Objective-C" %}

```objective-c
NSArray *items = @[
    [[CatalogItem alloc] initWithId:@"ITEM_1" quantity:1],
    [[CatalogItem alloc] initWithId:@"ITEM_2" quantity:2]
];
[PushSDKUserProfile updateCartWithItems:items];
```

{% endtab %}
{% endtabs %}

Or if the cart has been completely emptied provide an empty array:

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.UserProfile.updateCart(withItems: [])
```

{% endtab %}

{% tab title="Objective-C" %}

```objective-c
[PushSDKUserProfile updateCartWithItems:@[]];
```

{% endtab %}
{% endtabs %}

### Purchase

Send this event after a successful checkout. This clears abandoned cart state and enables revenue attribution.

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.UserProfile.trackPurchase(
    of: [
        CatalogItem(id: "ITEM_1"),
        CatalogItem(id: "ITEM_2", quantity: 2)
    ],
    withPurchaseId: "ORDER_123",
    withPriceValue: "49.99"
)
```

{% endtab %}

{% tab title="Objective-C" %}

```objective-c
NSArray *items = @[
    [[CatalogItem alloc] initWithId:@"ITEM_1"],
    [[CatalogItem alloc] initWithId:@"ITEM_2" quantity:2]
];

[PushSDKUserProfile trackPurchaseWithItems:items
                      withPurchaseId:@"ORDER_123"
                     withPriceValue:@"49.99"];
```

{% endtab %}
{% endtabs %}

#### Purchase Fields

* **priceValue** – Total purchase amount, in the currency configured for the domain
* **purchaseId** – Unique order identifier

If a `purchase` event is sent without item data, Pushly will still clear the user’s cart state.

#### Currency handling

`price_value` must be provided in the currency configured for the domain in the platform’s domain settings.

Use a period (`.`) as the decimal separator for all currencies, regardless of locale. Do not use commas.

Examples:

* USD: `"344.33"`
* JPY: `"5000"`

### Required Fields Summary

* `id` is required for all items
* `quantity` is required only when tracking cart or purchase quantities
* `rating` is required only when rating an item

### Implementation Best Practices

Initialize the SDK early in the page lifecycle to avoid missed events.

Use consistent item IDs across all interaction types so Pushly can correctly associate behavior.

Trigger `purchase` events from a reliable confirmation step whenever possible to avoid false abandoned cart notifications.


# Advanced

{% content-ref url="/pages/yXRgt7NVZZdxLEkdePsA" %}
[Self-Managed Integration](/integration/implementation-steps/apple-ios/advanced/self-managed-integration)
{% endcontent-ref %}


# Self-Managed Integration

To make integrations easier the PushSDK automatically integrates itself into your application delegate and user notification center by using method swizzling. You may want to disable the automatic integration in several scenarios including:

* Conflicts with other SDKs that also include swizzling or other automatic integrations
* Conflicting third party development solutions
* Our method swizzling conflicts with your existing architecture

{% hint style="info" %}
We suggest that you only consider a self-managed integration if swizzling is incompatible with existing code. If you choose to use a self-managed integration you will need to ensure that any new releases to the PushSDK do not add additional methods that must be called manually.
{% endhint %}

Add the following method call **before** calling `PushSDK.setConfiguration`:

{% tabs %}
{% tab title="Swift" %}

```swift
PushSDK.disableMethodSwizzling()
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
[PushSDK disableMethodSwizzling];
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
This **must** be invoked before calling\
`PushSDK.setConfiguration(appKey: myAppKey, withLaunchOptions: launchOptions)`
{% endhint %}

### Why the ordering matters

`disableMethodSwizzling()` only prevents the hooks from being **installed**. Installation happens during `setConfiguration`, so calling it afterwards prevents nothing:

* The hooks are already in place and continue to forward every notification callback.
* The SDK claims the `UNUserNotificationCenter` delegate slot during installation, and `UNUserNotificationCenter` provides no way to hand that slot back. The SDK therefore remains the notification delegate for the rest of the process lifetime.

If you are debugging a self-managed integration that behaves as though swizzling is still active, check that nothing runs `setConfiguration` ahead of your `disableMethodSwizzling()` call — including any code path that configures the SDK from a different entry point.

### Never combine automatic and manual integration

{% hint style="danger" %}
The automatic and self-managed integrations are mutually exclusive. If the hooks are installed and your app **also** calls the manual entry points below, every notification is processed **twice** — the installed hook and your own call both run the same code path, producing duplicate impressions and events.
{% endhint %}

Double processing surfaces in the device log as a duplicate completion handler invocation:

```
[PNUserNotificationCenter] willPresent completionHandler already called — ignoring duplicate invocation
[PNUserNotificationCenter] didReceive completionHandler already called — ignoring duplicate invocation
[PNApplication] completionHandler already called — ignoring duplicate invocation
```

Choose one integration style and use it consistently:

* **Automatic (default)** — do not call any of the manual methods listed below.
* **Self-managed** — call `disableMethodSwizzling()` before `setConfiguration`, then implement every manual method listed below.

Once swizzling has been disabled you'll also be required to place calls to the PushSDK to invoke the methods that are no longer swizzled. If you do not implement all of the following methods the PushSDK may not function properly.

The methods you must call manually are:

* `application:didRegisterForRemoteNotificationsWithDeviceToken:`
* `application:didFailToRegisterForRemoteNotificationsWithError:`
* `application:didReceiveRemoteNotification:fetchCompletionHandler:`
* `userNotificationCenter:willPresent:withCompletionHandler:`
* `userNotificationCenter:didReceive:withCompletionHandler:`
* If you are targeting iOS version 10 or earlier:
  * `application:didReceiveRemoteNotification:`

### UIApplicationDelegate Example

{% tabs %}
{% tab title="Swift" %}

```swift
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    PushSDK.disableMethodSwizzling()

    PushSDK.setConfiguration(appKey: "REPLACE_WITH_SDK_KEY", withLaunchOptions: launchOptions)

    PushSDK.showNativeNotificationPermissionPrompt() { granted, settings, error in
        // optional callback
        print("User accepted permissions: \(granted)")
    }

    return true
}

func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
    PushSDK.application(application, didRegisterForRemoteNotificationsWithDeviceToken: deviceToken)
}

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
    PushSDK.application(application, didFailToRegisterForRemoteNotificationsWithError: error)
}

func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    PushSDK.application(application, didReceiveRemoteNotification: userInfo) { result in
        // Capture the PushSDK UIBackgroundFetchResult, modify as needed for your own logic
        completionHandler(result)
    }
}

// For integrations targeting iOS version 10 or earlier
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any]) {
    PushSDK.application(application, didReceiveRemoteNotification: userInfo)
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    [PushSDK disableMethodSwizzling];

    [PushSDK setConfigurationAppKey:@"REPLACE_WITH_SDK_KEY" withLaunchOptions:launchOptions];

    [PushSDK showNativeNotificationPermissionPrompt:^(BOOL granted, UNNotificationSettings * _Nonnull settings, NSError * _Nullable error) {
        NSLog(@"User accepted permissions: %@", granted ? @"YES" : @"NO");
    }];

    return YES;
}

- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
{
    [PushSDK application:application didRegisterForRemoteNotificationsWithDeviceToken:deviceToken];
}

- (void)application:(UIApplication *)application didFailToRegisterForRemoteNotificationsWithError:(NSError *)error
{
    [PushSDK application:application didFailToRegisterForRemoteNotificationsWithError: error];
}

- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler
{
    [PushSDK application:application didReceiveRemoteNotification:userInfo fetchCompletionHandler:^(UIBackgroundFetchResult result) {
        // Capture the PushSDK UIBackgroundFetchResult, modify as needed for your own logic
        completionHandler(result);
    }];
}

// For integrations targeting iOS version 10 or earlier
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)userInfo
{
    [PushSDK application:application didReceiveRemoteNotification:userInfo];
}
```

{% endtab %}
{% endtabs %}

### UNUserNotificationCenterDelegate Example

{% tabs %}
{% tab title="Swift" %}

```swift
public func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    PushSDK.userNotificationCenter(center, willPresent: notification) { options in
        // Capture the PushSDK UNNotificationPresentationOptions, modify as needed for your own logic
        completionHandler(options)
    }

}

public func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
    PushSDK.userNotificationCenter(center, didReceive: response) {
        completionHandler()
    }
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler
{
    [PushSDK userNotificationCenter:center willPresent:notification withCompletionHandler:^(UNNotificationPresentationOptions options) {
        // Capture the PushSDK UNNotificationPresentationOptions, modify as needed for your own logic
        completionHandler(options);
    }];    
}

- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler
{
    [PushSDK userNotificationCenter:center didReceive:response withCompletionHandler:^{
        completionHandler();
    }];
}
```

{% endtab %}
{% endtabs %}


# iOS SDK Changelog

List of updates to the iOS SDK.

{% updates format="full" %}
{% update date="2026-07-30" %}

## 1.7.1

#### Fixed

* Rich notification images, titles, and bodies supplied by a content webhook now render. They were previously discarded.
* Notification destination routing now behaves the same whether the app was foregrounded, backgrounded, or terminated.
* Improved delivery reliability for subscription and analytics events during network or server disruptions.
  {% endupdate %}

{% update date="2026-06-29" %}

## 1.6.5

Layout improvements and additional logging for app messages
{% endupdate %}

{% update date="2026-06-23" %}

## 1.6.4

#### Adds

* More comprehensive on-device debug logging for app messages.
* Improved reliability for recording app message impressions.
  {% endupdate %}

{% update date="2026-05-19" %}

## 1.6.3

#### Fixes

* Fixed an issue that caused the Prompt Debug modal to show randomly.
  {% endupdate %}

{% update date="2026-05-11" %}

## 1.6.2

#### Adds

* Immediate state for ecomm events

#### Fixes

* Granted state for ReAuth flow (picks up authorization state if app was previously granted notification permissions)

#### Updates

* Logs from NSLog to OSLog
  {% endupdate %}

{% update date="2026-04-14" %}

## 1.6.1

* Fixed: Resolved a race condition crash when method swizzling is enabled alongside another library that intercepts the same notification callbacks.
  {% endupdate %}

{% update date="2026-03-12" %}

## 1.6.0

* Adds support for Catalog `RATE_ITEM` and `COMPLETE_ITEM` actions. See the [Catalog Documentation](/integration/implementation-steps/apple-ios/sdk-swift-obj-c/e-commerce-support) for more information.
  {% endupdate %}

{% update date="2026-02-17" %}

## 1.5.2

* Fixed: Sorting issue with multiple immediate app messages.
  {% endupdate %}

{% update date="2026-01-08" %}

## 1.5.1

* Cold boot start notification handling for hybrid SDKs.
  {% endupdate %}

{% update date="2025-11-05" %}

## 1.5.0

* Adds support for Catalog `VIEW_ITEM` and `SAVE_ITEM` actions.
  {% endupdate %}

{% update date="2025-10-14" %}

## 1.4.0

#### Added

* Added support for the **immediate** display condition to [App Messages](/platform/app-messages).

#### Fixed

* Fixed an issue with App Message global frequency cap calculations.
  {% endupdate %}

{% update date="2025-01-21" %}

## 1.3.9

#### Added

* Added support for push-to-start token registration and observation to [Live Activities](/integration/implementation-steps/apple-ios/sdk-swift-obj-c/live-activities).

#### Updated

* Updates `PNNotification.meta` structure to allow more developer friendly value access patterns.

#### **Live Activities - New Methods**

* `PushSDK.LiveActivities.register(_ activityType:, activityHandler:)`
  * Starts observing the provided `activityType` for push-to-start token updates and automatically handles registration. For each activity within the provided `activityType` started via push, or already running, the `activityHandler` is invoked.
* `PushSDK.LiveActivities.register(_ activity:, withId:)`
  * Starts observing the provided `activity` for push token updates and automatically handles registration for the provided activity ID.
    {% endupdate %}

{% update date="2024-12-11" %}

## 1.3.8

#### Added

* Added support for `PNNotification.meta` fields.
  {% endupdate %}

{% update date="2024-07-17" %}

## 1.3.7

#### Added

* Added support for App Message animations.

#### Updated

* Updated internal optimizations on tracked events.
  {% endupdate %}

{% update date="2024-04-23" %}

## 1.3.6

#### Added

* Added session heartbeat support.
* Added application version tracking.

#### Updated

* Updated internal event meta structure.
* Updated PushNotification pause and resume event tracking priority.

#### Fixed

* Fixed an issue with order of cached event hydration.
  {% endupdate %}

{% update date="2024-03-25" %}

## 1.3.5

#### Fixed

* Fixed an issue that could cause background notification registrations to send subscription events.
  {% endupdate %}

{% update date="2024-03-18" %}

## 1.3.4

#### Updated

* Updated event tracking deduplication processes.

#### Fixed

* Fixed an issue that could cause app crash when the number of pending events was too large.
  {% endupdate %}

{% update date="2024-03-12" %}

## 1.3.3

#### Added

* Added privacy manifest.
  {% endupdate %}

{% update date="2024-03-12" %}

## 1.3.2

#### Updated

* Updated App Message serialization for React Native and Flutter consumers.
  {% endupdate %}

{% update date="2024-03-06" %}

## 1.3.1

#### Added

* Added more extensive internal logging.

#### Fixed

* Removed dependencies on `NWPathMonitor`.
  {% endupdate %}

{% update date="2024-03-06" %}

## 1.3.0

#### Added

* Added support for App Messages.
  {% endupdate %}

{% update date="2024-03-07" %}

## 1.2.2

#### Added

* Added more extensive internal logging.

#### Fixed

* Removed dependencies on `NWPathMonitor`.
  {% endupdate %}

{% update date="2024-02-16" %}

## 1.2.1

#### Added

* Added support for pausing and resuming subscriber push notifications.

#### New Properties and Methods

* `PushSDK.PushNotifications.isPaused` - A boolean property indicating the subscriber's notifications are currently paused or not.
* `PushSDK.PushNotifications.pause()` - Will pause a subscriber's ability to receive notifications.
* `PushSDK.PushNotifications.resume()` - Will resume a subscriber's ability to receive notifications.

#### Moved / Deprecated Properties and Methods

* `PushSDK.UserProfile.isSubscribed` has been moved to `PushSDK.PushNotifications.isSubscribed` and will now display a deprecation warning.
* `PushSDK.UserProfile.isEligibleToPrompt` has been moved to `PushSDK.PushNotifications.isEligibleToPrompt` and will now display a deprecation warning.
* `PushSDK.showNativeNotificationPermissionPrompt()` has been aliased to `PushSDK.PushNotifications.showPermissionPrompt()`.
* `PushSDK.UserProfile.revertUserDeletion` will now warn and will be removed in a future release.
  {% endupdate %}

{% update date="2024-01-24" %}

## 1.2.0

#### Added

* Added support for LiveActivities.

#### Live Activities - New Module

* `PushSDK.LiveActivities.track(token:, forActivity:)`
  * Registers the provided pushToUpdate token on the current user to allow updates to the provided activity ID via push.
    {% endupdate %}

{% update date="2023-10-19" %}

## 1.1.5

#### Updated

* Updated internal event tracking metadata.
  {% endupdate %}

{% update date="2023-10-18" %}

## 1.1.4

#### Added

* Added support for [managed integrations](/integration/implementation-steps/apple-ios/advanced/self-managed-integration).
  {% endupdate %}

{% update date="2023-08-29" %}

## 1.1.3

#### Updated

* Updated internal resource caching policies.
  {% endupdate %}

{% update date="2023-07-26" %}

## 1.1.2

#### Added

* Added a convenience service for copying the PushSDK Anonymous ID based on specific user interaction.
  {% endupdate %}

{% update date="2023-07-18" %}

## 1.1.1

#### Updated

* Updated error reporting details and internal resource caching.
* Updated debug logging capabilities.
  {% endupdate %}

{% update date="2023-08-29" %}

## 1.0.15

#### Updated

* Updated internal resource caching policies.
  {% endupdate %}

{% update date="2023-07-26" %}

## 1.0.14

#### Added

* Added a convenience service for copying the PushSDK Anonymous ID based on specific user interaction.
  {% endupdate %}

{% update date="2023-07-26" %}

## 1.0.13

#### Updated

* Updated error reporting and internal resource caching.
  {% endupdate %}

{% update date="2023-06-06" %}

## 1.1.0

#### Updated

* Updated error reporting details.

#### Breaking Changes

* Removed automatic badge clearing behavior on app open or resume.
* Updated swizzled methods to call through to the original implementation if present.
  {% endupdate %}
  {% endupdates %}

### Closed Beta Releases

{% updates format="full" %}
{% update date="2023-05-24" %}

## 1.0.12

#### Updated

* Updated the stream ordering of tracked events related to user profiles.
  {% endupdate %}

{% update date="2023-05-17" %}

## 1.0.11

#### Updated

* Updated how `PushSDK.UserProfile.isSubscribed` is reported.
* Updated simulator functionality.
  {% endupdate %}

{% update date="2023-04-20" %}

## 1.0.10

#### Fixed

* Fixed an issue with `PushSDK.UserProfile.remove()` event tracking.
  {% endupdate %}

{% update date="2023-04-12" %}

## 1.0.9

#### Updated

* Updated internal version tracking.
  {% endupdate %}

{% update date="2023-03-16" %}

## 1.0.8

#### Updated

* Updated how resubscribes are tracked and includes simulator builds.
  {% endupdate %}

{% update date="2023-03-13" %}

## 1.0.7

#### Fixed

* Fixed Xcode warnings regarding `linking against a dylib which is not safe for use in application extensions`.
  {% endupdate %}
  {% endupdates %}


# Android

## Step 1: Firebase App Setup

All apps must have a Firebase project and matching credentials to capture subscriptions and send notifications. Follow the link below to create a Firebase project for your app and add the credentials to our platform.

{% content-ref url="/pages/knmp0V2urDB4krmIMd00" %}
[Firebase App Setup](/integration/implementation-steps/android/firebase-app-setup)
{% endcontent-ref %}

## Step 2: Integrate Push SDK

Once the Firebase credentials have been uploaded to the platform you can integrate the Push SDK into your app. Choose the appropriate SDK from the list below for implementation instructions.

{% content-ref url="/pages/67YAqvQYbTYJsGXS775z" %}
[SDK: Kotlin / Java](/integration/implementation-steps/android/sdk-kotlin-java)
{% endcontent-ref %}


# Firebase App Setup

Instructions for creating the required credentials to enable Android notifications

## Firebase App

First, go to the [<mark style="color:blue;">Firebase console</mark>](https://console.firebase.google.com/), log in, and select your existing project or add a new project.

Within your project click the gear icon next to the `Project Overview` section in the sidebar and select `General`.

<figure><img src="/files/Bn2m9I6sW2Q3isoZO2U3" alt=""><figcaption></figcaption></figure>

### Firebase Cloud Message API (V1) Setup

#### Find your Sender ID (Project ID)

On the `General` tab find the `Project number`. Use this value as the Firebase `Sender ID`.&#x20;

<figure><img src="/files/wB6ZkKZCEPk7ksU7rZpb" alt=""><figcaption></figcaption></figure>

{% hint style="info" %}
Firebase uses the project number as its Sender ID.
{% endhint %}

#### Download the service account JSON

Open the `Service accounts` tab.

Under `Firebase Admin SDK`, click `Generate new private key`.

In the confirmation dialog, click `Generate key`. Your browser downloads the service account JSON file.

{% hint style="warning" %}
Keep this file private. It contains credentials for your Firebase project.
{% endhint %}

<figure><img src="/files/TCA6E1r8YQrAm4AlpjBb" alt=""><figcaption></figcaption></figure>

#### Enable Integration in Platform

Log into the platform, navigate to the `Settings` page, and locate the `Send Integrations` section.

From here, click the `Edit` button in the `Send Integration` header, enable the `Native` switch under the `Android` heading and:

1. Enter the Firebase `Sender ID` that you retrieved earlier.
2. Click the `Upload your Service Account JSON File` area and choose the file you downloaded.
3. Click the `Submit` button

![](/files/fHH6Em1JtIPE7rn1oCOB)

If everything was entered correctly you should now see a section underneath `Send Integrations` that shows an active Android Native integration.

<figure><img src="/files/wemCIIJSWBTnIN5f3OrP" alt=""><figcaption></figcaption></figure>

Once this step is complete you can continue with implementation of the SDK into your app

### Cloud Messaging API (Legacy / Deprecated)

{% hint style="danger" %}
As of June 20, 2023, [Google has deprecated](https://firebase.google.com/support/faq#fcm-23-deprecation) the legacy FCM APIs and sending messages using them will no longer function starting June 2024.

As-such we recommend using the V1 instructions above to avoid interruptions in your future notification delivery.
{% endhint %}

If the `Cloud Messaging API (Legacy)` is marked as disabled you will need to enable it.

<figure><img src="/files/C6VmtIvG95fgGgc4FOlM" alt=""><figcaption></figcaption></figure>

Follow these steps:

* Click on the 3 dots next to `Cloud Messaging API (Legacy)`
* Select `Manage API on Google Cloud Console`
* Click `Enable` on the tab that opened
* After the page reloads you can close this tab and return to the Firebase Console
* On the Firebase Console tab refresh the page to reflect the enabled API

Once the legacy API has been enabled the `Server Key` and `Sender ID` will be displayed. Copy down each of these values and proceed to the Pushly platform.

Log into the platform, navigate to the `Settings` page, and locate the `Send Integrations` section.

From here, click the `Edit` button in the `Send Integration` header, enable the `Native` switch under the `Android` heading and:

1. Enter the Firebase `Sender ID`
2. Choose `Legacy` from the pill selection
3. Enter the `Server Key`
4. Click the `Submit` button

<figure><img src="/files/xz6lgG8t4vFmzTCEp5P5" alt=""><figcaption></figcaption></figure>

If everything was entered correctly you should now see a section underneath `Send Integrations` that shows an active Android Native integration.

<figure><img src="/files/wemCIIJSWBTnIN5f3OrP" alt=""><figcaption></figcaption></figure>

Once this step is complete you can continue with implementation of the SDK into your app


# SDK: Kotlin / Java

## Prerequisites

Before you can integrate the SDK with your app the following requirements must be met:

* Android device or emulator running API 19+ (Android 4.4) with Google Play Store Services installed.
* An app that uses [<mark style="color:blue;">Jetpack (AndroidX)</mark>](https://developer.android.com/jetpack/androidx/migrate)
* Firebase Sender ID and Server API Key or Service Account. If you do not already have these follow [<mark style="color:blue;">our documentation</mark>](/integration/implementation-steps/android/firebase-app-setup) to generate them.
* SDK Key: Obtained from the Pushly Platform in the `Domain Settings` page under the `Overview` > `Details` > `Domain Information` section.

{% hint style="warning" %}
This guide assumes that you have already [<mark style="color:blue;">implemented Firebase Messaging</mark>](https://firebase.google.com/docs/android/setup) in your application and have also set up a MainApplication class and linked it in your `AndroidManifest.xml's <application>` tag.
{% endhint %}

## Step 1: Import the PushSDK Framework

Add the following to the `dependencies` section of your `app/build.gradle` file:

{% tabs %}
{% tab title="app/build.gradle" %}

```gradle
dependencies {
    ...
    implementation 'com.pushly.android:pushsdk:[1.0, 2.0['
}
```

{% endtab %}
{% endtabs %}

## Step 2: Initialize the SDK

Add the following `import` to your app's main `Application` class:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import com.pushly.android.PushSDK
```

{% endtab %}

{% tab title="Java" %}

```java
import com.pushly.android.PushSDK;
```

{% endtab %}
{% endtabs %}

And then add the following lines inside of the `onCreate` method.&#x20;

Replace the `REPLACE_WITH_SDK_KEY` in the `setConfiguration` method with the SDK Key from the platform settings page.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.logLevel = PNLogLevel.INFO
PushSDK.setConfiguration(appKey="REPLACE_WITH_SDK_KEY", context=this)

PushSDK.PushNotifications.showPermissionPrompt(completion={ granted, response, error ->
    error?.let {
        println("Error encountered in permission request: $error")
        return@showNativeNotificationPermissionPrompt
    }

    println("Permissions granted: $granted, response: $response")
})
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.setLogLevel(PNLogLevel.INFO);
PushSDK.setConfiguration("REPLACE_WITH_SDK_KEY", this);

PushSDK.PushNotifications.showPermissionPrompt((granted, response, error) -> {
    if (error != null) {
        System.out.printf("Error encountered in permission request: %s", error);
    } else {
        System.out.printf("Permissions granted: %b, response: %s", granted, response);
    }

    return null;
});
```

{% endtab %}
{% endtabs %}

## Step 3: Test the Implementation

Run your app on an Android device or emulator (ensuring the emulator has `Google Play Store Services` installed) to make sure it builds correctly.

The code you added in the previous step will show the push permission dialog upon app open. This can be customized by using [<mark style="color:blue;">SDK methods</mark>](/integration/implementation-steps/android/sdk-kotlin-java/sdk-methods#manually-triggering-the-permission-dialog) to control when the dialog shows.

After accepting the dialog, log into the platform and navigate to `Notifications > Create Notification` and send your first notification, targeting the **Native: Android** channel, to your device.

## Next Steps

Once you have confirmed the SDK is working properly you may continue to add additional optional functionality like:

* [<mark style="color:blue;">Handling notification opens / app links / deep linking</mark>](/integration/implementation-steps/android/sdk-kotlin-java/deep-linking)
* [<mark style="color:blue;">Attaching attributes to a subscriber's profile</mark>](/integration/implementation-steps/android/sdk-kotlin-java/sdk-methods#adding-attributes-to-a-subscribers-profile)
* [<mark style="color:blue;">Sending information about what content a subscriber interacts with</mark>](/integration/implementation-steps/android/sdk-kotlin-java/sdk-methods#activity-tracking-url-screen-visits)


# Deep Linking

Overview of how the SDK handles deep links and how to perform custom navigation within your application

The PushSDK handles opening of a notification in the following ways:

* By default, the SDK will automatically create a new `ACTION_VIEW` intent using the landing URL attached to the notification.
* If you implement the `onPushSDKDidReceiveNotificationDestination` lifecycle callback you can perform any custom logic needed when the notification is opened.

## Implementing URL Handling Callbacks

The PushSDK provides callbacks that will be called when a subscriber interacts with a notification. These callbacks should be implemented so that the subscriber is automatically navigated to the proper activity after opening the notification.

{% hint style="danger" %}
We recommend putting all SDK callbacks within your application's main `onCreate` method to ensure proper handling of all notification events.
{% endhint %}

When the SDK receives a notification open event it will attempt to call one of the below callbacks with parameters that you can use to navigate the subscriber to the appropriate activity within your app.&#x20;

In order to implement these callbacks you must have a class that calls the SDK's `registerNotificationLifecycleCallbacks` method and implements the callback(s) within it.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.registerNotificationLifecycleCallbacks(object : PNNotificationLifecycleCallbacks {
    // Add function overrides here
})
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.registerNotificationLifecycleCallbacks(new PNNotificationLifecycleCallbacks() {
    // Add function overrides here
});
```

{% endtab %}
{% endtabs %}

For example, if a landing URL was attached to the notification and you implemented the callback to handle subscriber navigation within your `onCreate` method the code may look like this:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
import com.pushly.android.PushSDK
import com.pushly.android.PushSDK.PNNotificationLifecycleCallbacks
import com.pushly.android.PushSDK.PNNotificationInteraction

class MainApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        PushSDK.logLevel = PNLogLevel.INFO
        PushSDK.setConfiguration(appKey = "YOUR_APP_KEY", context = this)

        PushSDK.PushNotifications.showPermissionPrompt(completion = { granted, response, error ->
            error?.let {
                println("Error encountered in permission request: $error")
                return@showNativeNotificationPermissionPrompt
            }

            println("Permissions granted: $granted, response: $response")
        })

        PushSDK.registerNotificationLifecycleCallbacks(object : PNNotificationLifecycleCallbacks {
            override fun onPushSDKDidReceiveNotificationDestination(
                destination: String,
                interaction: PNNotificationInteraction
            ): Boolean {
                // Navigate to a activity within your application using the destination (the Landing URL)

                // Return true to inform the SDK that it should not navigate
                return true
            }
        })
    }

}
```

{% endtab %}

{% tab title="Java" %}

```java
import com.pushly.android.PushSDK;
import com.pushly.android.PushSDK.PNNotificationLifecycleCallbacks;
import com.pushly.android.PushSDK.PNNotificationInteraction;

class MainApplication extends Application {

    @Override()
    public void onCreate() {
        super.onCreate();

        PushSDK.setLogLevel(PNLogLevel.INFO);
        PushSDK.setConfiguration("YOUR_APP_KEY", this);

        PushSDK.PushNotifications.showPermissionPrompt((granted, response, error) -> {
            if (error != null) {
                System.out.printf("Error encountered in permission request: %s", error);
            } else {
                System.out.printf("Permissions granted: %b, response: %s" ,granted, response);
            }
            
            return null;
        });

        PushSDK.registerNotificationLifecycleCallbacks(new PNNotificationLifecycleCallbacks() {
            @Override
            public boolean onPushSDKDidReceiveNotificationDestination(@NonNull String destination, @NonNull PNNotificationInteraction pnNotificationInteraction) {
                // Navigate to a activity within your application using the destination (the Landing URL)

                // Return true to inform the SDK that it should not navigate
                return true;
            }
        });
    }

}
```

{% endtab %}
{% endtabs %}

## Notification Interaction Callbacks

### Notification Opened With URI/URL Destination

{% hint style="info" %}
Use this callback when you need to perform custom parsing and/or modification of the attached landing URL that wouldn't be properly handled by opening the URL directly.
{% endhint %}

This callback is invoked when a notification is opened that has destination landing URL attached. The subscriber should be navigated to the view that represents the provided landing URL.

This method expects a Boolean response. If the response is `true` then the SDK will perform no additional actions for this notification open. If the response is `false` then the SDK will attempt to navigate the subscriber to the destination URL.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
override fun onPushSDKDidReceiveNotificationDestination(
    destination: String,
    interaction: PNNotificationInteraction
): Boolean {
    // Navigate to a activity within your application using the destination (the Landing URL)

    // Return true to inform the SDK that it should not navigate
    return true
}
```

{% endtab %}

{% tab title="Java" %}

```java
@Override
public boolean onPushSDKDidReceiveNotificationDestination(@NonNull String destination, @NonNull PNNotificationInteraction pnNotificationInteraction) {
    // Navigate to a activity within your application using the destination (the Landing URL)

    // Return true to inform the SDK that it should not navigate
    return true;
}
```

{% endtab %}
{% endtabs %}


# Activity Tracking

How to implement activity tracking within your application to enable segmentation based on content views

## Overview

The PushSDK provides methods that allow you to track the type of content a subscriber is viewing within your application. This information can then be used in the Pushly platform to create cohorts of subscribers that have visited specific URLs  or screens with tags based on the number of visits and visit recency.

For example: "Subscribers who have visited a page/screen tagged with "Astrology" at least 4 times in the last 30 days".

## Methods

### UserProfile.trackActivity

To track a URL or screen visited along with its keyword/tag metadata:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.UserProfile.trackActivity(name="myapp://dashboard", tags=listOf("my-tag"))
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.UserProfile.trackActivity("myapp://dashboard", List.of("my-tag"));
```

{% endtab %}
{% endtabs %}

Or if there are no tags to be provided omit the `tags` parameter:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.UserProfile.trackActivity(name="myapp://sports/article-1")
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.UserProfile.trackActivity("https://pushly.com/article-1");
```

{% endtab %}
{% endtabs %}


# App Messages

A quick start guide to adding App Message support to your Android application with the Pushly PushSDK

App Messages, unlike Push Notifications and Live Activities, allow you to interact with and engage your user base without the need for a subscription token. App Message can be configured to automatically display based on a number of criteria including custom conditions that can be provided programmatically.

For App Messages with custom conditions the PushSDK provides convenience methods for you to signal when these conditions have been met as well as the ability to listen to certain lifecycle events.

## Prerequisites

Before you can integrate App Messages with the SDK the following requirements must be met:

* An app that uses [<mark style="color:blue;">Jetpack (AndroidX)</mark>](https://developer.android.com/jetpack/androidx/migrate)
* Android device or emulator running API 19+ (Android 4.4) with Google Play Store Services installed.
* PushSDK release supporting App Messages (See version support chart below)

## Step 1: Android SDK Setup

For applications **not** already using the PushSDK please see our Native App Push setup guide for [Android](/integration/implementation-steps/android).

For applications already using the PushSDK you might need to upgrade to a later version that includes App Message support. Use the following table to determine if you need to upgrade your installation.

<table><thead><tr><th width="191">SDK</th><th>Earliest Version with App Messages</th></tr></thead><tbody><tr><td><a href="/pages/67YAqvQYbTYJsGXS775z">Java / Kotlin</a></td><td>1.2.0+</td></tr><tr><td><a href="/pages/8V197NuJkTKCIk3qjzLC">React Native SDK</a></td><td>1.2.0+</td></tr><tr><td><a href="/pages/yewLFm3emng3NkOHaLKq">Flutter SDK</a></td><td>1.2.0+</td></tr></tbody></table>

{% tabs %}
{% tab title="app/build.gradle" %}

* Ensure you are targeting `com.pushly.android:pushsdk:[1.2, 1.3[` in your app gradle.
* Run gradle sync.

```gradle
dependencies {
    ...
    implementation 'com.pushly.android:pushsdk:[1.2, 1.3['
}
```

{% endtab %}
{% endtabs %}

## Step 2: Create and Schedule an App Message

Use the Pushly Platform to build and schedule your first [App Message](/platform/app-messages).

## Step 3: (Optional) Trigger Your App Message's Custom Condition

When using custom conditions the PushSDK is used to provide these conditions programmatically which then triggers an evaluation of which App Messages are currently active and match the provided conditions.

In most cases a single condition is triggered and evaluated at a time. For these situations you would use the `trigger(condition: String, value: String)` method signature.

{% tabs %}
{% tab title="Kotlin" %}
{% code fullWidth="false" %}

```kotlin
PushSDK.AppMessages.trigger(condition = "enabled_subscription", value = "true")
```

{% endcode %}
{% endtab %}

{% tab title="Java" %}

```java
PushSDK.AppMessages.trigger("enabled_subscription", "true");
```

{% endtab %}
{% endtabs %}

However, there might be situations where it is more convenient to trigger multiple conditions at once for evaluation. In these cases you can pass a HashMap containing any number of conditions to the `trigger(conditions: HashMap<String, String>)` method signature.

{% tabs %}
{% tab title="Kotlin" %}
{% code fullWidth="false" %}

```kotlin
PushSDK.AppMessages.trigger(conditions = hashMapOf(
    "enabled_subscription" to "true",
    "subscriber_tier" to "premium"
))
```

{% endcode %}
{% endtab %}

{% tab title="Java" %}

```java
PushSDK.AppMessages.trigger(Map.of(
    "enabled_subscription", "true",
    "subscriber_tier", "premium"
));
```

{% endtab %}
{% endtabs %}

## Step 4: (Optional) Listen to App Message Lifecycle Events

See our [SDK Callbacks](/integration/implementation-steps/android/sdk-kotlin-java/sdk-callbacks#app-message-lifecycle-callbacks) documentation for information on handling lifecycle events of your App Messages.


# Identity Synchronization

Information on how to link your identifier for the subscriber to our subscriber profile.

## Setting the Subscriber's External ID

Providing a unique User ID for your subscriber allows you to later interact with that subscriber via the API.

Once a subscriber's external ID has been set you may work with our team to set up bi-directional automated data syncs. The following use cases are common ways that publishers use the external ID via server-to-server requests:

* Sending notifications to individual subscribers programmatically
* Processing unsubscribe requests
* Adding and removing profile attributes and events to subscribers

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.UserProfile.externalId = "external-id"
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.UserProfile.setExternalId("external-id");
```

{% endtab %}
{% endtabs %}

You can also check to see if the subscriber is already tagged with an External ID:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val currentExternalId = PushSDK.UserProfile.externalId
```

{% endtab %}

{% tab title="Java" %}

```java
String currentExternalId = PushSDK.UserProfile.getExternalId();
```

{% endtab %}
{% endtabs %}

### External ID Restrictions

To prevent subscribers from being accidentally merged via External ID and to ensure integrity in  the subscriber documents, Pushly enforces the following restrictions upon External IDs:

* No empty or strings that contain white-space
* No identifiers that contain placeholder text: `[test, testing, example, guest, anon, anonymous, user, guest, visitor, n/a]`
* No common JavaScript fall-through words or keywords: \[`[Object ... ], undefined, NaN, Infinity, true, false]`
* Cannot be only punctuation/symbols, must contain alphanumeric characters: `[---, ***, -*-, -._+/=:@~]` &#x20;
* Identifiers should be between 3 and 256 characters in length

A [regex playground](https://regex101.com/r/2wEVEc/1) has been set up for testing the validity of External ID schema.


# SDK Methods

## Manually Triggering the Permission Dialog

You may choose to disable automatic triggering of the permission dialog via the platform. In this scenario you would choose to trigger the dialog based on your own criteria (eg: after a visitor interacts with a modal / soft prompt, visits a specific screen/page, etc).

The following code will manually trigger the dialog:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.PushNotifications.showPermissionPrompt(completion = { granted, response, error ->
    // Optional callback
    println("User accepted permissions: $granted")
}
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.PushNotifications.showPermissionPrompt((granted, response, error) -> {
    // Optional callback
    System.out.printf("User accepted permissions: %b", granted);
    return null;
});
```

{% endtab %}
{% endtabs %}

## Adding Attributes to a Subscriber's Profile

You can add attributes to a subscriber's profile and later perform segmentation based on those attributes. For example, you may want to tag visitors who are interested in a specific type of news (eg: politics, sports) so that you can target them with specific notifications.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.UserProfile.set(key="interests", value=listOf("politics", "news"))
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.UserProfile.set("interests", List.of("politics", "news"));
```

{% endtab %}
{% endtabs %}

If you want to set more than one attribute at a time you can also send a map of values. The following example sets both a `is_paying_subscriber` and `interests` attribute on the subscriber.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.UserProfile.set(data=hashMapOf(
  "is_paying_subscriber" to true,
  "interests" to listOf("politics", "news")
))
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.UserProfile.set(Map.of(
  "is_paying_subscriber", true,
  "interests", List.of("politics", "news")
));
```

{% endtab %}
{% endtabs %}

If you're storing the value of a property as an array you can use the `append` and `remove` methods to add or remove values:

{% tabs %}
{% tab title="Kotlin" %}

```swift
// add 'sports' to the subscriber's existing interests
PushSDK.UserProfile.append(key = "interests", values = listOf("sports"))

// remove 'fashion' and 'news' from the subscriber's interests
PushSDK.UserProfile.remove(key = "interests", values = listOf("fashion", "news"))
```

{% endtab %}

{% tab title="Java" %}

```java
// add 'sports' to the subscriber's existing interests
PushSDK.UserProfile.append("interests", List.of("sports"));

// remove 'fashion' and 'news' from the subscriber's interests
PushSDK.UserProfile.remove("interests", List.of("fashion", "news"));
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
Properties using the `append` and `remove` methods will only store the most recent 20 values provided.
{% endhint %}

## Activity Tracking (URL / Screen Visits)

To track the subscriber flow through your application you can use activity tracking. You can track via screen or URL and pass associated metadata/tags if desired.

This information can then be used in segmentation to create cohorts of subscribers that have visited specific URLs or screens with tags based on number of visits and recency. For example: "Subscribers who have visited a page/screen tagged with "Astrology" at least 4 times in the last 30 days".

{% tabs %}
{% tab title="Kotlin" %}
To track a URL or screen visited along with the keyword/tag metadata:

```kotlin
PushSDK.UserProfile.trackActivity(name="myapp://dashboard", tags=listOf("my-tag"))
```

Or if there are no tags to be provided omit the `tags` parameter:

```kotlin
PushSDK.UserProfile.trackActivity(name="https://www.pushly.com/article-1")
```

{% endtab %}

{% tab title="Java" %}
To track a URL or screen visited along with the keyword/tag metadata:

```java
PushSDK.UserProfile.trackActivity("myapp://dashboard", List.of("my-tag"));
```

Or if there are no tags to be provided omit the `2nd` parameter:

```java
PushSDK.UserProfile.trackActivity("https://pushly.com/article-1");
```

{% endtab %}
{% endtabs %}

## Retrieving Your Anonymous Push ID

The PushSDK automatically assigns an anonymous Push ID for event tracking and debugging purposes.

Run the following code to return the Push ID:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val pushId = PushSDK.UserProfile.anonymousId
```

{% endtab %}

{% tab title="Java" %}

```java
String pushId = PushSDK.UserProfile.getAnonymousId();
```

{% endtab %}
{% endtabs %}

## Setting the Subscriber's External ID

Providing a unique User ID for your subscriber allows you to later interact with that subscriber via the API.

Once a subscriber's external ID has been set you may work with our team to set up bi-directional automated data syncs. The following use cases are common ways that publishers use the external ID via server-to-server requests:

* Sending notifications to individual subscribers programmatically
* Processing unsubscribe requests
* Adding and removing profile attributes and events to subscribers

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.UserProfile.externalId = "external-id"
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.UserProfile.setExternalId("external-id");
```

{% endtab %}
{% endtabs %}

You can also check to see if the subscriber is already tagged with an External ID:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val currentExternalId = PushSDK.UserProfile.externalId
```

{% endtab %}

{% tab title="Java" %}

```java
String currentExternalId = PushSDK.UserProfile.getExternalId();
```

{% endtab %}
{% endtabs %}

## Determining if a Visitor is Already Subscribed

If you need to know if a user is already subscribed to notifications the following snippet can be used:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val subscribed = PushSDK.PushNotifications.isSubscribed
```

{% endtab %}

{% tab title="Java" %}

```java
Boolean subscribed = PushSDK.PushNotifications.isSubscribed();
```

{% endtab %}
{% endtabs %}

## Determining if a Visitor is Eligible to Prompt

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val isEligible = PushSDK.PushNotifications.isEligibleToPrompt
```

{% endtab %}

{% tab title="Java" %}

```java
Boolean isEligible = PushSDK.PushNotifications.isEligibleToPrompt();
```

{% endtab %}
{% endtabs %}

## Pausing / Resuming a User's Notifications (Soft Unsubscribe)

A user's notifications can be paused by calling the following method:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.PushNotifications.pause()
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.PushNotifications.pause();
```

{% endtab %}
{% endtabs %}

If the user's notifications should be resumed call the following method:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.PushNotifications.resume()
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.PushNotifications.resume();
```

{% endtab %}
{% endtabs %}

To check if the user's notifications are currently paused:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val isPaused = PushSDK.PushNotifications.isPaused
```

{% endtab %}

{% tab title="Java" %}

```java
Boolean isPaused = PushSDK.PushNotifications.isPaused();
```

{% endtab %}
{% endtabs %}

## Permanently Delete a User

If the user should be excluded from all notifications and tracking run the following method:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.UserProfile.requestUserDeletion()
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.UserProfile.requestUserDeletion();
```

{% endtab %}
{% endtabs %}

## Setting the SDK Log Level

The PushSDK log level is set to `NONE` by default. Supported log levels are: `VERBOSE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `CRITICAL`, and `NONE`.

To enable more detailed logging add the following code in your application's main `onCreate` method:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.logLevel = PNLogLevel.INFO
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.setLogLevel(PNLogLevel.INFO);
```

{% endtab %}
{% endtabs %}


# SDK Callbacks

{% hint style="danger" %}
We recommend putting all SDK callbacks within your application's main `onCreate` method to ensure proper handling of all events.
{% endhint %}

## SDK Lifecycle Callbacks

These callbacks can be implemented to observe SDK loading and exiting.

In order to implement these callbacks you must have a class that calls the SDK's `registerSDKLifecycleCallbacks` method and implements the callback(s) within it.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.registerPushSDKLifecycleCallbacks(object : PNPushSDKLifecycleCallbacks {
    // Add function overrides here
})
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.registerPushSDKLifecycleCallbacks(new PNPushSDKLifecycleCallbacks() {
    // Add function overrides here
});
```

{% endtab %}
{% endtabs %}

The following SDK Lifecycle callbacks are available:

### Finished Loading

This callback is executed when the SDK finishes its initialization and is ready for interaction.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
override fun onPushSDKDidFinishLoading(configuration: PNApplicationConfig, subscriberStatus: PNSubscriberStatus) {
    // Add code to execute after SDK finishes loading
}
```

{% endtab %}

{% tab title="Java" %}

```java
@Override
public void onPushSDKDidFinishLoading(@NonNull PNApplicationConfig configuration, @NonNull PNSubscriberStatus subscriberStatus) {
    // Add code to execute after SDK finishes loading
}
```

{% endtab %}
{% endtabs %}

## Permission Lifecycle Callbacks

These callbacks can be implemented to observe to a user's permission authorization changes.

In order to implement these callbacks you must have a class that calls the SDK's `registerPermissionLifecycleCallbacks` method and implements the callback(s) within it.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.registerPermissionLifecycleCallbacks(object : PNPermissionLifecycleCallbacks {
    // Add function overrides here
})
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.registerPermissionLifecycleCallbacks(new PNPermissionLifecycleCallbacks() {
    // Add function overrides here
});
```

{% endtab %}
{% endtabs %}

The following Permission Lifecycle callbacks are available:

### Permission Status Changed

This callback is executed when a user's permission status/authorization has changed.

The `response` variable represents the user's **new** permission status.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
override fun onPushSDKDidReceivePermissionResponse(response: PNPermissionResponse) {
    // Add code to execute after a user's notification permission has changed
}
```

{% endtab %}

{% tab title="Java" %}

```java
@Override
public void onPushSDKDidReceivePermissionResponse(@NonNull PNPermissionResponse response) {
    // Add code to execute after a user's notification permission has changed
}

```

{% endtab %}
{% endtabs %}

## Notification Lifecycle Callbacks

These callbacks can be implemented to observe events like impressions, opens, and other interactions with notifications.

In order to implement these callbacks you must have a class that calls the SDK's `registerNotificationLifecycleCallbacks` method and implements the callback(s) within it.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.registerNotificationLifecycleCallbacks(object : PNNotificationLifecycleCallbacks {
    // Add function overrides here
})
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.registerNotificationLifecycleCallbacks(new PNNotificationLifecycleCallbacks() {
    // Add function overrides here
});
```

{% endtab %}
{% endtabs %}

The following Notification Lifecycle callbacks are available:

### Notification Received

This callback is executed when a user receives a notification

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
override fun onPushSDKDidReceiveRemoteNotification(notification: PNNotification) {
    // Add code to execute after the subscriber's device receives a notification
}
```

{% endtab %}

{% tab title="Java" %}

```java
@Override
public void onPushSDKDidReceiveRemoteNotification(@NonNull PNNotification notification) {
    // Add code to execute after the subscriber's device receives a notification
}
```

{% endtab %}
{% endtabs %}

### Notification Opened With URI/URL Destination

This callback is executed when a notification is opened that has a landing URL attached. The subscriber should be navigated to the view that represents the URL.

This method expects a Boolean response. If the response is `true` then the SDK will perform no additional actions for this notification open. If the response is `false` then the SDK will attempt to navigate the subscriber to the destination URL.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
override fun onPushSDKDidReceiveNotificationDestination(destination: String, interaction: PNNotificationInteraction): Boolean {
    // Navigate to a activity within your application using the destination (the Landing URL)

    // Return true to inform the SDK that it should not navigate
    return true
}
```

{% endtab %}

{% tab title="Java" %}

```java
@Override
public boolean onPushSDKDidReceiveNotificationDestination(@NonNull String destination, @NonNull PNNotificationInteraction pnNotificationInteraction) {
    // Navigate to a activity within your application using the destination (the Landing URL)

    // Return true to inform the SDK that it should not navigate
    return true;
}
```

{% endtab %}
{% endtabs %}

See the [Deep Linking](/integration/implementation-steps/android/sdk-kotlin-java/deep-linking) documentation for a complete example.

## App Message Lifecycle Callbacks

These callbacks can be implemented to observe events like impressions, opens, and other interactions with app messages.

In order to implement these callbacks you must have a class that calls the SDK's `registerAppMessageLifecycleCallbacks` method and implements the callback(s) within it.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.registerAppMessageLifecycleCallbacks(object : PNAppMessageLifecycleCallbacks {
    // Add function overrides here
})
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.registerAppMessageLifecycleCallbacks(new PNAppMessageLifecycleCallbacks() {
    // Add function overrides here
});
```

{% endtab %}
{% endtabs %}

The following App Message Lifecycle callbacks are available:

### App Message Will Present

This callback is executed when an App Message is presented to a user.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
override fun onPushSDKWillPresentAppMessage(appMessage: PNAppMessage) {
    // Add code to execute when a user's device presents an app message
}
```

{% endtab %}

{% tab title="Java" %}

```java
@Override
public void onPushSDKWillPresentAppMessage(@NonNull PNAppMessage appMessage) {
    // Add code to execute when a user's device presents an app message
}
```

{% endtab %}
{% endtabs %}

### App Message Received User Interaction

This callback is executed when a user interacts with an App Message.

This method expects a Boolean response. If the response is `true` then the SDK will not handle the interaction or attempt to perform navigation when the interaction is an Open URL action. If the response is `false` then the SDK will process the interaction and if the interaction is an Open URL action will attempt to navigate the user to the destination URL by using the `open` method.

{% tabs %}
{% tab title="Swift" %}

```kotlin
override fun onPushSDKDidReceiveAppMessageInteraction(interaction: PNAppMessageInteraction, appMessage: PNAppMessage): Boolean {
    // Add code to execute when a user interacts with an app message
    
    // Return true to inform the SDK that it should not handle the interaction
    return true
}
```

{% endtab %}

{% tab title="Java" %}

```java
@Override
public boolean onPushSDKDidReceiveAppMessageInteraction(@NonNull PNAppMessageInteraction interaction, @NonNull PNAppMessage appMessage) {
    // Navigate to a activity within your application using the destination (the Landing URL)

    // Return true to inform the SDK that it should not navigate
    return true;
}
```

{% endtab %}
{% endtabs %}


# Commerce & Catalog Item Events

How to set up activity tracking and commerce interactions via the SDK.

This guide explains how to send commerce and catalog-related user interactions to Pushly using the Android SDK. These events power features like abandoned cart notifications, saved item reminders, revenue attribution, and catalog-driven recommendation campaigns.

The Pushly Android SDK must be loaded and initialized before sending any of the events described below.

{% hint style="info" %}
The following steps assume that you are providing an item catalog/feed to our team. Please contact your account manager for more information on this process.
{% endhint %}

### Supported Interaction Types

Pushly supports the following commerce and catalog interactions:

* **view\_item** – A user views an item detail page
* **save\_item** – A user saves or favorites an item
* **unsave\_item** – A user removes saved or unfavorites an item
* **complete\_item** – A user completes an item
* **uncomplete\_item** – A user removes completed item
* **rate\_item** – A user rates an item
* **unrate\_item** – A user removes rating from an item
* **add\_to\_cart** – A user adds an item to their cart
* **update\_cart** – A user modifies cart contents or quantities
* **purchase** – A user completes a transaction

Each interaction can reference items using one of three identifier types, depending on your catalog configuration.

Each item requires an `id`. Quantity and rating are optional unless otherwise noted.

{% hint style="danger" %}
All of the following code snippets must be ran **after** the SDK has been initialized.
{% endhint %}

### View Item

Send this event when a user views an item detail page.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.UserProfile.viewItem(CatalogItem(id = "ITEM_ID"))
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.UserProfile.viewItem(new CatalogItem("ITEM_ID"));
```

{% endtab %}
{% endtabs %}

### Save Item

Use this event when a user saves, favorites, or bookmarks an item. This enables saved‑item reminder campaigns.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.UserProfile.saveItem(CatalogItem(id = "ITEM_ID"))
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.UserProfile.saveItem(new CatalogItem("ITEM_ID"));
```

{% endtab %}
{% endtabs %}

### Unsave Item

Use this event when a user removes an item from the saved list, favorites, or bookmarks.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.UserProfile.unsaveItem(CatalogItem(id = "ITEM_ID"))
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.UserProfile.unsaveItem(new CatalogItem("ITEM_ID"));
```

{% endtab %}
{% endtabs %}

### Complete Item

A user marks an item as completed after finishing its intended experience.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.UserProfile.completeItem(CatalogItem(id = "ITEM_ID"))
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.UserProfile.completeItem(new CatalogItem("ITEM_ID"));
```

{% endtab %}
{% endtabs %}

### Uncomplete Item

Use this event to revert the completion of an item.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.UserProfile.uncompleteItem(CatalogItem(id = "ITEM_ID"))
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.UserProfile.uncompleteItem(new CatalogItem("ITEM_ID"));
```

{% endtab %}
{% endtabs %}

### Rate Item

The `rate_item` event includes an optional `rating` property. If provided, `rating` must be a numeric value between **0 and 100** inclusive, with **up to 1 decimal place**. Integer values are valid.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.UserProfile.rateItem(CatalogItem(id = "ITEM_ID", rating = 5))
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.UserProfile.rateItem(new CatalogItem("ITEM_ID"));
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
The `rating` value should reflect your product's native scale — use whatever range makes sense for your use case (e.g., 1–5 for a star rating, 0–10 for a score, 1–100 for a percentage-style rating). The only requirement is that the value falls within 0–100 and has no more than 1 decimal place.
{% endhint %}

### Unrate Item

Use this event when a user removes the rating from an item.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.UserProfile.unrateItem(CatalogItem(id = "ITEM_ID"))
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.UserProfile.unrateItem(new CatalogItem("ITEM_ID"));
```

{% endtab %}
{% endtabs %}

### Add To Cart

Send this event whenever a user adds an item to their cart. Pushly will accumulate cart state across multiple calls.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.UserProfile.addToCart(listOf(CatalogItem("ITEM_ID", 5)))
```

{% endtab %}

{% tab title="Java" %}

```java
List<CatalogItem> items = new ArrayList<>();
items.add(new CatalogItem("ITEM_ID", 5));
PushSDK.UserProfile.addToCart(items);
```

{% endtab %}
{% endtabs %}

You may call `add_to_cart` as many times as necessary to keep track of all items in a visitor's cart. Abandoned Cart notifications may be sent for any item in a customer's cart that has not been purchased.

After a purchase is made the visitor's cart will be emptied and no notifications for a purchased item will be sent to the visitor.

### Update Cart

Use this event when a user changes their cart without completing a purchase, such as adjusting quantities or removing items.

To remove an item from the cart call the `update_cart` method with the full current cart information (omitting the removed item):

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.UserProfile.updateCart(listOf(CatalogItem("ITEM_ID", 5)))
```

{% endtab %}

{% tab title="Java" %}

```java
List<CatalogItem> items = new ArrayList<>();
items.add(new CatalogItem("ITEM_ID", 5));
PushSDK.UserProfile.updateCart(items);
```

{% endtab %}
{% endtabs %}

Or if the cart has been completely emptied provide an empty array:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.UserProfile.updateCart(emptyList())
```

{% endtab %}

{% tab title="Java" %}

```java
PushSDK.UserProfile.updateCart(new ArrayList<>());
```

{% endtab %}
{% endtabs %}

### Purchase

Send this event after a successful checkout. This clears abandoned cart state and enables revenue attribution.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
PushSDK.UserProfile.trackPurchase(
    items = listOf(CatalogItem(id = "ITEM_ID", quantity = 5)),
    purchaseId = "ABC123",
    priceValue = "344.33"
)
```

{% endtab %}

{% tab title="Java" %}

```java
List<CatalogItem> items = new ArrayList<>();
items.add(new CatalogItem("ITEM_ID", 5));
PushSDK.UserProfile.trackPurchase(items, "ABC123", "344.33");
```

{% endtab %}
{% endtabs %}

#### Purchase Fields

* **priceValue** – Total purchase amount, in the currency configured for the domain
* **purchaseId** – Unique order identifier

If a `purchase` event is sent without item data, Pushly will still clear the user’s cart state.

#### Currency handling

`price_value` must be provided in the currency configured for the domain in the platform’s domain settings.

Use a period (`.`) as the decimal separator for all currencies, regardless of locale. Do not use commas.

Examples:

* USD: `"344.33"`
* JPY: `"5000"`

### Required Fields Summary

* `id` is required for all items
* `quantity` is required only when tracking cart or purchase quantities
* `rating` is required only when rating an item

### Implementation Best Practices

Initialize the SDK early in the page lifecycle to avoid missed events.

Use consistent item IDs across all interaction types so Pushly can correctly associate behavior.

Trigger `purchase` events from a reliable confirmation step whenever possible to avoid false abandoned cart notifications.


# Live Activities

A quick start guide to adding Live Activity notification support to your Android application with the Pushly PushSDK

Android Live Activity notifications provides a similar feature-set and experience to [iOS Live Activities](/integration/implementation-steps/android/sdk-kotlin-java/live-activities) on the Android platform allowing for real-time updated content via push notifications.

The PushSDK helps you streamline Live Activity registration and updates by allowing developers to register and manage the Live Activity handlers which can then be updated via our server API.

## Prerequisites

Before you can integrate Live Activities with the SDK the following requirements must be met:

* An Android Send Integration configured with a Firebase Sender ID and Server API Key or Service Account.
* An Android app.
* A device or emulator running API 19+ (Android 4.4) with Google Play Store Services installed.
* PushSDK release supporting Live Activities (See version support chart below).

## Step 1: Android SDK Setup

For applications **not** already using the PushSDK please see our Native App Push setup guide for [Android](#step-1-android-sdk-setup).

For applications already using the PushSDK you might need to upgrade to a later version that includes Live Activity support. Use the following tables to determine if you need to upgrade your installation.

<table><thead><tr><th width="190">SDK</th><th>Earliest Version with Live Activities</th></tr></thead><tbody><tr><td><a href="/pages/67YAqvQYbTYJsGXS775z">Kotlin / Java</a></td><td>1.3.0+</td></tr></tbody></table>

{% tabs %}
{% tab title="app/build.gradle" %}

```gradle
dependencies {
    ...
    implementation 'com.pushly.android:pushsdk:[1.3, 2.0['
}
```

{% endtab %}
{% endtabs %}

## Step 2: Creating a Handler for Live Activity Updates

To handle and display updates you will need to implement the `PNLiveActivityHandler` class. The handler requires a `name` that will uniquely identifier this type of activity and start via push. An optional notification `channelId` can also be specified to group Live Activity notifications into their own channel. When no custom channel ID is specified the PushSDK will use a default channel.

An `onUpdate` method must be implemented and is responsible for handling any Live Activity event as it is received (start, update, or end). Use this method to customize how updates are displayed to users.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
class MyActivityHandler : PNLiveActivityHandler(
    name = "game_updates",
    channelId = "sports_channel"
) {
    override fun onUpdate(context: Context, contentState: ContentState): PNLiveActivityUpdateResult {
        // Create your notification builder
        val builder = Notification.Builder(context, channelId)
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle("Game Update")
            .setContentText("${contentState.getString("home_team")} vs ${contentState.getString("away_team")}")
        
        // Return builder with the result
        return PNLiveActivityUpdateResult.Ok(builder)
    }
}
```

{% endtab %}

{% tab title="Java" %}

```java
class MyActivityHandler extends PNLiveActivityHandler {
    public MyActivityHandler() {
        super("game_updates", "sports_channel");
    }

    @Override
    public @NonNull PNLiveActivityUpdateResult onUpdate(@NonNull Context context, @NonNull ContentState contentState) {
        String homeTeam = contentState.getStringOrDefault("home_team", "Home Team");
        String awayTeam = contentState.getStringOrDefault("away_team", "Away Team");

        // Create your notification builder
        NotificationCompat.Builder builder = new NotificationCompat.Builder(context, this.getChannelId())
                .setSmallIcon(R.drawable.ic_notification)
                .setContentTitle("Game Update")
                .setContentText(String.format("%s, vs %s", homeTeam, awayTeam));

        // Return builder with the result
        return new PNLiveActivityUpdateResult.Ok(builder);
    }
}
```

{% endtab %}
{% endtabs %}

### Registering the Handler

Once you have implemented your handler you will need to register it with the PushSDK. The following code snippet shows how to register the handler:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
class MainApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        
        // ... PushSDK configuration code

        PushSDK.LiveActivities.register(MyActivityHandler())
    }
}
```

{% endtab %}

{% tab title="Java" %}

```java
class MainApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        
        // ... PushSDK configuration code

        PushSDK.LiveActivities.register(new MyActivityHandler());
    }
}
```

{% endtab %}
{% endtabs %}

## Step 3: Starting a Live Activity

### Starting from Within the App

The following code snippet requests to start a Live Activity using a unique `activityId` , a `handler` name that corresponds to a previously register handler class, and the initial `contentState` of the activity. In this scenario we will use the `PNLiveActivityHandler` class registered with the name: `game_updates`.

{% tabs %}
{% tab title="Kotlin" %}
{% code fullWidth="false" %}

```kotlin
class MainActivity : AppCompatActivity() {
    public fun startLiveActivity() {
        PushSDK.LiveActivities.start(
            activityId = "my_activity_id",
            handler = "game_updates", // Must match registered handler name
            contentState = contentStateOf(
                "home_team" to "Chiefs",
                "home_team_score" to 0,
                "away_team" to "Chargers",
                "away_team_score" to 0
            )
        )
    }
}
```

{% endcode %}
{% endtab %}

{% tab title="Java" %}

```java
class MainActivity extends AppCompatActivity {
    public void startLiveActivity() {
        PushSDK.LiveActivities.start(
            "my_activity_id",
            "game_updates", // Must match registered handler name
            ContentState.builder()
                .put("home_team", "Chiefs")
                .put("home_team_score", 0)
                .put("away_team", "Chargers")
                .put("away_team_score", 0)
                .build()
        );
    }
}
```

{% endtab %}
{% endtabs %}

#### Content State

`ContentState` is used to pass data to Live Activities. You can create your state using convenience method `contentStateOf()`:

{% tabs %}
{% tab title="Kotlin" %}
{% code fullWidth="false" %}

```kotlin
val state = contentStateOf(
    "key1" to "value1",
    "key2" to 42,
    "key3" to null
)
```

{% endcode %}
{% endtab %}

{% tab title="Java" %}

```java
ContentState state = ContentState.builder()
    .put("key1", "value1")
    .put("key2", 1)
    .put("key3", null)
    .build();
```

{% endtab %}
{% endtabs %}

`ContentState` supports the following value types: String, Int, Long, Float, Double, Boolean, null.

### Starting via Push

Once you have registered the handler with the PushSDK you can use our [Live Activity API documentation](https://developers.pushly.com/reference/management-update-live-activity) to send a start event by specifying the corresponding handler name name in the `activity.android.data.handler_name` property and passing the associated unique activity identifier in the request path - in this scenario we used `my_activity_id` which we can then use in subsequent update events.

{% tabs %}
{% tab title="Example Start Request" %}

```json
POST Request to:
https://api.pushly.com/domains/{domain_id}/live-activities/my_activity_id
{
    "name": "my-request-event-name",
    "event": "start",
    "activity": {
        "android": {
            "data": {
                "handler_name": "game_updates",
                "content_state": {
                    "home_team": "Chiefs",
                    "home_team_score": 0,
                    "away_team": "Chargers",
                    "away_team_score": 0
                },
                "notification": {
                    "title": "The Game Has Begun",
                    "body": "Stay tuned for more updates!"
                }
            }
        }
    }
}
```

{% endtab %}
{% endtabs %}

## Step 4: Send Your Live Activity an Update

Once the Live Activity has been started, either locally or via push, you can use our [Live Activity API documentation](https://developers.pushly.com/reference/management-update-live-activity) to send updates to all registered Live Activities via a single request.

{% tabs %}
{% tab title="Example Update Event" %}
{% code fullWidth="false" %}

```json
POST Request to:
https://api.pushly.com/domains/{domain_id}/live-activities/my_activity_id
{
    "name": "my-request-event-name",
    "event": "update",
    "activity": {
        "android": {
            "data": {
                "content_state": {
                    "home_team": "Chiefs",
                    "home_team_score": 7,
                    "away_team": "Chargers",
                    "away_team_score": 0
                }
            }
        }
    }
}
```

{% endcode %}
{% endtab %}
{% endtabs %}

## Step 5: Removing Live Activities

In a similar fashion to how iOS Live Activities work Android Live Activity notifications will automatically end and self-cleanup after 12 hours from the time they are started. However, there might be situations where you want to cleanup the activity before that time limit has occurred. In those scenarios you can remove a single activity or remove all active activities with the following methods:

### Remove a Specific Activity

{% tabs %}
{% tab title="Kotlin" %}
{% code fullWidth="false" %}

```kotlin
PushSDK.LiveActivities.remove(activityId = "my_activity_id")
```

{% endcode %}
{% endtab %}

{% tab title="Java" %}

```java
PushSDK.LiveActivities.remove("my_activity_id");
```

{% endtab %}
{% endtabs %}

### Remove All Activities

{% tabs %}
{% tab title="Kotlin" %}
{% code fullWidth="false" %}

```kotlin
PushSDK.LiveActivities.removeAll()
```

{% endcode %}
{% endtab %}

{% tab title="Java" %}

```java
PushSDK.LiveActivities.removeAll();
```

{% endtab %}
{% endtabs %}

## Best Practices

1. Always register handlers in your application's `onCreate` method for proper initialization.
2. Return a properly configured `Notification.Builder` in the handler's `onUpdate` method.
3. Use consistent activity IDs to properly track and update specific activities.
4. Handle all possible states in your handler's `onUpdate` method.
5. Implement proper error handling in your handler.
6. Use meaningful handler names that reflect their purpose.
7. Clean up activities when they're no longer needed using and `end` event or manually calling one of the remove methods.


# Advanced

{% content-ref url="/pages/ukadvttqAeQttIR9Q7pu" %}
[Self-Managed Integration](/integration/implementation-steps/android/advanced/self-managed-integration)
{% endcontent-ref %}


# Self-Managed Integration

To make integrations easier the PushSDK automatically includes an extension of the `FirebaseMessagingService` class. However, you may have your own service and need to forward calls to the PushSDK in several scenarios including:

* Conflicts with other SDKs
* Conflicting third party development solutions
* Our messaging service conflicts with your existing architecture

{% hint style="info" %}
We suggest that you only consider a self-managed integration if you need to customize the integration with FirebaseMessaging. If you choose to use a self-managed integration you will need to ensure that any new releases to the PushSDK do not add additional methods that must be called manually.
{% endhint %}

### Integrating Your Own Messaging Service

First you will want to remove the service automatically added by the PushSDK and add your own to the app manifest file.

{% tabs %}
{% tab title="AndroidManifest.xml" %}

```xml
<manifest 
    ...
    xmlns:tools="http://schemas.android.com/tools"
>
    <!-- Remove the PushSDK messaging service -->
    <service
        android:name="com.pushly.android.PNMessagingService"
        tools:node="remove" />

    <!-- Add your own messaging service -->
    <service
        android:name=".MyMessageService"
        android:exported="false">
        <intent-filter>
            <action android:name="com.google.firebase.MESSAGING_EVENT" />
        </intent-filter>
    </service>
</manifest>
```

{% endtab %}
{% endtabs %}

Once you have added your own messaging service you'll also be required to place calls to the PushSDK to invoke the methods that are no longer automatically handled. If you do not implement all of the following methods the PushSDK may not function properly.

The methods you must call manually are:

* `PushSDK.handleOnNewToken(context: Context, token: String)`
* `PushSDK.handleOnMessageReceived(context: Context, message: RemoteMessage)`

Pass the messaging service itself as the `Context` (a `FirebaseMessagingService` is a `Context`).

{% hint style="warning" %}
**Use the `Context` overloads.** When a push is delivered to a killed app, FCM starts your app's process solely to run your messaging service — no Activity or host code has configured the SDK yet. The `Context` overloads initialize the SDK from the persisted app key before handling the callback, so the notification is displayed and a rotated token is recorded. The older `handleOnNewToken(token)` and `handleOnMessageReceived(message)` forms cannot do this and are deprecated; a push delivered to a killed app is dropped when they are used.
{% endhint %}

### Messaging Service Example

{% tabs %}
{% tab title="Kotlin" %}

<pre class="language-kotlin"><code class="lang-kotlin"><strong>override fun onNewToken(token: String) {
</strong><strong>    super.onNewToken(token)
</strong>    PushSDK.handleOnNewToken(this, token)
}

override fun onMessageReceived(message: RemoteMessage) {
    super.onMessageReceived(message)
    PushSDK.handleOnMessageReceived(this, message)
}
</code></pre>

{% endtab %}

{% tab title="Java" %}

```java
@Override
public void onNewToken(@NonNull String token) {
    super.onNewToken(token);
    PushSDK.handleOnNewToken(this, token);
}

@Override
public void onMessageReceived(@NonNull RemoteMessage message) {
    super.onMessageReceived(message);
    PushSDK.handleOnMessageReceived(this, message);
}
```

{% endtab %}
{% endtabs %}


# Android SDK Changelog

List of updates to the Android SDK.

{% updates format="full" %}
{% update date="2026-08-13" %}

## 1.8.2 & 1.7.5

* Notification images are now decoded at the size the notification displays them, rather than at full source size.
  {% endupdate %}

{% update date="2026-07-30" %}

## 1.8.1

#### Fixed

* Notification destination routing now behaves the same whether the app was foregrounded, backgrounded, or terminated.
* Improved delivery reliability for subscription and analytics events during network or server disruptions. Token rotations are now sent immediately.

#### Deprecated

* For self-managed `FirebaseMessagingService` integrations, `handleOnMessageReceived(message)` and `handleOnNewToken(token)` are deprecated in favor of the `Context` overloads `handleOnMessageReceived(context, message)` and `handleOnNewToken(context, token)`. The `Context` overloads are required to deliver pushes to a terminated app; the older forms cannot re-initialize a cold-started process.
  {% endupdate %}

{% update date="2026-06-29" %}

## 1.7.4

* Layout improvements for app messages
  {% endupdate %}

{% update date="2026-06-23" %}

## 1.7.3

* Added comprehensive on-device debug logging for app messages.
* Improved reliability for recording app message impressions.
  {% endupdate %}

{% update date="2026-05-19" %}

## 1.7.2

* More reliably detect foregrounding/backgrounding when a user performs the Pushly ID debug sequence.
  {% endupdate %}

{% update date="2026-03-12" %}

## 1.7.0

* Adds support for Catalog `RATE_ITEM` and `COMPLETE_ITEM` actions. See the [Catalog Documentation](/integration/implementation-steps/apple-ios/sdk-swift-obj-c/e-commerce-support) for more information.
  {% endupdate %}

{% update date="2026-02-17" %}

## 1.6.1

* Fixes sorting issue with multiple immediate app messages.
  {% endupdate %}

{% update date="2025-11-05" %}

## 1.6.0

* Adds support for E-Commerce `VIEW_ITEM` and `SAVE_ITEM` actions.
  {% endupdate %}

{% update date="2025-10-14" %}

## 1.5.0

#### Added

* Added support for the **immediate** display condition to [App Messages](/platform/app-messages).
  {% endupdate %}

{% update date="2025-10-03" %}

## 1.4.3

#### Fixed

* Fixed potential crashes on Android 15+ by replacing deprecated Kotlin methods with safe alternatives.
  {% endupdate %}

{% update date="2025-09-25" %}

## 1.4.2

#### Fixed

* Fixed an issue that could result in notifications not displaying when the `meta` field is `null`.
  {% endupdate %}

{% update date="2025-08-25" %}

## 1.4.1

#### Fixed

* Fixes an issue with references to the LiveActivities interface.
  {% endupdate %}

{% update date="2025-08-25" %}

## 1.4.0

#### Updated

* Updates Firebase BOM dependency to version `34.1.0`.
* Removed dependency on Firebase Messaging KTX library.
  {% endupdate %}

{% update date="2025-08-05" %}

## 1.3.6

#### Fixed

* Fix edge case where old FCM token is persisted after a new token is generated.
  {% endupdate %}

{% update date="2025-07-31" %}

## 1.3.5

#### Fixed

* Ensure the proper app icon is used.
  {% endupdate %}

{% update date="2025-03-17" %}

## 1.3.4

#### Fixed

* Additional guards against action button concurrent access; Fixes a race condition related to showing notification action buttons.
  {% endupdate %}

{% update date="2025-02-05" %}

## 1.3.3

#### Fixed

* Fixed an issue with the network detection module resulting in possible failed host checks during network requests.
  {% endupdate %}

{% update date="2025-01-27" %}

## 1.3.2

#### Updated

* Updated background permissions check frequency to better account for manual integrations and quicker subscription change response times.
  {% endupdate %}

{% update date="2025-01-23" %}

## 1.3.1

#### Updated

* Updated Live Activity ContentState with expanded Java interoperability support.
  {% endupdate %}

{% update date="2025-01-21" %}

## 1.3.0

#### Added

* Added support for [Live Activities](/integration/implementation-steps/android/sdk-kotlin-java/live-activities) on Android.

#### Updated

* Updates `PNNotification.meta` structure to allow more developer friendly value access patterns.

#### **Live Activities**

* `PushSDK.LiveActivities.start(activityId: String, handler: String, contentState: ContentState)`
  * Locally starts a Live Activity notification and registers the current subscriber to receive updates for the provided `activityId` and `handler` combination.
* `PushSDK.LiveActivities.register(handler: PNLiveActivityHandler)`
  * Registers the provided `handler` for remote start capability and automatically handles update registrations when an activity is started via push.
* `PushSDK.LiveActivities.remove(activityId: String)`
  * Ends the Live Activity notification and removes it from view.
* `PushSDK.LiveActivities.removeAll()`
  * Ends all active Live Activity notifications and removes them from view.
    {% endupdate %}

{% update date="2024-12-11" %}

## 1.2.8

#### Added

* Added support for `PNNotification.meta` fields.
  {% endupdate %}

{% update date="2024-11-25" %}

## 1.2.7

#### Updated

* Updated permissions background worker to better handle Android pre 33.
* Updated internal tracked event categorizations.

#### Fixed

* Fixed tracked event priority header value encoding.
  {% endupdate %}

{% update date="2024-07-17" %}

## 1.2.6

#### Added

* Added Support for App Message animations.

#### Updated

* Updated internal optimizations on tracked events.
  {% endupdate %}

{% update date="2024-05-13" %}

## 1.2.5

#### Fixed

* Fixed a threading issue with sending large batches of events simultaneously.
  {% endupdate %}

{% update date="2024-04-23" %}

## 1.2.4

#### **Added**

* Added session heartbeat support.
* Added application version tracking.

#### **Updated**

* Updated internal event meta structure.
* Updated PushNotification pause and resume event tracking priority.
  {% endupdate %}

{% update date="2024-03-15" %}

## 1.2.3

#### Fixed

* Fixed issue with App Message configuration JSON deserialization.
  {% endupdate %}

{% update date="2024-03-11" %}

## 1.2.2

#### Fixed

* Fixed issue with App Message orientation change handler.
  {% endupdate %}

{% update date="2024-03-07" %}

## 1.2.1

#### Fixed

* Fixed issue with App Message orientation change handlers in Flutter and ReactNative.
  {% endupdate %}

{% update date="2024-03-04" %}

## 1.2.0

#### **Added**

* Added support for App Messages.
  {% endupdate %}

{% update date="2024-02-16" %}

## 1.1.17

#### **Added**

* Added support for pausing and resuming subscriber push notifications.

#### **New Properties and Methods**

* `PushSDK.PushNotifications.isPaused` - A boolean property indicating the subscriber's notifications are currently paused or not.
* `PushSDK.PushNotifications.pause()` - Will pause a subscriber's ability to receive notifications.
* `PushSDK.PushNotifications.resume()` - Will resume a subscriber's ability to receive notifications.

#### **Moved / Deprecated Properties and Methods**

* `PushSDK.UserProfile.isSubscribed` has been moved to `PushSDK.PushNotifications.isSubscribed` and will now display a deprecation warning.
* `PushSDK.UserProfile.isEligibleToPrompt` has been moved to `PushSDK.PushNotifications.isEligibleToPrompt` and will now display a deprecation warning.
* `PushSDK.showNativeNotificationPermissionPrompt()` has been aliased to `PushSDK.PushNotifications.showPermissionPrompt()`.
* `PushSDK.UserProfile.revertUserDeletion` will now warn and will be removed in a future release.
  {% endupdate %}

{% update date="2023-10-19" %}

## 1.1.16

#### **Updated**

* Updated internal event tracking metadata.
  {% endupdate %}

{% update date="2023-10-18" %}

## 1.1.15

#### **Added**

* Added support for [managed integrations](/integration/implementation-steps/android/advanced/self-managed-integration).
  {% endupdate %}

{% update date="2023-09-18" %}

## 1.1.14

#### Fixed

* Fixed an issue with the debug service that miscounted trigger interactions.
  {% endupdate %}

{% update date="2023-08-29" %}

## 1.1.13

#### **Updated**

* Updated internal resource caching policies.
  {% endupdate %}

{% update date="2023-08-16" %}

## 1.1.12

#### Updated

* Expanded proguard rules.
  {% endupdate %}

{% update date="2023-08-08" %}

## 1.1.11

#### Updated

* Updated proguard rules.
  {% endupdate %}

{% update date="2023-07-26" %}

## 1.1.10

#### **Added**

* Added a convenience service for copying the PushSDK Anonymous ID based on specific user interaction.
* Added package name to tracked events for better user segmentation capabilities.
  {% endupdate %}

{% update date="2023-05-26" %}

## 1.1.9

#### Updated

* Moved check for subscription required check on tracked events to action enum.
  {% endupdate %}

{% update date="2023-05-18" %}

## 1.1.8

#### Updated

* Updated `PNSubscriberStatus` enum instantiation.
* Updated subscriber dismissed state internal validation.
  {% endupdate %}

{% update date="2023-05-01" %}

## 1.1.7

#### Fixed

* Fixed an issue that allowed tracked events to attempt to send before SDK initialization completed.
  {% endupdate %}

{% update date="2023-04-19" %}

## 1.1.6

#### Updated

* Updated PushSDK internal singleton references.
  {% endupdate %}

{% update date="2023-04-17" %}

## 1.1.5

#### Fixed

* Fixed a critical issue that caused a crash when notification interaction occurred while app was running in background and activity intent was no longer in context.
  {% endupdate %}

{% update date="2023-03-16" %}

## 1.1.4

#### Fixed

* Fixed an issue with reverting user deletion.
  {% endupdate %}

{% update date="2023-03-14" %}

## 1.1.3

#### Fixed

* Fixed an issue with permission dialog animation.
  {% endupdate %}

{% update date="2023-03-13" %}

## 1.1.2

#### Added

* Added wrapper library (ReactNative, Flutter) convenience methods for tracked events.
  {% endupdate %}

{% update date="2023-09-18" %}

## 1.1.1

#### Updated

* Updated proguard rules.
  {% endupdate %}

{% update date="2023-03-06" %}

## 1.1.0

#### Added

* Added custom notification permission dialog flow.

#### Updated

* Optimized internal post SDK configuration flow.
* Updated Java method interop coverage.
  {% endupdate %}
  {% endupdates %}

### Closed Beta Releases

{% updates format="full" %}
{% update date="2023-02-10" %}

## 1.0.2

#### Updated

* Updated internal CDN references.
  {% endupdate %}

{% update date="2023-01-24" %}

## 1.0.1

#### Added

* Added default notification small icon for use when not provided via the notification request.

#### Updated

* Expanded automatic badge clearing capability for more Android versions.
* Optimized HTTP request handling and network availability checks.

#### Fixed

* Fixed an issue with notification small icon transparency handling.
* Fixed an issue that could cause multiple invocations of permission request.
  {% endupdate %}

{% update date="2022-12-21" %}

## 1.0.0

#### Initial SDK Release

{% endupdate %}
{% endupdates %}


# React Native

## Step 1: Prerequisites

### Android Requirements

All apps must have a Firebase project and matching credentials to capture subscriptions and send notifications. Follow the link below to create a Firebase project for your app and add the credentials to our platform.

Before you can integrate the SDK with your app the following requirements must be met:

* Android device or emulator running API 24+ (Android 7.0) with Google Play Store Services installed.
* An app that uses [<mark style="color:blue;">Jetpack (AndroidX)</mark>](https://developer.android.com/jetpack/androidx/migrate)
* Firebase Sender ID and Server API Key or Service Account. If you do not already have these follow [<mark style="color:blue;">our documentation</mark>](/integration/implementation-steps/android/firebase-app-setup) to generate them.
* SDK Key: Obtained from the Pushly Platform in the `Domain Settings` page under the `Details` > `Domain Information` section.

{% hint style="warning" %}
**React Native SDK 1.7+**

The bundled native Android SDK depends on `androidx.core` 1.17 and carries Kotlin 2.2 metadata, so a consuming project must build with a matching toolchain:

* Kotlin 2.2 or newer
* Android Gradle Plugin 8.9.1 or newer
* `compileSdk` 36
* `minSdk` 24
* JDK 17–21
  {% endhint %}

#### React Native architecture

The SDK works with either React Native architecture. It ships both a TurboModule and a legacy bridge implementation, and selects between them using your project's `newArchEnabled` Gradle property.

### iOS Requirements

In order to begin subscribing users to notifications you must have either a P8 Key (recommended) or a P12 Certificate; Either of these can be generated via the Apple Developers portal.

* An [<mark style="color:blue;">Apple Developer Account</mark>](https://developer.apple.com/account/) that has Admin privileges
* Your application's App Bundle ID (e.g., `com.pushly.example`)
* An iOS application or XCode project with the Push Notifications capability added

In addition to the above the following requirements must be met:

* An iOS 15.1+ device (iPhone, iPad, or iPod Touch) to develop and test on, or a simulator that meets the requirements in [<mark style="color:blue;">Testing on a simulator</mark>](/integration/implementation-steps/apple-ios/sdk-swift-obj-c#testing-on-a-simulator).
* A Mac OS device with XCode 15.1+ installed.
* An iOS P12 Push Certificate or P8 Key. If you do not already have these follow [<mark style="color:blue;">our documentation</mark>](/integration/implementation-steps/apple-ios/p8-key-or-p12-cert-setup) to generate them.
* SDK Key: Obtained from the Pushly Platform in the `Domain Settings` page under the `Details` > `Domain Information` section.

{% hint style="info" %}
The iOS deployment target and XCode version are set by React Native, not by the Pushly SDK, so a newer React Native version may raise both above the floors listed here.
{% endhint %}


# SDK: React Native

## Step 1: Import the PushSDK Library

Install the PushSDK using either your preferred package manager.

{% tabs %}
{% tab title="Yarn" %}

```sh
yarn add @pushly/push-sdk-react-native
```

{% endtab %}

{% tab title="NPM" %}

```sh
npm install --save @pushly/push-sdk-react-native
```

{% endtab %}
{% endtabs %}

[<mark style="color:blue;">Autolinking is provided automatically</mark>](https://reactnative.dev/blog/2019/07/03/version-60#native-modules-are-now-autolinked) if you are using React Native version **0.60** or greater.

## Step 2: Android Setup

### Step 2.1: Gradle

Update your gradle scripts with the required dependencies:

{% tabs %}
{% tab title="android/build.gradle" %}

```gradle
buildscript {
    ...
    dependencies {
        ...
        classpath 'com.google.gms:google-services:4.3.13'
    }
}
```

{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="android/app/build.gradle" %}

```gradle
android {
    compileSdk 36
    ...

    defaultConfig {
        ...
        minSdk 24
        targetSdk 34
    }
}

apply plugin: 'com.google.gms.google-services'
```

{% endtab %}
{% endtabs %}

### Step 2.2: Google Services

Add your `google-services.json` file to `android/app` directory.

## Step 3: iOS Setup

### Step 3.1: Install Pods

Within your `<project_root>/ios` directory run: `pod install`

### Step 3.2: Add Notification Service Extension

The Notification Service Extension is required to enable rich notifications that support images and custom actions.

Open XCode using your app's **xcworkspace.** Next, select `File > New > Target` and then select `Notification Service Extension` inside the iOS templates tab and click `Next`.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FIKOtBgXyBx78VJBxuNs9%2Fimage.png?alt=media&#x26;token=b0055514-7e75-4ce8-a0b3-bbba8f2543c6" alt=""><figcaption></figcaption></figure>

Enter "NotificationServiceExtension" for the `Product Name` and any other configuration details for your app extension and then click `Finish` but **do not click activate** on the subsequent dialog.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2F96yVKAthjykdmnXGXlem%2Fimage.png?alt=media&#x26;token=b8ed3424-44e0-4259-b97b-bb1457411164" alt=""><figcaption></figcaption></figure>

Click `Cancel` on the dialoging prompting you to activate the service extension so that you can set the deployment target once the modal is closed.

<img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FcGXECnSyGjGGEUBg5KDM%2Fimage.png?alt=media&#x26;token=bc91b955-baa5-490f-a32b-dd7367657f9f" alt="" data-size="original">

Set the extension's `Deployment Target` to the **same value as your primary application target**.

### Step 3.3: Add the Pushly Dependency to the Service Extension

In your `<project_root>/ios/Podfile`, add the **NotificationServiceExtension** at the same level as your main target:

{% tabs %}
{% tab title="Podfile" %}

```ruby
target 'NotificationServiceExtension' do
  pod 'Pushly', '>= 1.0', '< 2.0'
end
```

{% endtab %}

{% tab title="Example Podfile" %}

```ruby
require_relative '../node_modules/react-native/scripts/react_native_pods'
require_relative '../node_modules/@react-native-community/cli-platform-ios/native_modules'

platform :ios, min_ios_version_supported
prepare_react_native_project!

linkage = ENV['USE_FRAMEWORKS']
if linkage != nil
  Pod::UI.puts "Configuring Pod with #{linkage}ally linked Frameworks".green
  use_frameworks! :linkage => linkage.to_sym
end

target 'PushlyDevApp' do
  config = use_native_modules!

  use_react_native!(
    :path => config[:reactNativePath],
    # An absolute path to your application root.
    :app_path => "#{Pod::Config.instance.installation_root}/.."
  )

  target 'PushlyDevAppTests' do
    inherit! :complete
    # Pods for testing
  end

  post_install do |installer|
    react_native_post_install(
      installer,
      config[:reactNativePath],
      # Set `mac_catalyst_enabled` to `true` in order to apply patches
      # necessary for Mac Catalyst builds
      :mac_catalyst_enabled => false
    )
  end
end

target 'NotificationServiceExtension' do
  pod 'Pushly', '>= 1.0', '< 2.0'
end
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
This version range is deliberately wide, and you do not need to narrow it when you upgrade. The React Native SDK's podspec already declares the exact `Pushly` version its bridge needs, and CocoaPods installs the single version that satisfies both requirements.
{% endhint %}

Now that the service extension dependency has been added to the Podfile you will need to re-run installation. Within the `<project_root>/ios` directory, run: `pod install`.

### Step 3.4: Update the Service Extension

Now, open the newly created Notification Service Extension and replace the code with the following:

{% tabs %}
{% tab title="Swift" %}

```swift
import Pushly

class NotificationService: PNNotificationServiceExtension {
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
// Make sure to keep the header file import
#import "NotificationService.h"

@import Pushly;

@interface NotificationService ()

@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;

@end

@implementation NotificationService

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
    self.contentHandler = contentHandler;
    self.bestAttemptContent = [request.content mutableCopy];
    
    [PNNotificationServiceExtensionHandler didReceiveExtensionRequest:request content:self.bestAttemptContent withContentHandler:contentHandler];
}

- (void)serviceExtensionTimeWillExpire {
    // Called just before the extension will be terminated by the system.
    // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
    [PNNotificationServiceExtensionHandler didRecieveExtensionTimeWillExpire:self.bestAttemptContent withContentHandler:self.contentHandler];

    self.contentHandler(self.bestAttemptContent);
}

@end
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
If Xcode warns that it cannot find Pushly, make sure you have a physical device or the "Any iOS Device" option selected as your build destination.
{% endhint %}

### Step 3.4: Add Capabilities to Primary Application Target

{% hint style="warning" %}
The Push Notifications capability should only be added to the **primary application target**.
{% endhint %}

Select the root project and then your primary application target > `Signing & Capabilities`

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FPEKQiayxyjIphpjeqxB1%2Fimage.png?alt=media&#x26;token=84e69c08-d120-4a48-a193-a272b9e4f109" alt=""><figcaption></figcaption></figure>

Click the `+ Capability` and add `Push Notifications`.

Click the `+ Capability` and add `Background Modes`.

After adding the `Background Modes` capability ensure that `Remote Notifications` are enabled.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FMAfJ7lisFKGMxQvjpIaB%2Fimage.png?alt=media&#x26;token=7b5b4afb-0a24-4b0a-8ef6-594f763231ea" alt=""><figcaption></figcaption></figure>

Click the `+ Capability` and add `App Groups`.

Click the `+` symbol located inside the `App Groups` section to add a new named container. To ensure that the PushSDK can properly capture information the container should be named `group.{app-bundle-id}.push`.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FOR42sYi1kH4o1227pQpY%2Fimage.png?alt=media&#x26;token=c504305e-c703-4e7e-afff-834d9de68aac" alt=""><figcaption></figcaption></figure>

Check the box next to the newly created App Group in the primary application target.

### Step 3.5: Add Capabilities to The Notification Service Extension

Now in your **NotificationServiceExtension** target select the `Signing & Capabilities` tab.

Click the `+ Capability` and add `App Groups`.

Finally, check the box next to the newly created App Group.

## Step 4: SDK Initialization

In your `index.tsx` or `app.tsx` file initialize the PushSDK using the example implementation code below.

Replace the `REPLACE_WITH_SDK_KEY` in the `setConfiguration` method with the SDK Key from the platform settings page.

{% tabs %}
{% tab title="index.tsx" %}

```tsx
...
import { PushSDK, LogLevel } from '@pushly/push-sdk-react-native';

export const App = () => {
    ...

    // We recommend initializing the PushSDK in a useEffect block
    // to prevent the application from attempting to invoke
    // PushSDK.setConfiguration() on each rerender
    React.useEffect(() => {
        PushSDK.setLogLevel(LogLevel.INFO);
        PushSDK.setConfiguration({ appKey: 'REPLACE_WITH_SDK_KEY' });

        PushSDK.showNativeNotificationPermissionPrompt().then(({ granted }) => {
            console.log(`Permissions granted: ${granted}`);
        });
    }, []);
    
    // The rest of the your app
    ...
};
```

{% endtab %}
{% endtabs %}

Run your application on either an Android device or emulator (ensure the emulator has `Google Play Store Services` installed) or on an iOS device, if building for iOS, to make sure it builds correctly.

The code you added in the previous step will show the push permission dialog upon app open. This can be customized by using [SDK methods](/integration/implementation-steps/react-native/sdk-react-native/sdk-methods) to control when the dialog shows.

After accepting the dialog log into the platform and navigate to `Notifications > Create Notification` and send your first notification, targeting the appropriate **Native: iOS** or **Native: Android** channel, to your device.

## Next Steps

Once you have confirmed the SDK is working properly you may continue to add additional optional functionality like:

* [<mark style="color:blue;">Handling notification opens / app links / deep linking</mark>](/integration/implementation-steps/react-native/sdk-react-native/deep-linking)
* [<mark style="color:blue;">Attaching attributes to a subscriber's profile</mark>](/integration/implementation-steps/react-native/sdk-react-native/sdk-methods)
* [<mark style="color:blue;">Sending information about what content a subscriber interacts with</mark>](/integration/implementation-steps/react-native/sdk-react-native/activity-tracking)


# Deep Linking

Overview of how the SDK handles deep links and how to perform custom navigation within your application

The PushSDK handles opening of a notification in the following ways:

#### Android

* By default, the SDK will automatically create a new `ACTION_VIEW` intent using the landing URL attached to the notification.

#### iOS

* By default, the SDK will automatically call the `open` method using the landing URL attached to the notification.

If you implement the `onPushSDKDidReceiveNotificationDestination` lifecycle callback you can perform any custom logic needed when the notification is opened.

## Implementing URL Handling Callbacks

The PushSDK provides callbacks that will be called when a subscriber interacts with a notification. These callbacks should be implemented so that the subscriber is automatically navigated to the proper activity after opening the notification.

{% hint style="danger" %}
It is recommend to put all SDK callbacks within your application's `index.tsx` or `app.tsx` file to ensure proper handling of all notification events.
{% endhint %}

When the SDK receives a notification open event it will attempt to call one of the below callbacks with parameters that you can use to navigate the subscriber to the appropriate activity within your app.&#x20;

In order to implement these callbacks you can use SDK's `registerNotificationLifecycleCallbacks` method and implements the callback(s) within it.

{% tabs %}
{% tab title="React" %}

```tsx
import PushSDK from '@pushly/push-sdk-react-native';

...
PushSDK.registerNotificationLifecycleCallbacks({
    // Add function overrides here
});
```

{% endtab %}
{% endtabs %}

For example, if a landing URL was attached to the notification and you implemented the callback to handle subscriber navigation your method the code may look like this:

{% tabs %}
{% tab title="React" %}

```tsx
import PushSDK, { NotificationInteraction } from '@pushly/push-sdk-react-native';

...
PushSDK.registerNotificationLifecycleCallbacks({
    onPushSDKDidReceiveNotificationDestination(
        destination: string,
        interaction: NotificationInteraction
    ): boolean {
        // Navigate to a activity within your application using the destination (the Landing URL)
        
        // Return true to inform the SDK that it should not navigate 
        return true;
    }
});
```

{% endtab %}
{% endtabs %}

## Notification Interaction Callbacks

### Notification Opened With URI/URL Destination

{% hint style="info" %}
Use this callback when you need to perform custom parsing and/or modification of the attached landing URL that wouldn't be properly handled by opening the URL directly.
{% endhint %}

This callback is invoked when a notification is opened that has destination landing URL attached. The subscriber should be navigated to the view that represents the provided landing URL.

This method expects a `boolean` response. If the response is `true` then the SDK will perform no additional actions for this notification open. If the response is `false` then the SDK will attempt to navigate the subscriber to the destination URL.

{% tabs %}
{% tab title="React" %}

```tsx
onPushSDKDidReceiveNotificationDestination(
    destination: string,
    interaction: NotificationInteraction
): boolean {
    // Navigate to a activity within your application using the destination (the Landing URL)
        
    // Return true to inform the SDK that it should not navigate 
    return true;
}
```

{% endtab %}
{% endtabs %}


# Activity Tracking

How to implement activity tracking within your application to enable segmentation based on content views

## Overview

The PushSDK provides methods that allow you to track the type of content a subscriber is viewing within your application. This information can then be used in the Pushly platform to create cohorts of subscribers that have visited specific URLs  or screens with tags based on the number of visits and visit recency.

For example: "Subscribers who have visited a page/screen tagged with "Astrology" at least 4 times in the last 30 days".

## Methods

### UserProfile.trackActivity

To track a URL or screen visited along with its keyword/tag metadata:

{% tabs %}
{% tab title="React" %}

```tsx
PushSDK.UserProfile.trackActivity("myapp://dashboard", ["my-tag"]);
```

{% endtab %}
{% endtabs %}

Or if there are no tags to be provided omit the `tags` parameter:

{% tabs %}
{% tab title="React" %}

```tsx
PushSDK.UserProfile.trackActivity("myapp://sports/article-1");
```

{% endtab %}
{% endtabs %}


# Identity Synchronization

Information on how to link your identifier for the subscriber to our subscriber profile.

## Setting the Subscriber's External ID

Providing a unique User ID for your subscriber allows you to later interact with that subscriber via the API.

Once a subscriber's external ID has been set you may work with our team to set up bi-directional automated data syncs. The following use cases are common ways that publishers use the external ID via server-to-server requests:

* Sending notifications to individual subscribers programmatically
* Processing unsubscribe requests
* Adding and removing profile attributes and events to subscribers

{% tabs %}
{% tab title="React" %}

```tsx
PushSDK.UserProfile.setExternalId("external-id");
```

{% endtab %}
{% endtabs %}

You can also check to see if the subscriber is already tagged with an External ID:

{% tabs %}
{% tab title="React" %}

```tsx
const currentExternalId = await PushSDK.UserProfile.getExternalId();
```

{% endtab %}
{% endtabs %}


# SDK Methods

## Manually Triggering the Permission Dialog

You may choose to disable automatic triggering of the permission dialog via the platform. In this scenario you would choose to trigger the dialog based on your own criteria (eg: after a visitor interacts with a modal / soft prompt, visits a specific screen/page, etc).

The following code will manually trigger the dialog:

{% tabs %}
{% tab title="React" %}

```tsx
const { granted } = await PushSDK.showNativeNotificationPermissionPrompt();
console.log(`User accepted permissions: ${granted}`);
```

{% endtab %}
{% endtabs %}

## Adding Attributes to a Subscriber's Profile

You can add attributes to a subscriber's profile and later perform segmentation based on those attributes. For example, you may want to tag visitors who are interested in a specific type of news (eg: politics, sports) so that you can target them with specific notifications.

{% tabs %}
{% tab title="React" %}

```tsx
PushSDK.UserProfile.set("interests", ["politics", "news"]);
```

{% endtab %}
{% endtabs %}

If you want to set more than one attribute at a time you can also send a map of values. The following example sets both a `is_paying_subscriber` and `interests` attribute on the subscriber.

{% tabs %}
{% tab title="React" %}

```tsx
PushSDK.UserProfile.set({
  is_paying_subscriber: true,
  interests: ["politics", "news"]
});
```

{% endtab %}
{% endtabs %}

If you're storing the value of a property as an array you can use the `append` and `remove` methods to add or remove values. Both are asynchronous and return a `Promise<void>` that resolves once the update has been sent.

{% tabs %}
{% tab title="React" %}

```tsx
// add 'sports' to the subscriber's existing interests
await PushSDK.UserProfile.append("interests", ["sports"]);

// remove 'fashion' and 'news' from the subscriber's interests
await PushSDK.UserProfile.remove("interests", ["fashion", "news"]);
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Properties updated via `PushSDK.UserProfile.append()` and `PushSDK.UserProfile.remove()` will only store the most recent 20 values provided.
{% endhint %}

## Activity Tracking (URL / Screen Visits)

To track the subscriber flow through your application you can use activity tracking. You can track via screen or URL and pass associated metadata/tags if desired.

This information can then be used in segmentation to create cohorts of subscribers that have visited specific URLs or screens with tags based on number of visits and recency. For example: "Subscribers who have visited a page/screen tagged with "Astrology" at least 4 times in the last 30 days".

{% tabs %}
{% tab title="React" %}
To track a URL or screen visited along with the keyword/tag metadata:

```tsx
PushSDK.UserProfile.trackActivity("myapp://dashboard", ["my-tag"]);
```

Or if there are no tags to be provided omit the `tags` parameter:

```tsx
PushSDK.UserProfile.trackActivity("https://www.pushly.com/article-1");
```

{% endtab %}
{% endtabs %}

## Retrieving Your Anonymous Push ID

The PushSDK automatically assigns an anonymous Push ID for event tracking and debugging purposes.

Run the following code to return the Push ID:

{% tabs %}
{% tab title="React" %}

```tsx
const pushId = await PushSDK.UserProfile.getAnonymousId();
```

{% endtab %}
{% endtabs %}

## Setting the Subscriber's External ID

Providing a unique User ID for your subscriber allows you to later interact with that subscriber via the API.

Once a subscriber's external ID has been set you may work with our team to set up bi-directional automated data syncs. The following use cases are common ways that publishers use the external ID via server-to-server requests:

* Sending notifications to individual subscribers programmatically
* Processing unsubscribe requests
* Adding and removing profile attributes and events to subscribers

{% tabs %}
{% tab title="React" %}

```tsx
PushSDK.UserProfile.setExternalId("external-id");
```

{% endtab %}
{% endtabs %}

You can also check to see if the subscriber is already tagged with an External ID:

{% tabs %}
{% tab title="React" %}

```tsx
const currentExternalId = await PushSDK.UserProfile.getExternalId();
```

{% endtab %}
{% endtabs %}

## Determining if a Visitor is Already Subscribed

If you need to know if a user is already subscribed to notifications the following snippet can be used:

{% tabs %}
{% tab title="React" %}

```tsx
const subscribed = await PushSDK.UserProfile.getIsSubscribed();
```

{% endtab %}
{% endtabs %}

## Determining if a Visitor is Eligible to Prompt

{% tabs %}
{% tab title="React" %}

```tsx
const isEligible = await PushSDK.UserProfile.getIsEligibleToPrompt();
```

{% endtab %}
{% endtabs %}

## Pausing / Resuming a User's Notifications (Soft Unsubscribe)

A user's notifications can be paused by calling the following method. It is asynchronous and returns a `Promise<void>` that resolves once the pause has been sent to the SDK:

{% tabs %}
{% tab title="React" %}

```tsx
await PushSDK.PushNotifications.pause();
```

{% endtab %}
{% endtabs %}

If the user's notifications should be resumed call the following method, which is also asynchronous and returns a `Promise<void>`:

{% tabs %}
{% tab title="React" %}

```tsx
await PushSDK.PushNotifications.resume();
```

{% endtab %}
{% endtabs %}

To check if the user's notifications are currently paused, call `getIsPaused()`:

{% tabs %}
{% tab title="React" %}

```tsx
const isPaused = await PushSDK.PushNotifications.getIsPaused();
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
The React Native SDK exposes this as the asynchronous method `getIsPaused()`, returning `Promise<boolean | null>`. The Android and iOS SDKs expose the same state synchronously as `isPaused` (a property/method). When following Android or iOS documentation or examples, substitute `await PushSDK.PushNotifications.getIsPaused()` for `isPaused`.
{% endhint %}

## Permanently Delete a User

If the user should be excluded from all notifications and tracking run the following method:

{% tabs %}
{% tab title="React" %}

```tsx
PushSDK.UserProfile.requestUserDeletion();
```

{% endtab %}
{% endtabs %}

## Setting the SDK Log Level

The PushSDK log level is set to `NONE` by default. Supported log levels are: `VERBOSE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `CRITICAL`, and `NONE`.

To enable more detailed logging add the following code in your application's `index.tsx` or `app.tsx` file:

{% tabs %}
{% tab title="React" %}

```tsx
PushSDK.setLogLevel(LogLevel.INFO);
```

{% endtab %}
{% endtabs %}


# Commerce & Catalog Item Events

How to set up activity tracking and commerce interactions via the SDK.

This guide explains how to send commerce and catalog-related user interactions to Pushly using the React Native SDK. These events power features like abandoned cart notifications, saved item reminders, revenue attribution, and catalog-driven recommendation campaigns.

The Pushly React Native SDK must be loaded and initialized before sending any of the events described below.

{% hint style="info" %}
The following steps assume that you are providing an item catalog/feed to our team. Please contact your account manager for more information on this process.
{% endhint %}

### Supported Interaction Types

Pushly supports the following commerce and catalog interactions:

* **view\_item** – A user views an item detail page
* **save\_item** – A user saves or favorites an item
* **unsave\_item** – A user removes saved or unfavorites an item
* **complete\_item** – A user completes an item
* **uncomplete\_item** – A user removes completed item
* **rate\_item** – A user rates an item
* **unrate\_item** – A user removes rating from an item
* **add\_to\_cart** – A user adds an item to their cart
* **update\_cart** – A user modifies cart contents or quantities
* **purchase** – A user completes a transaction

Each item requires an `id`. Quantity and rating are optional unless otherwise noted.

{% hint style="danger" %}
All of the following code snippets must be ran **after** the SDK has been initialized.
{% endhint %}

### View Item

Send this event when a user views an item detail page.

{% tabs %}
{% tab title="React" %}

```typescript
PushSDK.UserProfile.viewItem({ id: 'ITEM_ID' })
```

{% endtab %}
{% endtabs %}

### Save Item

Use this event when a user saves, favorites, or bookmarks an item. This enables saved‑item reminder campaigns.

{% tabs %}
{% tab title="React" %}

```typescript
PushSDK.UserProfile.saveItem({ id: 'ITEM_ID' })
```

{% endtab %}
{% endtabs %}

### Unsave Item

Use this event when a user removes an item from the saved list, favorites, or bookmarks.

{% tabs %}
{% tab title="React" %}

```typescript
PushSDK.UserProfile.unsaveItem({ id: 'ITEM_ID' })
```

{% endtab %}
{% endtabs %}

### Complete Item

A user marks an item as completed after finishing its intended experience.

{% tabs %}
{% tab title="React" %}

```typescript
PushSDK.UserProfile.completeItem({ id: 'ITEM_ID' })
```

{% endtab %}
{% endtabs %}

### Uncomplete Item

Use this event to revert the completion of an item.

{% tabs %}
{% tab title="React" %}

```typescript
PushSDK.UserProfile.uncompleteItem({ id: 'ITEM_ID' })
```

{% endtab %}
{% endtabs %}

### Rate Item

The `rate_item` event includes an optional `rating` property. If provided, `rating` must be a numeric value between **0 and 100** inclusive, with **up to 1 decimal place**. Integer values are valid.

{% tabs %}
{% tab title="React" %}

```typescript
PushSDK.UserProfile.rateItem({ id: 'ITEM_ID', rating: 5 })
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
The `rating` value should reflect your product's native scale — use whatever range makes sense for your use case (e.g., 1–5 for a star rating, 0–10 for a score, 1–100 for a percentage-style rating). The only requirement is that the value falls within 0–100 and has no more than 1 decimal place.
{% endhint %}

### Unrate Item

Use this event when a user removes the rating from an item.

{% tabs %}
{% tab title="React" %}

```typescript
PushSDK.UserProfile.unrateItem({ id: 'ITEM_ID' })
```

{% endtab %}
{% endtabs %}

### Add To Cart

Send this event whenever a user adds an item to their cart. Pushly will accumulate cart state across multiple calls.

{% tabs %}
{% tab title="React" %}

```typescript
PushSDK.UserProfile.addToCart([{ id: 'ITEM_ID', quantity: 5 }])
```

{% endtab %}
{% endtabs %}

You may call `add_to_cart` as many times as necessary to keep track of all items in a user's cart. Abandoned Cart notifications may be sent for any item in a customer's cart that has not been purchased.

After a purchase is made the user's cart will be emptied and no notifications for a purchased item will be sent to the user.&#x20;

### Update Cart

Use this event when a user changes their cart without completing a purchase, such as adjusting quantities or removing items.

To remove an item from the cart call the `update_cart` method with the full current cart information (omitting the removed item):

{% tabs %}
{% tab title="React" %}

```typescript
PushSDK.UserProfile.updateCart([{ id: 'ITEM_ID', quantity: 7 }])
```

{% endtab %}
{% endtabs %}

Or if the cart has been completely emptied provide an empty array:

{% tabs %}
{% tab title="React" %}

```typescript
PushSDK.UserProfile.updateCart([])
```

{% endtab %}
{% endtabs %}

### Purchase

Send this event after a successful checkout. This clears abandoned cart state and enables revenue attribution.

{% tabs %}
{% tab title="React" %}

```typescript
PushSDK.UserProfile.trackPurchase(
    [{ id: 'ITEM_ID', quantity: 5 }], // items
    "ABC123",                         // purchase_id
    "344.33"                          // price_value
)
```

{% endtab %}
{% endtabs %}

#### Parameter order

`trackPurchase(items, purchase_id, price_value)`

Because this method uses positional parameters, values must be passed in this order:

* `items`
* `purchase_id`
* `price_value`

#### Purchase Fields

* **price\_value** – Total purchase amount, in the currency configured for the domain
* **purchase\_id** – Unique order identifier

If a `purchase` event is sent without item data, Pushly will still clear the user’s cart state.

#### Currency handling

`price_value` must be provided in the currency configured for the domain in the platform’s domain settings.

Use a period (`.`) as the decimal separator for all currencies, regardless of locale. Do not use commas.

Examples:

* USD: `"344.33"`
* JPY: `"5000"`

### Required Fields Summary

* `id` is required for all items
* `quantity` is required only when tracking cart or purchase quantities
* `rating` is required only when rating an item

### Implementation Best Practices

Initialize the SDK early in the page lifecycle to avoid missed events.

Use consistent item IDs across all interaction types so Pushly can correctly associate behavior.

Trigger `purchase` events from a reliable confirmation step whenever possible to avoid false abandoned cart notifications.


# SDK Callbacks

{% hint style="warning" %}
`PushSDKLifecycleCallbacks`, `PermissionLifecycleCallbacks`, `NotificationLifecycleCallbacks`, and `AppMessageLifecycleCallbacks` are **not exported** from `@pushly/push-sdk-react-native`. There is no `import type { X } from '@pushly/push-sdk-react-native'` available for any of them. Pass a plain object literal matching the shape documented below directly to the matching `register*` method — TypeScript will structurally type-check the literal against the method's parameter type without an explicit import.
{% endhint %}

{% hint style="danger" %}
We recommend registering all SDK callbacks once, near your application's root component (eg. in a `useEffect` alongside `PushSDK.setConfiguration`), to ensure proper handling of all events.
{% endhint %}

## SDK Lifecycle Callbacks

These callbacks can be implemented to observe SDK loading and exiting.

In order to implement these callbacks call the SDK's `registerPushSDKLifecycleCallbacks` method with an object literal implementing the callback(s) you need.

{% tabs %}
{% tab title="React" %}

```tsx
PushSDK.registerPushSDKLifecycleCallbacks({
  // Add function overrides here
});
```

{% endtab %}
{% endtabs %}

The following SDK Lifecycle callbacks are available:

### Finished Loading

This callback is executed when the SDK finishes its initialization and is ready for interaction.

{% tabs %}
{% tab title="React" %}

```tsx
onPushSDKDidFinishLoading(configuration, subscriberStatus) {
  // Add code to execute after SDK finishes loading
}
```

{% endtab %}
{% endtabs %}

### Exited With Subscriber Status

This callback is executed when the SDK exits and reports the subscriber's current status.

{% tabs %}
{% tab title="React" %}

```tsx
onPushSDKDidExitWithSubscriberStatus(status, deleted) {
  // Add code to execute after the SDK exits
}
```

{% endtab %}
{% endtabs %}

## Permission Lifecycle Callbacks

These callbacks can be implemented to observe a user's permission authorization changes.

In order to implement these callbacks call the SDK's `registerPermissionLifecycleCallbacks` method with an object literal implementing the callback(s) you need.

{% tabs %}
{% tab title="React" %}

```tsx
PushSDK.registerPermissionLifecycleCallbacks({
  // Add function overrides here
});
```

{% endtab %}
{% endtabs %}

The following Permission Lifecycle callbacks are available:

### Permission Status Changed

This callback is executed when a user's permission status/authorization has changed.

The `response` variable represents the user's **new** permission status.

{% tabs %}
{% tab title="React" %}

```tsx
onPushSDKDidReceivePermissionResponse(response) {
  // Add code to execute after a user's notification permission has changed
}
```

{% endtab %}
{% endtabs %}

### Permission Status Changed With Error

This callback is executed when a user's permission status/authorization changes and the SDK also has an error to report for that change.

{% tabs %}
{% tab title="React" %}

```tsx
onPushSDKDidReceivePermissionResponseWithError(response, error) {
  // Add code to execute after a user's notification permission has
  // changed alongside an error
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
`error` is typed as `Error`, but the SDK delivers an internal `PushSDKException` instance, which is not exported. You can read `error.message`, but you cannot import the class to check `instanceof`.
{% endhint %}

### Registered For Remote Notifications

This callback is executed when the device successfully registers for remote notifications and receives a device token.

{% tabs %}
{% tab title="React" %}

```tsx
onPushSDKDidRegisterForRemoteNotificationsWithDeviceToken(token) {
  // Add code to execute after the device registers for remote notifications
}
```

{% endtab %}
{% endtabs %}

### Failed To Register For Remote Notifications

This callback is executed when the device fails to register for remote notifications.

{% tabs %}
{% tab title="React" %}

```tsx
onPushSDKDidFailToRegisterForRemoteNotificationsWithError(error) {
  // Add code to execute after the device fails to register for remote notifications
}
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
As with `onPushSDKDidReceivePermissionResponseWithError`, `error` is typed as `Error` but delivered as an internal, non-exported `PushSDKException` instance.
{% endhint %}

## Notification Lifecycle Callbacks

These callbacks can be implemented to observe events like impressions, opens, and other interactions with notifications.

In order to implement these callbacks call the SDK's `registerNotificationLifecycleCallbacks` method with an object literal implementing the callback(s) you need.

{% tabs %}
{% tab title="React" %}

```tsx
PushSDK.registerNotificationLifecycleCallbacks({
  // Add function overrides here
});
```

{% endtab %}
{% endtabs %}

The following Notification Lifecycle callbacks are available:

### Notification Received

This callback is executed when a user receives a notification.

{% tabs %}
{% tab title="React" %}

```tsx
onPushSDKDidReceiveRemoteNotification(notification) {
  // Add code to execute after the subscriber's device receives a notification
}
```

{% endtab %}
{% endtabs %}

### Notification Opened With URI/URL Destination

This callback is executed when a notification is opened that has a landing URL attached. The subscriber should be navigated to the view that represents the URL.

This method expects a Boolean response (a `Promise<boolean>` is also supported). If the response is `true` then the SDK will perform no additional actions for this notification open. If the response is `false` then the SDK will attempt to navigate the subscriber to the destination URL.

{% tabs %}
{% tab title="React" %}

```tsx
onPushSDKDidReceiveNotificationDestination(destination, interaction): boolean {
  // Navigate within your application using the destination (the Landing URL)

  // Return true to inform the SDK that it should not navigate
  return true;
}
```

{% endtab %}
{% endtabs %}

See the [Deep Linking](/integration/implementation-steps/react-native/sdk-react-native/deep-linking) documentation for a complete example.

## App Message Lifecycle Callbacks

These callbacks can be implemented to observe events like impressions, opens, and other interactions with app messages.

In order to implement these callbacks call the SDK's `registerAppMessageLifecycleCallbacks` method with an object literal implementing the callback(s) you need.

{% tabs %}
{% tab title="React" %}

```tsx
PushSDK.registerAppMessageLifecycleCallbacks({
  // Add function overrides here
});
```

{% endtab %}
{% endtabs %}

The following App Message Lifecycle callbacks are available:

### App Message Will Present

This callback is executed when an App Message is presented to a user.

{% tabs %}
{% tab title="React" %}

```tsx
onPushSDKWillPresentAppMessage(appMessage) {
  // Add code to execute when a user's device presents an app message
}
```

{% endtab %}
{% endtabs %}

### App Message Received User Interaction

This callback is executed when a user interacts with an App Message.

This method expects a Boolean response (a `Promise<boolean>` is also supported). If the response is `true` then the SDK will not handle the interaction or attempt to perform navigation when the interaction is an Open URL action. If the response is `false` then the SDK will process the interaction and, if the interaction is an Open URL action, will attempt to navigate the user to the destination URL.

{% tabs %}
{% tab title="React" %}

```tsx
onPushSDKDidReceiveAppMessageInteraction(interaction, appMessage): boolean {
  // Add code to execute when a user interacts with an app message

  // Return true to inform the SDK that it should not handle the interaction
  return true;
}
```

{% endtab %}
{% endtabs %}

### Failed To Process Message

This callback is executed when the SDK fails to process an App Message.

{% tabs %}
{% tab title="React" %}

```tsx
onPushSDKDidFailToProcessMessage(message, appMessage) {
  // Add code to execute when the SDK fails to process an app message
}
```

{% endtab %}
{% endtabs %}


# App Messages

A quick start guide to adding App Message support to your React Native application with the Pushly PushSDK

App Messages, unlike Push Notifications, allow you to interact with and engage your user base without the need for a subscription token. App Message can be configured to automatically display based on a number of criteria including custom conditions that can be provided programmatically.

For App Messages with custom conditions the PushSDK provides convenience methods for you to signal when these conditions have been met as well as the ability to listen to certain lifecycle events.

## Prerequisites

Before you can integrate App Messages with the SDK the following requirements must be met:

* A React Native application already integrated with the PushSDK
* PushSDK release supporting App Messages (See version support chart below)

<table><thead><tr><th width="191">SDK</th><th>Earliest Version with App Messages</th></tr></thead><tbody><tr><td>React Native SDK</td><td>1.2.0+</td></tr><tr><td><a href="/pages/NJP4mfr4BQnCU6xJIJ7C">Java / Kotlin</a></td><td>1.2.0+</td></tr><tr><td><a href="/pages/jeRemeIR69xGEEDV1daj">Objective-C / Swift</a></td><td>1.3.0+</td></tr></tbody></table>

## Step 1: React Native SDK Setup

For applications **not** already using the PushSDK please see our Native App Push setup guide for [React Native](/integration/implementation-steps/react-native).

For applications already using the PushSDK you might need to upgrade to a later version that includes App Message support.

{% tabs %}
{% tab title="Yarn" %}

```sh
yarn add @pushly/push-sdk-react-native@">=1.2.0 <2.0.0"
```

{% endtab %}

{% tab title="NPM" %}

```sh
npm install --save @pushly/push-sdk-react-native@">=1.2.0 <2.0.0"
```

{% endtab %}
{% endtabs %}

## Step 2: Create and Schedule an App Message

Use the Pushly Platform to build and schedule your first [App Message](/platform/app-messages).

## Step 3: (Optional) Trigger Your App Message's Custom Condition

When using custom conditions the PushSDK is used to provide these conditions programmatically which then triggers an evaluation of which App Messages are currently active and match the provided conditions.

Both trigger methods are asynchronous and return a `Promise<void>` that resolves once the condition(s) have been sent to the SDK.

In most cases a single condition is triggered and evaluated at a time. For these situations you would use the `trigger(condition: string, value: string): Promise<void>` method signature.

{% tabs %}
{% tab title="React" %}

```tsx
await PushSDK.AppMessages.trigger("enabled_subscription", "true");
```

{% endtab %}
{% endtabs %}

However, there might be situations where it is more convenient to trigger multiple conditions at once for evaluation. In these cases you can pass an object containing any number of conditions to the `triggers(conditions: Record<string, string>): Promise<void>` method signature.

{% tabs %}
{% tab title="React" %}

```tsx
await PushSDK.AppMessages.triggers({
  enabled_subscription: "true",
  subscriber_tier: "premium"
});
```

{% endtab %}
{% endtabs %}

## Step 4: (Optional) Listen to App Message Lifecycle Events

See our [SDK Callbacks](/integration/implementation-steps/react-native/sdk-react-native/sdk-callbacks#app-message-lifecycle-callbacks) documentation for information on handling lifecycle events of your App Messages.


# React Native SDK Changelog

List of updates to the React Native SDK

{% updates format="full" %}
{% update date="2026-07-30" %}

## 1.7.0

**Fixed**

* Notification taps now reach their destination consistently whether the app was foregrounded, backgrounded, or terminated.

**Updated**

* Updates core libraries to Android PushSDK **1.8.1** and iOS PushSDK **1.7.1**.
  {% endupdate %}

{% update date="2026-03-23" %}

## 1.6.0

Adds support for Catalog `RATE_ITEM` and `COMPLETE_ITEM` actions. See the [Catalog Documentation](/integration/implementation-steps/apple-ios/sdk-swift-obj-c/e-commerce-support) for more information.
{% endupdate %}

{% update date="2025-11-05" %}

## 1.5.0

* Adds support for E-Commerce `VIEW_ITEM` and `SAVE_ITEM` actions.
  {% endupdate %}

{% update date="2025-10-14" %}

## 1.4.0

#### Added

* Added support for the **immediate** display condition to [App Messages](/platform/app-messages).

#### Updated

* Updates core libraries to Android PushSDK **1.5.0** and iOS PushSDK **1.4.0**.
  {% endupdate %}

{% update date="2025-09-29" %}

## 1.3.2

#### Updated

* Adds support for React Native 0.81.x
  {% endupdate %}

{% update date="2025-05-21" %}

## 1.3.0

#### Updated

* Added support for Turbo Modules
  {% endupdate %}
  {% endupdates %}


# React Native Expo Plugin

## Step 1: Prerequisites

### Android Requirements

All apps must have a Firebase project and matching credentials to capture subscriptions and send notifications. Follow the link below to create a Firebase project for your app and add the credentials to our platform.

Before you can integrate the SDK with your app the following requirements must be met:

* Android device or emulator running API 24+ (Android 7.0) with Google Play Store Services installed.
* An app that uses [<mark style="color:blue;">Jetpack (AndroidX)</mark>](https://developer.android.com/jetpack/androidx/migrate)
* Firebase Sender ID and Server API Key or Service Account. If you do not already have these follow [<mark style="color:blue;">our documentation</mark>](/integration/implementation-steps/android/firebase-app-setup) to generate them.
* SDK Key: Obtained from the Pushly Platform in the `Domain Settings` page under the `Details` > `Domain Information` section.

{% hint style="warning" %}
**Android build toolchain (React Native SDK 1.7+)**

The bundled native Android SDK depends on `androidx.core` 1.17 and carries Kotlin 2.2 metadata, so a consuming project must build with a matching toolchain:

* Kotlin 2.2 or newer
* Android Gradle Plugin 8.9.1 or newer
* `compileSdk` 36
* `minSdk` 24
* JDK 17–21

If your Expo SDK's Android defaults are lower than these, raise them with [<mark style="color:blue;">expo-build-properties</mark>](https://docs.expo.dev/versions/latest/sdk/build-properties/).
{% endhint %}

### iOS Requirements

In order to begin subscribing users to notifications you must have either a P8 Key (recommended) or a P12 Certificate; Either of these can be generated via the Apple Developers portal.

* An [<mark style="color:blue;">Apple Developer Account</mark>](https://developer.apple.com/account/) that has Admin privileges
* Your application's App Bundle ID (e.g., `com.pushly.example`)
* An iOS application or XCode project with the Push Notifications capability added

In addition to the above the following requirements must be met:

* An iOS 15.1+ device (iPhone, iPad, or iPod Touch) to develop and test on, or a simulator that meets the requirements in [<mark style="color:blue;">Testing on a simulator</mark>](/integration/implementation-steps/apple-ios/sdk-swift-obj-c#testing-on-a-simulator).
* A Mac OS device with XCode 15.1+ installed.
* An iOS P12 Push Certificate or P8 Key. If you do not already have these follow [<mark style="color:blue;">our documentation</mark>](/integration/implementation-steps/apple-ios/p8-key-or-p12-cert-setup) to generate them.
* SDK Key: Obtained from the Pushly Platform in the `Domain Settings` page under the `Details` > `Domain Information` section.


# SDK: Expo plugin

## Step 1: Install the packages

`@pushly/push-sdk-expo` applies the native configuration; `@pushly/push-sdk-react-native` provides the runtime SDK you import from. Install both.

{% tabs %}
{% tab title="Yarn" %}

```sh
yarn add @pushly/push-sdk-expo @pushly/push-sdk-react-native
```

{% endtab %}

{% tab title="NPM" %}

```sh
npm install --save @pushly/push-sdk-expo @pushly/push-sdk-react-native
```

{% endtab %}
{% endtabs %}

## Step 2: Adding plugin to Expo config

### Step 2.1: Google Services

Add your `google-services.json` file to `app` directory.

### Step 2.2: app.config.js

Add the plugin to your Expo config and provide the App Group ID, Android application ID (must match the package inside `google-services.json`), and the path to your `google-services.json` file:

{% tabs %}
{% tab title="app.config.js" %}

```javascript
export default {
  expo: {
    ...,
    plugins: [
      [
        '@pushly/push-sdk-expo',
        {
          ios: {
            nseTargetName: 'NotificationServiceExtension',
            nseBundleIdSuffix: '.NotificationServiceExtension',
            appGroupId: 'group.com.example.myapp.push',
            swiftVersion: '5.0',
          },
          android: {
            applicationId: 'com.example.myapp',
            googleServicesFile: './app/google-services.json',
            applyGoogleServicesPlugin: true,
          },
        },
      ],
    ],
  },
};
```

{% endtab %}
{% endtabs %}

<table><thead><tr><th>Property</th><th>Definition</th></tr></thead><tbody><tr><td><pre><code>ios.nseTargetName
</code></pre></td><td>User-defined name for NotificationServiceExtension.<br><code>default: NotificationServiceExtension</code></td></tr><tr><td><pre><code>ios.nseBundleIdSuffix
</code></pre></td><td>Bundle id suffix for NotificationServiceExtension. Value is appended to PRODUCT_BUNDLE_IDENTIFIER env var.<br><code>default: .NotificationServiceExtension</code></td></tr><tr><td><pre><code>ios.appGroupId
</code></pre></td><td>To ensure that the PushSDK can properly capture information the container should be named <code>group.{app-bundle-id}.push</code>.</td></tr><tr><td><pre><code>ios.swiftVersion
</code></pre></td><td>Used to setup NotificationServiceExtension target in pbxproj file.<br><code>default: 5.0</code></td></tr><tr><td><pre><code>android.applicationId
</code></pre></td><td>Application ID used by Android application. Defaults to <code>android.package</code> from your Expo config.</td></tr><tr><td><pre><code>android.googleServicesFile
</code></pre></td><td>Location of google-services.json file relative to the project root</td></tr><tr><td><pre><code>android.applyGoogleServicesPlugin
</code></pre></td><td>Should google services plugin be applied in gradle.<br><code>default: true</code></td></tr></tbody></table>

Run `expo prebuild --clean` (or use EAS Build/custom dev clients) to generate native projects with the Pushly setup applied.

## Step 3: iOS Setup

### Step 3.1: Install Pods

Within your `<project_root>/ios` directory run: `bundle exec pod install`

### Step 3.2: Notification Service Extension

A **Notification Service Extension** is required to unlock full rich media and analytics tracking functionality, which this plugin installs for you during prebuild.

{% hint style="warning" %}
Use `expo run:ios` or an EAS build to ensure automatic NSE installation. If your project has a checked-in `ios/` directory, `expo prebuild` will not regenerate it and EAS skips prebuild entirely. In that case, run `npx expo prebuild --platform ios --clean` so the extension is installed.
{% endhint %}

To verify the extension was installed in a build, inspect the IPA:

```bash
unzip -l YourApp.ipa | grep appex
```

This should list `PlugIns/NotificationServiceExtension.appex`. If it does not, the extension was never installed — confirm you are using a dev or EAS build and that prebuild ran.

## Step 4: SDK Initialization

In your `index.tsx` or `app.tsx` file initialize the PushSDK using the example implementation code below.

Replace the `REPLACE_WITH_SDK_KEY` in the `setConfiguration` method with the SDK Key from the platform settings page.

{% tabs %}
{% tab title="index.tsx" %}

```tsx
...
import { PushSDK, LogLevel } from '@pushly/push-sdk-react-native';

export const App = () => {
    ...

    // We recommend initializing the PushSDK in a useEffect block
    // to prevent the application from attempting to invoke
    // PushSDK.setConfiguration() on each rerender
    React.useEffect(() => {
        PushSDK.setLogLevel(LogLevel.INFO);
        PushSDK.setConfiguration({ appKey: 'REPLACE_WITH_SDK_KEY' });

        PushSDK.showNativeNotificationPermissionPrompt().then(({ granted }) => {
            console.log(`Permissions granted: ${granted}`);
        });
    }, []);
    
    // The rest of the your app
    ...
};
```

{% endtab %}
{% endtabs %}

Run your application on either an Android device or emulator (ensure the emulator has `Google Play Store Services` installed) or on an iOS device, if building for iOS, to make sure it builds correctly.

The code you added in the previous step will show the push permission dialog upon app open. This can be customized by using [SDK methods](/integration/implementation-steps/react-native/sdk-react-native/sdk-methods) to control when the dialog shows.

After accepting the dialog log into the platform and navigate to `Notifications > Create Notification` and send your first notification, targeting the appropriate **Native: iOS** or **Native: Android** channel, to your device.

## Next Steps

Once you have confirmed the SDK is working properly you may continue to add additional optional functionality like:

* [<mark style="color:blue;">Handling notification opens / app links / deep linking</mark>](/integration/implementation-steps/react-native/sdk-react-native/deep-linking)
* [<mark style="color:blue;">Attaching attributes to a subscriber's profile</mark>](/integration/implementation-steps/react-native/sdk-react-native/sdk-methods)
* [<mark style="color:blue;">Sending information about what content a subscriber interacts with</mark>](/integration/implementation-steps/react-native/sdk-react-native/activity-tracking)


# Expo Plugin Changelog

List of updates to the Expo plugin

{% updates format="full" %}
{% update date="2026-07-30" %}

## 1.3.0

**Updated**

* Widens the `@pushly/push-sdk-react-native` peer dependency to support the 1.7 SDK line. Consumers on earlier plugin versions must upgrade.
  {% endupdate %}

{% update date="2026-04-14" %}

## 1.1.2

* Updates iOS pod to `~> 1.6.1`
* Fixed: Resolved a race condition crash when iOS method swizzling is enabled alongside another library that intercepts the same notification callbacks.
  {% endupdate %}

{% update date="2026-04-02" %}

## 1.1.1

* Clarified package roles: `@pushly/push-sdk-expo` (Expo config plugin) vs `@pushly/push-sdk-react-native` (runtime SDK)
* Improved iOS integration:
  * Automatically syncs deployment target between app and Notification Service Extension.
  * Updates Pushly iOS pod to `~> 1.6.0` and reduces common Xcode signing issues.
* Improved Android integration:
  * Ensures a default Firebase Messaging notification channel.
    {% endupdate %}

{% update date="2026-02-24" %}

## 1.0.0

* Initial release supporting Expo prebuild/EAS Build and custom dev clients
  {% endupdate %}
  {% endupdates %}


# Flutter

{% hint style="info" %}
Native App Push integrations are currently in closed beta. Contact your account manager for more information on becoming a beta partner.
{% endhint %}

## Step 1: Prerequisites

### Android Requirements

All apps must have a Firebase project and matching credentials to capture subscriptions and send notifications. Follow the link below to create a Firebase project for your app and add the credentials to our platform.

Before you can integrate the SDK with your app the following requirements must be met:

* Android device or emulator running API 19+ (Android 4.4) with Google Play Store Services installed.
* An app that uses [<mark style="color:blue;">Jetpack (AndroidX)</mark>](https://developer.android.com/jetpack/androidx/migrate)
* Firebase Sender ID and Server API Key or Service Account. If you do not already have these follow [<mark style="color:blue;">our documentation</mark>](/integration/implementation-steps/android/firebase-app-setup) to generate them.
* SDK Key: Obtained from the Pushly Platform in the `Domain Settings` page under the `Details` > `Domain Information` section.

### iOS Requirements

In order to begin subscribing users to notifications you must have either a P8 Key (recommended) or a P12 Certificate; Either of these can be generated via the Apple Developers portal.

* An [<mark style="color:blue;">Apple Developer Account</mark>](https://developer.apple.com/account/) that has Admin privileges
* Your application's App Bundle ID (e.g., `com.pushly.example`)
* An iOS application or XCode project with the Push Notifications capability added

In addition to the above the following requirements must be met:

* An iOS 11+ device (iPhone, iPad, or iPod Touch) to develop and test on, or a simulator that meets the requirements in [<mark style="color:blue;">Testing on a simulator</mark>](/integration/implementation-steps/apple-ios/sdk-swift-obj-c#testing-on-a-simulator).
* A Mac OS device with XCode 13+ installed.
* An iOS P12 Push Certificate or P8 Key. If you do not already have these follow [<mark style="color:blue;">our documentation</mark>](/integration/implementation-steps/apple-ios/p8-key-or-p12-cert-setup) to generate them.
* SDK Key: Obtained from the Pushly Platform in the `Domain Settings` page under the `Details` > `Domain Information` section.


# SDK: Flutter

{% hint style="info" %}
Native App Push integrations are currently in closed beta. Contact your account manager for more information on becoming a beta partner.
{% endhint %}

## Step 1: Import the PushSDK Plugin

Install the PushSDK plugin

{% tabs %}
{% tab title="Flutter" %}

```sh
flutter pub add pushly_pushsdk
```

{% endtab %}
{% endtabs %}

## Step 2: Android Setup

### Step 2.1: Gradle

Update your gradle scripts with the required dependencies:

{% tabs %}
{% tab title="android/build.gradle" %}

```gradle
buildscript {
    ...
    dependencies {
        ...
        classpath 'com.google.gms:google-services:4.3.13'
    }
}
```

{% endtab %}
{% endtabs %}

{% tabs %}
{% tab title="android/app/build.gradle" %}

```gradle
android {
    compileSdk 33
    ...

    defaultConfig {
        ...
        targetSdk 33
    }
}

apply plugin: 'com.google.gms.google-services'
```

{% endtab %}
{% endtabs %}

### Step 2.2: Google Services

Add your `google-services.json` file to `android/app` directory.

## Step 3: iOS Setup

### Step 3.1: Install Pods

Within your `<project_root>/ios` directory run: `pod install`

### Step 3.2: Add Notification Service Extension

The Notification Service Extension is required to enable rich notifications that support images and custom actions.

Open XCode using your app's **xcworkspace.** Next, select `File > New > Target` and then select `Notification Service Extension` inside the iOS templates tab and click `Next`.

<figure><img src="/files/i2LPQ3Xo7LB8lalaloml" alt=""><figcaption></figcaption></figure>

Enter "NotificationServiceExtension" for the `Product Name` and any other configuration details for your app extension and then click `Finish` but **do not click activate** on the subsequent dialog.

<figure><img src="/files/rJQM5DdzcgKwQ16rxcZy" alt=""><figcaption></figcaption></figure>

Click `Cancel` on the dialoging prompting you to activate the service extension so that you can set the deployment target once the modal is closed.

<img src="/files/yTEMfBdpo3u015gMUuDK" alt="" data-size="original">

Make sure to set your deployment target to the **same as your primary application target**. Unless you have a specific reason not to, you should set the `Deployment Target` to be iOS 11 which is the lowest supported version of iOS in the PushSDK Framework and in the most recent releases of XCode.

### Step 3.3: Add the Pushly Dependency to the Service Extension

In your `<project_root>/ios/Podfile`, add the **NotificationServiceExtension** at the same level as your **Runner** target:

{% tabs %}
{% tab title="Podfile" %}

```ruby
target 'NotificationServiceExtension' do
  use_frameworks!
  pod 'Pushly', '>= 1.0', '< 2.0'
end
```

{% endtab %}

{% tab title="Example Podfile" %}

```ruby
# Uncomment this line to define a global platform for your project
platform :ios, '11.0'

# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

project 'Runner', {
  'Debug' => :debug,
  'Profile' => :release,
  'Release' => :release,
}

def flutter_root
  generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
  unless File.exist?(generated_xcode_build_settings_path)
    raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
  end

  File.foreach(generated_xcode_build_settings_path) do |line|
    matches = line.match(/FLUTTER_ROOT\=(.*)/)
    return matches[1].strip if matches
  end
  raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end

require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)

flutter_ios_podfile_setup

target 'Runner' do
  use_frameworks!
  use_modular_headers!

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
end

target 'NotificationServiceExtension' do
  use_frameworks!
  pod 'Pushly', '>= 1.0', '< 2.0'
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    flutter_additional_ios_build_settings(target)
  end
end
```

{% endtab %}
{% endtabs %}

Now that the service extension dependency has been added to the Podfile you will need to re-run installation. Within the `<project_root>/ios` directory, run: `pod install`.

### Step 3.4: Update the Service Extension

Now, open the newly created Notification Service Extension and replace the code with the following:

{% tabs %}
{% tab title="Swift" %}

```swift
import Pushly

class NotificationService: PNNotificationServiceExtension {
}
```

{% endtab %}

{% tab title="Objective-C" %}

```objectivec
// Make sure to keep the header file import
#import "NotificationService.h"

@import Pushly;

@interface NotificationService ()

@property (nonatomic, strong) void (^contentHandler)(UNNotificationContent *contentToDeliver);
@property (nonatomic, strong) UNMutableNotificationContent *bestAttemptContent;

@end

@implementation NotificationService

- (void)didReceiveNotificationRequest:(UNNotificationRequest *)request withContentHandler:(void (^)(UNNotificationContent * _Nonnull))contentHandler {
    self.contentHandler = contentHandler;
    self.bestAttemptContent = [request.content mutableCopy];
    
    [PNNotificationServiceExtensionHandler didReceiveExtensionRequest:request content:self.bestAttemptContent withContentHandler:contentHandler];
}

- (void)serviceExtensionTimeWillExpire {
    // Called just before the extension will be terminated by the system.
    // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
    [PNNotificationServiceExtensionHandler didRecieveExtensionTimeWillExpire:self.bestAttemptContent withContentHandler:self.contentHandler];

    self.contentHandler(self.bestAttemptContent);
}

@end
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
If Xcode warns that it cannot find Pushly, make sure you have a physical device or the "Any iOS Device" option selected as your build destination.
{% endhint %}

### Step 3.4: Add Capabilities to Primary Application Target

{% hint style="warning" %}
The Push Notifications capability should only be added to the **primary application target**.
{% endhint %}

Select the root project and then your primary application target > `Signing & Capabilities`

<figure><img src="/files/i1b3kHNcmqq7WSn5xdAj" alt=""><figcaption></figcaption></figure>

Click the `+ Capability` and add `Push Notifications`.

Click the `+ Capability` and add `Background Modes`.

After adding the `Background Modes` capability ensure that `Remote Notifications` are enabled.

<figure><img src="/files/zdGxGVSS0kCK8sRjpod7" alt=""><figcaption></figcaption></figure>

Click the `+ Capability` and add `App Groups`.

Click the `+` symbol located inside the `App Groups` section to add a new named container. To ensure that the PushSDK can properly capture information the container should be named `group.{app-bundle-id}.push`.

<figure><img src="/files/JBp3BzMBvsX6jjySBlK3" alt=""><figcaption></figcaption></figure>

Check the box next to the newly created App Group in the primary application target.

### Step 3.5: Add Capabilities to The Notification Service Extension

Now in your **NotificationServiceExtension** target select the `Signing & Capabilities` tab.

Click the `+ Capability` and add `App Groups`.

Finally, check the box next to the newly created App Group.

## Step 4: SDK Initialization

In your `main.dart` file initialize the PushSDK using the example implementation code below.

Replace the `REPLACE_WITH_SDK_KEY` in the `setConfiguration` method with the SDK Key from the platform settings page.

{% tabs %}
{% tab title="main.dart" %}

```dart
...
import 'package:pushly_pushsdk/pushsdk.dart';

...
// Within an initializer function of your app
await PushSDK.setLogLevel(PNLogLevel.info);
await PushSDK.setConfiguration('REPLACE_WITH_SDK_KEY');

await PushSDK.showNativeNotificationPermissionPrompt(completion: ( granted, status, error ) {
    print('Permissions granted: $granted');
});
```

{% endtab %}
{% endtabs %}

Run your application on either an Android device or emulator (ensure the emulator has `Google Play Store Services` installed) or on an iOS device, if building for iOS, to make sure it builds correctly.

The code you added in the previous step will show the push permission dialog upon app open. This can be customized by using [<mark style="color:blue;">SDK methods</mark>](/integration/implementation-steps/flutter/sdk-flutter/sdk-methods) to control when the dialog shows.

After accepting the dialog log into the platform and navigate to `Notifications > Create Notification` and send your first notification, targeting the appropriate **Native: iOS** or **Native: Android** channel, to your device.

## Next Steps

Once you have confirmed the SDK is working properly you may continue to add additional optional functionality like:

* [<mark style="color:blue;">Handling notification opens / app links / deep linking</mark>](/integration/implementation-steps/flutter/sdk-flutter/deep-linking)
* [<mark style="color:blue;">Attaching attributes to a subscriber's profile</mark>](/integration/implementation-steps/flutter/sdk-flutter/sdk-methods#adding-attributes-to-a-subscribers-profile)
* [<mark style="color:blue;">Sending information about what content a subscriber interacts with</mark>](https://documentation.pushly.com/integration/implementation-steps/flutter/pages/Aub7w3VchKXxXdWnfo5W#userprofile.trackactivity)


# Deep Linking

Overview of how the SDK handles deep links and how to perform custom navigation within your application

The PushSDK handles opening of a notification in the following ways:

#### Android

* By default, the SDK will automatically create a new `ACTION_VIEW` intent using the landing URL attached to the notification.

#### iOS

* By default, the SDK will automatically call the `open` method using the landing URL attached to the notification.

If you implement the `onPushSDKDidReceiveNotificationDestination` lifecycle callback you can perform any custom logic needed when the notification is opened.

## Implementing URL Handling Callbacks

The PushSDK provides callbacks that will be called when a subscriber interacts with a notification. These callbacks should be implemented so that the subscriber is automatically navigated to the proper activity after opening the notification.

{% hint style="danger" %}
It is recommend to put all SDK callbacks within your application's `main.dart`  file to ensure proper handling of all notification events.
{% endhint %}

When the SDK receives a notification open event it will attempt to call one of the below callbacks with parameters that you can use to navigate the subscriber to the appropriate activity within your app.&#x20;

In order to implement these callbacks you can use SDK's `registerNotificationLifecycleCallbacks` method and implements the callback(s) within it.

{% tabs %}
{% tab title="Dart" %}

```dart
import 'package:pushly_pushsdk/pushsdk.dart';

...
await PushSDK.registerNotificationLifecycleCallbacks(
    // Add function overrides here
);
```

{% endtab %}
{% endtabs %}

For example, if a landing URL was attached to the notification and you implemented the callback to handle subscriber navigation your method the code may look like this:

{% tabs %}
{% tab title="Dart" %}

```dart
import 'package:pushly_pushsdk/pushsdk.dart';

...
await PushSDK.registerNotificationLifecycleCallbacks(
    onPushSDKDidReceiveNotificationDestination: (destination, interaction) {
        // Navigate to a activity within your application using the destination (the Landing URL)
        
        // Return true to inform the SDK that it should not navigate 
        return true;
    }
);
```

{% endtab %}
{% endtabs %}

## Notification Interaction Callbacks

### Notification Opened With URI/URL Destination

{% hint style="info" %}
Use this callback when you need to perform custom parsing and/or modification of the attached landing URL that wouldn't be properly handled by opening the URL directly.
{% endhint %}

This callback is invoked when a notification is opened that has destination landing URL attached. The subscriber should be navigated to the view that represents the provided landing URL.

This method expects a `boolean` response. If the response is `true` then the SDK will perform no additional actions for this notification open. If the response is `false` then the SDK will attempt to navigate the subscriber to the destination URL.

{% tabs %}
{% tab title="Dart" %}

```dart
onPushSDKDidReceiveNotificationDestination: (destination, interaction) {
    // Navigate to a activity within your application using the destination (the Landing URL)
        
    // Return true to inform the SDK that it should not navigate 
    return true;
}
```

{% endtab %}
{% endtabs %}


# Activity Tracking

How to implement activity tracking within your application to enable segmentation based on content views

## Overview

The PushSDK provides methods that allow you to track the type of content a subscriber is viewing within your application. This information can then be used in the Pushly platform to create cohorts of subscribers that have visited specific URLs  or screens with tags based on the number of visits and visit recency.

For example: "Subscribers who have visited a page/screen tagged with "Astrology" at least 4 times in the last 30 days".

## Methods

### UserProfile.trackActivity

To track a URL or screen visited along with its keyword/tag metadata:

{% tabs %}
{% tab title="Dart" %}

```dart
await PushSDK.UserProfile.trackActivity('myapp://dashboard', ['my-tag']);
```

{% endtab %}
{% endtabs %}

Or if there are no tags to be provided pass an empty `tags` parameter:

{% tabs %}
{% tab title="Dart" %}

```dart
await PushSDK.UserProfile.trackActivity('myapp://sports/article-1', []);
```

{% endtab %}
{% endtabs %}


# Identity Synchronization

Information on how to link your identifier for the subscriber to our subscriber profile.

## Setting the Subscriber's External ID

Providing a unique User ID for your subscriber allows you to later interact with that subscriber via the API.

Once a subscriber's external ID has been set you may work with our team to set up bi-directional automated data syncs. The following use cases are common ways that publishers use the external ID via server-to-server requests:

* Sending notifications to individual subscribers programmatically
* Processing unsubscribe requests
* Adding and removing profile attributes and events to subscribers

{% tabs %}
{% tab title="Dart" %}

```dart
await PushSDK.UserProfile.setExternalId('external-id');
```

{% endtab %}
{% endtabs %}

You can also check to see if the subscriber is already tagged with an External ID:

{% tabs %}
{% tab title="Dart" %}

```dart
final currentExternalId = await PushSDK.UserProfile.getExternalId();
```

{% endtab %}
{% endtabs %}

### External ID Restrictions

To prevent subscribers from being accidentally merged via External ID and to ensure integrity in  the subscriber documents, Pushly enforces the following restrictions upon External IDs:

* No empty or strings that contain white-space
* No identifiers that contain placeholder text: `[test, testing, example, guest, anon, anonymous, user, guest, visitor, n/a]`
* No common JavaScript fall-through words or keywords: \[`[Object ... ], undefined, NaN, Infinity, true, false]`
* Cannot be only punctuation/symbols, must contain alphanumeric characters: `[---, ***, -*-, -._+/=:@~]` &#x20;
* Identifiers should be between 3 and 256 characters in length

A [regex playground](https://regex101.com/r/2wEVEc/1) has been set up for testing the validity of External ID schema.


# SDK Methods

## Manually Triggering the Permission Dialog

You may choose to disable automatic triggering of the permission dialog via the platform. In this scenario you would choose to trigger the dialog based on your own criteria (eg: after a visitor interacts with a modal / soft prompt, visits a specific screen/page, etc).

The following code will manually trigger the dialog:

{% tabs %}
{% tab title="Dart" %}

```dart
await PushSDK.showNativeNotificationPermissionPrompt(completion: (granted, status, error) {
    print('User accepted permissions: $granted');
});

```

{% endtab %}
{% endtabs %}

## Adding Attributes to a Subscriber's Profile

You can add attributes to a subscriber's profile and later perform segmentation based on those attributes. For example, you may want to tag visitors who are interested in a specific type of news (eg: politics, sports) so that you can target them with specific notifications.

{% tabs %}
{% tab title="Dart" %}

```dart
await PushSDK.UserProfile.set('interests', ['politics', 'news']);
```

{% endtab %}
{% endtabs %}

If you want to set more than one attribute at a time you can also send a map of values. The following example sets both a `is_paying_subscriber` and `interests` attribute on the subscriber.

{% tabs %}
{% tab title="Dart" %}

```dart
await PushSDK.UserProfile.setData({
  'is_paying_subscriber': true,
  'interests': ['politics', 'news']
});
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
Properties set via `PushSDK.UserProfile.append()` will only store the most recent 20 values provided.
{% endhint %}

## Activity Tracking (URL / Screen Visits)

To track the subscriber flow through your application you can use activity tracking. You can track via screen or URL and pass associated metadata/tags if desired.

This information can then be used in segmentation to create cohorts of subscribers that have visited specific URLs  or screens with tags based on number of visits and recency. For example: "Subscribers who have visited a page/screen tagged with "Astrology" at least 4 times in the last 30 days".

{% tabs %}
{% tab title="Dart" %}
To track a URL or screen visited along with the keyword/tag metadata:

```dart
await PushSDK.UserProfile.trackActivity('myapp://dashboard', ['my-tag']);
```

Or if there are no tags to be provided pass an empty `tags` parameter:

```dart
await PushSDK.UserProfile.trackActivity('https://www.pushly.com/article-1', []);
```

{% endtab %}
{% endtabs %}

## Retrieving Your Anonymous Push ID

The PushSDK automatically assigns an anonymous Push ID for event tracking and debugging purposes.

Run the following code to return the Push ID:

{% tabs %}
{% tab title="Dart" %}

```dart
final pushId = await PushSDK.UserProfile.getAnonymousId();
```

{% endtab %}
{% endtabs %}

## Setting the Subscriber's External ID

Providing a unique User ID for your subscriber allows you to later interact with that subscriber via the API.

Once a subscriber's external ID has been set you may work with our team to set up bi-directional automated data syncs. The following use cases are common ways that publishers use the external ID via server-to-server requests:

* Sending notifications to individual subscribers programmatically
* Processing unsubscribe requests
* Adding and removing profile attributes and events to subscribers

{% tabs %}
{% tab title="Dart" %}

```dart
await PushSDK.UserProfile.setExternalId('external-id');
```

{% endtab %}
{% endtabs %}

You can also check to see if the subscriber is already tagged with an External ID:

{% tabs %}
{% tab title="Dart" %}

```dart
final currentExternalId = await PushSDK.UserProfile.getExternalId();
```

{% endtab %}
{% endtabs %}

## Determining if a Visitor is Already Subscribed

If you need to know if a user is already subscribed to notifications the following snippet can be used:

{% tabs %}
{% tab title="Dart" %}

```dart
final subscribed = await PushSDK.UserProfile.isSubscribed();
```

{% endtab %}
{% endtabs %}

## Determining if a Visitor is Eligible to Prompt

{% tabs %}
{% tab title="Dart" %}

```dart
final isEligible = await PushSDK.UserProfile.isEligibleToPrompt();
```

{% endtab %}
{% endtabs %}

## Permanently Delete a User

If the user should be excluded from all notifications and tracking run the following method:

{% tabs %}
{% tab title="Dart" %}

```dart
await PushSDK.UserProfile.requestUserDeletion();
```

{% endtab %}
{% endtabs %}

## Setting the SDK Log Level

The PushSDK log level is set to `none` by default. Supported log levels are: `verbose`, `debug`, `info`, `warn`, `error`, `critical`, and `none`.

To enable more detailed logging add the following code in your application's `main.dart` file:

{% tabs %}
{% tab title="Dart" %}

```dart
await PushSDK.setLogLevel(PNLogLevel.info);
```

{% endtab %}
{% endtabs %}


# Commerce & Catalog Item Events

How to set up activity tracking and commerce interactions via the SDK.

This guide explains how to send commerce and catalog-related user interactions to Pushly using the Flutter SDK. These events power features like abandoned cart notifications, saved item reminders, revenue attribution, and catalog-driven recommendation campaigns.

The Pushly Flutter SDK must be loaded and initialized before sending any of the events described below.

{% hint style="info" %}
The following steps assume that you are providing an item catalog/feed to our team. Please contact your account manager for more information on this process.
{% endhint %}

### Supported Interaction Types

Pushly supports the following commerce and catalog interactions:

* **view\_item** – A user views an item detail page
* **save\_item** – A user saves or favorites an item
* **unsave\_item** – A user removes saved or unfavorites an item
* **complete\_item** – A user completes an item
* **uncomplete\_item** – A user removes completed item
* **rate\_item** – A user rates an item
* **unrate\_item** – A user removes rating from an item
* **add\_to\_cart** – A user adds an item to their cart
* **update\_cart** – A user modifies cart contents or quantities
* **purchase** – A user completes a transaction

Each interaction can reference items using one of three identifier types, depending on your catalog configuration.

Each item requires an `id`. Quantity and rating are unless otherwise noted.

{% hint style="danger" %}
All of the following code snippets must be ran **after** the SDK has been initialized.
{% endhint %}

### View Item

Send this event when a user views an item detail page.

{% tabs %}
{% tab title="Dart" %}

```dart
await PushSDK.UserProfile.viewItem(CatalogItem("ITEM_ID"));
```

{% endtab %}
{% endtabs %}

### Save Item

Use this event when a user saves, favorites, or bookmarks an item. This enables saved‑item reminder campaigns.

{% tabs %}
{% tab title="Dart" %}

```dart
await PushSDK.UserProfile.saveItem(CatalogItem("ITEM_ID"));
```

{% endtab %}
{% endtabs %}

### Unsave Item

Use this event when a user removes an item from the saved list, favorites, or bookmarks.

{% tabs %}
{% tab title="Dart" %}

```dart
await PushSDK.UserProfile.unsaveItem(CatalogItem("ITEM_ID"));
```

{% endtab %}
{% endtabs %}

### Complete Item

A user marks an item as completed after finishing its intended experience.

{% tabs %}
{% tab title="Kotlin" %}

```dart
await PushSDK.UserProfile.completeItem(CatalogItem("ITEM_ID"));
```

{% endtab %}
{% endtabs %}

### Uncomplete Item

Use this event to revert the completion of an item.

{% tabs %}
{% tab title="Dart" %}

```dart
await PushSDK.UserProfile.uncompleteItem(CatalogItem("ITEM_ID"));
```

{% endtab %}
{% endtabs %}

### Rate Item

The `rate_item` event includes an optional `rating` property. If provided, `rating` must be a numeric value between **0 and 100** inclusive, with **up to 1 decimal place**. Integer values are valid.

{% tabs %}
{% tab title="Dart" %}

```dart
await PushSDK.UserProfile.rateItem(CatalogItem("ITEM_ID", rating: 5));
```

{% endtab %}
{% endtabs %}

{% hint style="info" %}
The `rating` value should reflect your product's native scale — use whatever range makes sense for your use case (e.g., 1–5 for a star rating, 0–10 for a score, 1–100 for a percentage-style rating). The only requirement is that the value falls within 0–100 and has no more than 1 decimal place.
{% endhint %}

### Unrate Item

Use this event when a user removes the rating from an item.

{% tabs %}
{% tab title="Dart" %}

```dart
await PushSDK.UserProfile.unrateItem(CatalogItem("ITEM_ID"));
```

{% endtab %}
{% endtabs %}

### Add To Cart

Send this event whenever a user adds an item to their cart. Pushly will accumulate cart state across multiple calls.

{% tabs %}
{% tab title="Dart" %}

```dart
await PushSDK.UserProfile.addToCart([
    CatalogItem("ITEM_ID", 2),
]);
```

{% endtab %}
{% endtabs %}

You may call `add_to_cart` as many times as necessary to keep track of all items in a visitor's cart. Abandoned Cart notifications may be sent for any item in a customer's cart that has not been purchased.

After a purchase is made the visitor's cart will be emptied and no notifications for a purchased item will be sent to the visitor.&#x20;

### Update Cart

Use this event when a user changes their cart without completing a purchase, such as adjusting quantities or removing items.

To remove an item from the cart call the `update_cart` method with the full current cart information (omitting the removed item):

{% tabs %}
{% tab title="Dart" %}

```dart
await PushSDK.UserProfile.updateCart([
    CatalogItem("ITEM_ID", 2),
]);
```

{% endtab %}
{% endtabs %}

Or if the cart has been completely emptied provide an empty array:

{% tabs %}
{% tab title="Dart" %}

```dart
await PushSDK.UserProfile.updateCart([]);
```

{% endtab %}
{% endtabs %}

### Purchase

Send this event after a successful checkout. This clears abandoned cart state and enables revenue attribution.

{% tabs %}
{% tab title="Dart" %}

```dart
await PushSDK.UserProfile.trackPurchase(
      [CatalogItem("ITEM_ID", 2)],             // items
      "ABC123",                                // purchase_id
      "19.99"                                  // price_value
);
```

{% endtab %}
{% endtabs %}

#### Parameter order

`trackPurchase(items, purchase_id, price_value)`

Because this method uses positional parameters, values must be passed in this order:

* `items`
* `purchase_id`
* `price_value`

#### Purchase Fields

* **price\_value** – Total purchase amount, in the currency configured for the domain
* **purchase\_id** – Unique order identifier

If a `purchase` event is sent without item data, Pushly will still clear the user’s cart state.

#### Currency handling

`price_value` must be provided in the currency configured for the domain in the platform’s domain settings.

Use a period (`.`) as the decimal separator for all currencies, regardless of locale. Do not use commas.

Examples:

* USD: `"344.33"`
* JPY: `"5000"`

### Required Fields Summary

* `id` is required for all items
* `quantity` is required only when tracking cart or purchase quantities
* `rating` is required only when rating an item

### Implementation Best Practices

Initialize the SDK early in the page lifecycle to avoid missed events.

Use consistent item IDs across all interaction types so Pushly can correctly associate behavior.

Trigger `purchase` events from a reliable confirmation step whenever possible to avoid false abandoned cart notifications.


# Flutter SDK Changelog

List of updates to the Flutter SDK

{% updates format="full" %}
{% update date="2026-06-29" %}

## 1.5.1

Layout improvements for app messages
{% endupdate %}

{% update date="2026-03-23" %}

## 1.5.0

Adds support for Catalog `RATE_ITEM` and `COMPLETE_ITEM` actions. See the [Catalog Documentation](/integration/implementation-steps/apple-ios/sdk-swift-obj-c/e-commerce-support) for more information.
{% endupdate %}

{% update date="2025-11-05" %}

## 1.4.0

* Adds support for E-Commerce `VIEW_ITEM` and `SAVE_ITEM` actions.
  {% endupdate %}

{% update date="2025-10-14" %}

## 1.3.0

#### Added

* Added support for the **immediate** display condition to [App Messages](/platform/app-messages).

#### Updated

* Updates core libraries to Android PushSDK **1.5.0** and iOS PushSDK **1.4.0**.
  {% endupdate %}
  {% endupdates %}


# API Access Management

The Pushly API uses API Tokens to allow access to the API.

API Tokens are available in the platform to **Domain Admin** users. They can be generated by navigating to the **Domain Settings** page, selecting the **Security** tab, and then clicking the **Create Token** button.

<figure><img src="/files/pPpWzWagVHq6BPts5NJb" alt=""><figcaption></figcaption></figure>

## Creating a New API Token

From the **Security** tab, locate the **API Tokens** section and click the **Create Token** button. Enter a **Token Label** that represents the primary use of the API Token, and then click the **Create Token** button.

After clicking the **Create Token** button a new modal will appear containing the newly generated token. Click the **Copy Token** button to copy the new token to your clipboard.

> ⚠️ Ensure you save the token before closing the modal. Once the modal is closed you will not be able to view the token again.

<figure><img src="/files/lW6BtcnNSX4K8AYiVFyD" alt=""><figcaption></figcaption></figure>

## Revoking an Existing API Token

If an existing API Token is no longer in use or needs to be invalidated, click the **Revoke** button from the API Tokens list. On the resulting confirmation modal, click the **Revoke Token** button to invalidate the token from future use.

<figure><img src="/files/2V2YqubT02cRMAqrUJdZ" alt=""><figcaption></figcaption></figure>


# Secrets

Secrets allow you to securely store credentials and sensitive values — such as API tokens, usernames and passwords, or JSON configuration — for use in integrations and notification templates. Secret values are encrypted and stored securely. Once saved, values cannot be viewed again and are only accessible at the time of use.

Secrets are available in the platform to **Org Admin** and **Domain Admin** users. They can be managed by navigating to the **Domain Settings** page and selecting the **Security** tab. The **Secrets** section is located below **API Tokens**.

The **Secrets** list displays the following columns: **Alias**, **Type**, **Last Updated Date**, and **Actions**.

<figure><img src="/files/oFJJJt18935SrElKOVio" alt=""><figcaption></figcaption></figure>

**Creating a New Secret**

From the **Security** tab, locate the **Secrets** section and click the **Add Secret** button. A **Create secret** drawer will slide out from the right with the following fields:

* **Alias** — A unique name for the secret (e.g., `weather_fx`). Aliases are case-insensitive and must contain only lowercase letters, numbers, hyphens, and underscores.
* **Secret Type** — Select the type of secret you want to store:
  * **Token** — A single string value such as an API key.
  * **Username and password** — A pair of credentials.
  * **JSON** — A JSON key-value configuration object.
* **Value** — The input fields change based on the selected secret type:
  * **Token**: A single masked input field.
  * **Username and password**: A username text field and a masked password field.
  * **JSON**: A code editor with JSON validation.

Click the **Submit** button to save the secret.

> ⚠️ Secret values are write-only. Once created, you will not be able to view the stored value. Ensure your credentials are saved elsewhere before submitting.

<figure><img src="/files/6kA7C3yWuLRl7OerWLwz" alt=""><figcaption></figcaption></figure>

**Editing a Secret**

To update the stored value of an existing secret, click the **Edit** action on the secret's row in the list. The edit drawer will appear with the **Alias** and **Secret Type** fields locked. Enter the new value and click **Submit** to rotate the stored credentials.

This is useful when you need to rotate an API key or update a password without changing the alias that is already referenced in your templates or integrations.

**Archiving a Secret**

To remove a secret, click the **Archive** action on the secret's row. A confirmation dialog will appear with the message:

> "Are you sure you want to archive this secret? Any existing integrations using this secret will no longer function."

Click **Archive Secret** to confirm, or **Cancel** to go back. Archived secrets are removed from the list and can no longer be used in integrations or templates.

**Using Secrets in Templates**

Secrets can be referenced in notification templates and integrations using the **Macro Manager**. When building a template, open the Macro Manager and select the **Secrets** category. Choosing a secret will insert the reference into your template using the format:

```
secrets.<alias>
```

For example, if you created a secret with the alias `weather_fx`, you would reference it as `secrets.weather_fx`. At send time, the platform will securely resolve the secret value without exposing it in the template editor.


# API Reference

You can find [<mark style="color:blue;">Our API Documentation here</mark>](https://developers.pushly.com/). The documentation is not the full breadth of our capabilities so please reach out to your Account Manager with questions, ideas, or requests for needed assistance.&#x20;


# Dashboard

The Dashboard provides a high-level overview of the current audience, subscription counts, and Notification KPIs. If you utilize us across multiple properties, you can switch between them by clicking the domain name in the left panel.

The top five boxes on the Dashboard provide roll-up numbers covering:

* **Total Subscribers** - The currently available audience for messaging via Notifications.
* **New Subscribers Yesterday** - The number of visitors who opted-in to receive Notifications yesterday.
* **Attrition Yesterday** - The percentage of your available audience which became unsubscribed/unavailable yesterday.
* **Clicks Yesterday** - The number of Clicks your Notifications generated yesterday.
* **CTR Yesterday** - Calculated as Clicks/Impressions, this is the engagement rate for your Notifications yesterday.

![](/files/jBot5hA33OPmFNRtxNfF)

The top half of the "Performance Overview" section provides data associated with Content (Notification) performance. Hover over a day to view total Notification KPIs. Adjustments made to date grouping and date ranges will effect both sections of the "Performance Overview"

![](/files/MplnkY6OYI3J47b7qon7)

The lower half of the "Performance Overview" section provides data associated with how many new Subscribers you are gathering. The date selector in the top right of the "Performance Overview" section will also adjust the Subscriber data.

![](/files/LZQOzmBGKfTaum1k1qd4)

The bottom of the Dashboard contains "Recent Notifications", the full list of Notifications can be found in the Notification section of the platform.

![](/files/QT98MeUFej23yfymCrBq)


# Notifications

## Notification List

The Notification List page displays all notifications that are scheduled to be sent or have previously been sent. Notifications are displayed in a chronologically descending order with scheduled notifications at the top followed by the mostly recently sent notifications.

Each row in the table represents a unique notification sent to an audience and contains all relevant information about the notification:

* Title
* Body
* Delivery Status
* A/B Test Name
* Audiences
* Feature Icons
* Delivery/Schedule Date
* Notification Statistics&#x20;
* Actions

### Actions

![](/files/HoVDR4IZIuLlGJRtnXLj)

The actions dropdown contains the following options:

* View: Displays a page with a preview and full delivery statistics for that notification.
* Duplicate: Opens the notification create page with a duplicate of the notification for quick alterations and deployment.

If the notification is part of a test additional actions are available:

* View Test: Displays a page with aggregate test statistics and statistics for each variant in the test.
* Duplicate Test: Opens the notification create page with a duplicate of the test for quick alterations and deployment.

## Notification Details

The Notification Details page contains all relevant information about the notification and its performance over time.

![](/files/EQXsKC1eJrGplkYnFJy8)

## Creating a Notification

To create a new notification, click the **Create Notification** button. The Create Notification page contains the following sections:

#### **Delivery Channels**

<figure><img src="/files/6zD6Ji4kihNZOo8zDJ5R" alt=""><figcaption></figcaption></figure>

Select which channels to deliver the notification through by toggling each channel on or off:

* **Web** — Sends the notification to web push subscribers.
* **Native Apple** — Sends the notification to iOS app subscribers.
* **Native Android** — Sends the notification to Android app subscribers.

You can enable any combination of channels for a single notification. The **Audience Size** displayed in the top-right panel updates to reflect the total number of subscribers across the selected channels.

#### **Audience**

<figure><img src="/files/mPUlticDMTTOsiZ0qlxK" alt=""><figcaption></figcaption></figure>

Choose which subscribers will receive the notification:

* **All Subscribers** — Delivers the notification to your entire subscriber base.
* **Specific Segments** — Delivers the notification only to subscribers who match one or more saved segments.

Toggle on **Exclude segments** to remove specific subscriber segments from the selected audience.

#### **Content**

The **Content** section is where you compose the notification message. Each notification starts with a single **Notification** tab. You can add additional variants to create an A/B test — see the A/B Testing page for details.

<figure><img src="/files/TkVq3Owh51aSyayxe0CM" alt=""><figcaption></figcaption></figure>

#### **Destination**

<figure><img src="/files/YMXOz0C3qpX4SHufZVDL" alt=""><figcaption></figcaption></figure>

* **Landing URL** — The URL that opens when a subscriber clicks the notification. Use the **{ }** button to insert dynamic macros into the URL. Use the **Content Tools** button to access **Pushly AI** content generation and **Page Analysis** tools — see the [Pushly AI](/platform/notifications/pushly-ai) page for full details.
  * In addition to full AI content generation, the **Content Tools** dropdown provides two **Page Analysis** options that work with your landing page URL:
    * **Autofill Content**
      * Select **Autofill content** to automatically populate the notification's title and body fields based on the content found on the landing page. This provides a quick starting point that you can then customize.
    * **Review Content Suggestions**
      * Select **Review content suggestions** to view AI-generated suggestions without automatically applying them. This allows you to evaluate the suggestions and manually apply the ones you prefer.

* **Use a Different URL for Native App** — Toggle this on to specify a separate deep link URL for native app subscribers (e.g., `myapp://story/12345`).

Click **Character Limits** in the top-right corner of the Content section to view the character limits for each platform.

**Message**

* **Title** — The headline of the notification. It is recommended the character count remain below 40. Use the emoji picker or **{ }** macro button to insert dynamic content.
* **Body** — Additional content which is included below the title. Character limit heavily depends on the subscriber's device and browser combination, however no more than 150 characters are recommended. Use the emoji picker or **{ }** macro button to insert dynamic content. Use the expand button to enlarge the text area for longer messages.

**Image and Icon**

* **Image** — Click **Upload** to add a large image to the notification. This image is displayed underneath the title and body on Windows and Android. Recommended minimum size is 360x180 with an aspect ratio of 2:1.
* **Icon** — This image is displayed as a medium-size icon next to the title and body on all platforms. Recommended size is 512x512 with an aspect ratio of 1:1. A default icon is pre-configured for your domain and can be overridden per notification.

#### **Tracking**

* **Keywords** — Every user who clicks on this notification will be automatically tagged with the provided keywords. You can then build user segments based on users tagged with specific keywords. You can add multiple keywords by pressing the enter/return key or entering a comma after each individual keyword.

#### **Behavior**

* **Lifespan** — Determines how long the notification will attempt to be delivered to the subscriber. The default is **7 Days**. Use the dropdown to change the time unit.

#### **Channel Settings**

Expand the **Native: iOS** or **Native: Android** sections to configure platform-specific delivery settings for each native channel.

#### **Preview**

The right-side panel provides a real-time preview of your notification as you compose it.

* **Platform** — Select the platform to preview (e.g., **Android (Web)**, **iOS**).
* **State** — Toggle between **Expanded** and **Collapsed** views.
* **Send Personal Preview** — Click to send a test notification to your own device before delivering to the full audience.

### **Delivery**

<figure><img src="/files/pwyIXFTp1ZwOluULVD6Q" alt=""><figcaption></figcaption></figure>

Configure when and how the notification is sent:

* **Immediate** — The notification will be delivered to the audience immediately.
* **Scheduled** — Schedule the notification for a future date and time.

**Sending the Notification**

Once you have configured all sections, click the **Send Now** button to deliver the notification. You can also save the notification as a draft by selecting **Save Draft** from the dropdown. The **Send Now** button includes a dropdown arrow for additional send options. Click **Cancel** to discard the notification without sending.

## Key Terms

While creating a Notification you will encounter the following terms:

| Term              | Description                                                                                                                                                                                                                                                                                                                                                                                                        |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Audience          | <p>The segment of subscribers that will receive the notification:<br>-<em>All Subscribers</em>: The notification will be delivered to all subscribers on your site.<br>-<em>Specific Segments</em>: Choose individual segments that will receive the notification.</p><p><em>-Exclude Segments</em>: Choose individual segments that should not receive the notification. </p>                                     |
| Title             | The headline of the Notification. It is recommended the character count remain below 40. You also have the ability to add Emoji's in by clicking the smiley face icon in the Title text bar.                                                                                                                                                                                                                       |
| Body              | Additional content which is included below the Title. Character limit heavily depends on the subscribers device and browser combination, however no more than 150 characters are recommended. You also have the ability to add Emoji's in by clicking the smiley face icon in the Body text bar.                                                                                                                   |
| Image             | This image will be displayed underneath the title and body of the notification on Windows and Android. Recommended minimum size is 360x180 with an aspect ratio of 2:1.                                                                                                                                                                                                                                            |
| Icon              | This image will be displayed as a medium-size icon next to the title and body of the notification on all platforms. Recommended size is 512x512 and an aspect ratio of 1:1.                                                                                                                                                                                                                                        |
| Badge             | This image will be displayed as a small black-and-white icon in the top left corner of an Android notification. Specs: 128x128 pixels, 1:1 ratio, monochromatic, with alpha transparency. If you send an image with both black and white areas, it will show up as an all-black square. PNG is preferred format.                                                                                                   |
| Landing URL       | The URL for which the subscriber should be directed to after clicking on the notification.                                                                                                                                                                                                                                                                                                                         |
| Keywords          | <p>Every user who clicks on this notification will be automatically tagged with the provided keywords. You can then build user segments based on users tagged with specific keywords.</p><p>You can add multiple keywords by pressing the enter/return key or entering a comma after each individual keyword.</p><p>Your Account Manager can also upload a preexisting list if you have one you'd like to use.</p> |
| Customize Buttons | By default, all notifications have a “Visit Site” or "Read More" button that, when clicked, directs the user to the primary landing URL. Enabling this setting allows you to customize the button behavior, label, & landing URL and add an optional 2nd button.                                                                                                                                                   |
| Silent Delivery   | When enabled, the notification will be delivered with no alert sound or vibration on supported mobile devices.                                                                                                                                                                                                                                                                                                     |
| Lifespan          | <p>Determines how long the notification will attempt to be delivered to the subscriber.</p><p>For example: A notification with a lifespan of 1 day will not be delivered to a user who does not become eligible for delivery within 24 hours of the notification being sent.</p>                                                                                                                                   |

## Character Limits

The title and body of the notification only support a specific number of characters before the text is cut off. The number supported depends on several factors including:

* The subscriber's operating system
* The subscribers' browser
* Whether an image was provided within the notification

The following are guidelines that can be used to ensure the greatest number of subscribers receive a notification with proper formatting:

| OS: Browser             | Title Characters | Body Characters |
| ----------------------- | ---------------- | --------------- |
| Android: All Browsers   | \~50             | \~150           |
| Windows: Chrome         | \~40             | \~0-150         |
| Windows: Firefox        | \~40             | \~149-190       |
| Mac OS: Chrome, Firefox | \~20-40          | \~20-50         |


# Pushly AI

### Pushly AI

Pushly AI uses artificial intelligence to generate notification content based on your landing page. It can automatically create titles, body text, images, and keywords — and even suggest multiple A/B test variants — by analyzing the content of the URL you provide.

#### Accessing Pushly AI

Pushly AI is accessed from the **Content Tools** button in the **Content** section of the Create Notification form. After entering a **Landing URL** in the **Destination** section, click the **Content Tools** button (located to the right of the Landing URL field). A dropdown appears with two sections:

* **Pushly AI**
  * **Generate AI content** — Analyzes your landing page and generates AI-powered notification content suggestions.

#### Generating AI Content

<figure><img src="/files/JDPgjlvXib49FLeC9i0i" alt=""><figcaption></figcaption></figure>

To generate AI-powered notification content:

1. Enter a **Landing URL** in the **Destination** section of your notification.
2. Click the **Content Tools** button next to the Landing URL field.
3. Select **Generate AI content** from the dropdown.
4. The **Pushly AI Generated Results** modal will appear with up to 4 variant suggestions.

#### Reviewing AI-Generated Variants

The **Pushly AI Generated Results** modal displays up to 4 AI-generated variant cards. Each variant card contains:

* **Toggle** — Enable or disable the variant. Disabled variants will not be applied when you accept selections.
* **Usage dropdown** — Determines how the variant will be applied:
  * **Use as notification** — Replaces the current notification content with this variant's content.
  * **Use as new test variant** — Adds this variant as a new A/B test variant tab in the Content section.
  * **Variant not enabled** — Displayed when the variant toggle is off.
* **Tone badge** — A colored label indicating the tone of the generated content (e.g., **Dramatic**, **Inspiring**, **Informative**, **Curious**).
* **Why this Variant?** — A brief AI-generated explanation of the content strategy for this variant.
* **Content fields** — Each variant includes generated content for:
  * **Title** — The suggested notification headline.
  * **Body** — The suggested notification body text.
  * **Image** — A suggested image for the notification.

Each content field has a checkbox that allows you to select or deselect individual fields. Only checked fields will be applied when you accept the variant.

<figure><img src="/files/pzU4mciONbkwjM4ErgoO" alt=""><figcaption></figcaption></figure>

#### Applying AI-Generated Content

At the bottom of the modal, a summary line shows how many selections you have made (e.g., "Selections: 1 new test variant, 1 replacement"). To apply your selections:

1. Enable the toggle for each variant you want to use.
2. Select the usage type from the dropdown (**Use as notification** or **Use as new test variant**).
3. Check or uncheck individual content fields (Title, Body, Image) to control which elements are applied.
4. Click the **Accept Selections** button to apply all enabled variants.

Variants set to **Use as notification** will replace the content in the current notification tab. Variants set to **Use as new test variant** will be added as new variant tabs in the **Content** section, automatically enabling A/B testing.

> ℹ️ Selecting multiple AI suggestions will enable A/B testing. The variant tabs in the Content section will be updated to reflect the selected variants.

#### AI Content in Notification Details

<figure><img src="/files/pyFW9h59I4RrvtyDop0i" alt=""><figcaption></figcaption></figure>

After a notification that used Pushly AI content has been delivered, the **Notification Details** page displays additional metadata in the **Content** section:

* **Content Source** — Displays **Pushly AI** to indicate the content was AI-generated.
* **AI Content Tone** — Shows the tone that was applied to the notification (e.g., **Urgent**, **Dramatic**, **Inspiring**).
* **AI Content Reason** — A brief description of why the AI chose this content approach.


# A/B Testing

A/B Testing allows you to test visual and behavioral changes within a single notification against portions of your audience to determine what works best.

## Creating an A/B Testing

![](/files/IwTKYe5mFWuXpPAmSEuy)

A/B testing is automatically enabled when you add more than one notification variant. To create an A/B test:

1. From the Create Notification page, locate the **Content** section. Your initial notification appears under the **Notification** tab.
2. Click the green **+** button next to the variant tab. A dropdown appears with two options:
   * **Add Blank Test Variant** — Creates a new empty variant that you can configure from scratch.
   * **Duplicate Notification** — Creates a new variant pre-filled with the content from the current notification.
3. Once a second variant is added, the tabs are automatically renamed to **Variant A**, **Variant B**, and so on.
4. Click on each variant tab to configure its content independently — you can vary the title, body, image, landing URL, or any other content field.

⚠️ Test naming and traffic distribution options appear at the end of the form when multiple variants are added.

### Managing Variants

<figure><img src="/files/YGIGFQHAOsDcd3YHlnUt" alt=""><figcaption></figcaption></figure>

Each variant tab has a **three-dot menu** ( **⋮** ) that provides options for managing that variant.

When multiple variants exist, the menu for each variant tab contains:

* **Duplicate** — Creates a copy of this variant as a new tab.
* **Reset content** — Clears all content fields in this variant and returns them to their default empty state.
* **Remove** — Deletes this variant from the test.
* **Remove All Other Test Variants** — Removes all other variants, keeping only the selected one and returning to a single notification (non-test) state.

### Test Name

Naming your test is important for future identification of notifications that comprise a test as well as identifying performance for reporting and analytics purposes within the platform.

{% hint style="info" %}
A test name cannot be used more than once. We suggest adding dates to test names if you want to a single test multiple times.&#x20;

For example: `Image Test / 2020-02-10`
{% endhint %}

## Variant Distribution

Once you've created each variant you want to test, the `Test Distribution` section on the top right- hand side of the page provides options that allow you to determine how your test will be deployed to your audience.

### Split Delivery

Split Delivery allows you to choose a percentage of your audience that each variant should receive. This distribution type allows you to test new variants on small percentages of your audience while ensuring clickthrough rates stay high with what you already know works.

![](/files/-MUyhuTOVKhhHVMZrmp7)

### Sample Delivery

Sample Delivery delivers a sampling percentage of your audience to each variant before deploying the best performing notification to the rest of your audience.&#x20;

Use the sliders to determine what percentage of your audience each variant and the winner will receive. Each variant must receive at least 5% of the audience's delivery.

![](/files/-MUyhxHDgaB9Gp6QJIoO)

When Sample Distribution is chosen a `Winner Strategy` section appears that allows you determine when the variant with the best performance is deployed to the rest of your audience. There are two ways to configure this timing:

1. **Time Elapsed After Send:** Specify the number of hours that have elapsed after the initial variants were sent.
2. **Specific Time:** Schedule the winning notification to be delivered at a specific date and time in the future.

{% hint style="warning" %}
This option is not available for audiences with fewer than 5,000 subscribers.
{% endhint %}

#### Performance Constraint (Sample Delivery Only)

When running a Sample Delivery test, you have the option to activate a performance constraint. If none of your deployed variants in the A/B Test reach the CTR value placed in the performance constraint, then no Notification will be deployed to the remaining audience.


# Custom Buttons

Notification Custom Buttons allow you to add up to two additional buttons to your deployed Notifications. This enables you to give the subscribers multiple buttons from which they can choose.

⚠️ Custom buttons are available for Web and Android only. They are not supported on iOS native notifications.

## Enabling Custom Action Buttons

When creating a Notification, you will see a "Custom Buttons" option within the Interactions section. First, input your Primary Landing URL as normal and then enable the switch show the `Custom Buttons` configuration section.

## Configuring Action Buttons

Every notification is required to have at least 1 button. Each button has 3 elements to configure:&#x20;

| Field      | Definition                                                                                                                                                                                                                                            |
| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Action** | What occurs when the subscriber clicks on the button. "Open URL" will send the subscriber to the designated page you input. "Dismiss Notification" will hide the notification and remove it from the subscriber's action center or notification tray. |
| **Label**  | The text the subscribers will see on the notification's button. Customize this button to provide the most appropriate call to action for the delivered content.                                                                                       |
| **URL**    | Where the subscriber will be delivered when they click the action button. You may choose to use the primary landing URL entered above or disable the switch to enter a URL specific to this action button.                                            |

### Modifying the First Button

When you activate Custom Buttons, the first button will have the `Open URL` action with a `Read Now` label set by default. It will also be set to direct the subscriber to the primary landing URL when the button is clicked. Each of these behaviors can be customized to provide the most relevant behavior for the notification's content.

![Button 1 uses the already configured Primary Landing URL.](/files/Wz9K3YfVuiGGyB4SxVmO)

### Adding a Second Button

Press the `Add Button` option to create a second button for customization. As you can see below, we will use a CTA of `Option 2` for this Label

After you input your Label, input the URL you wish to send subscribers to if they engage with the `Option 2` button. In this example, they will go to `landingpage-2.com/example` rather than the Primary Landing URL used.

![](/files/1ClGeItTuC7rrSCQdAjQ)

The Inline Preview will show you what your subscribers will see:

![](/files/cHyltOIc3TZTEz2kKWjo)

In the above example, if the subscriber clicks anywhere on the notification or the `Option 1` button they will be sent to `the Primary Landing URL`. If they click on the `Option 2` button they will be sent to `landingpage-2.com/example`.

### Configuring a Dismiss Behavior

If you prefer to give your subscribers the option to dismiss the notification upon clicking a button, simply change the `Action` drop-down to `Dismiss Notification` on Button 2 and update your label.&#x20;

![](/files/2iMqu1WpLmowQoND6AcJ)

## Changing the Default Button Label

The first button's label is set by your Account Manager by default. You can change this by accessing your domain settings under the `Notification tab`. There you can click  `Edit` and input a different label under the Button Defaults section.

{% hint style="danger" %}
**Remember to click the `Submit` button before closing to ensure your changes are saved.**
{% endhint %}


# Notification Previews

## Sending Previews

During notification creation a real-time `Preview` shows on the right side of the page. The platform preview is a best-guess at how this notification will look across the most common devices for the selected operating system.

While this preview is a good indication of what the notification may look like at time of delivery, you may want to see it exactly as it would appear on your device or send a preview to the rest of your team to get feedback or approval.

After you have constructed the `Content` portion of your notification, you have two options to send previews:

1. **Send Personal Preview:** A preview notification will be delivered to each device you have registered for previews.
2. **Send Team Preview:** A preview notification will be delivered to all platform users that have registered for previews for that domain.

{% hint style="info" %}
If this is your first time sending a preview you will be prompted to accept notification permissions after you click the `Send Preview` button.
{% endhint %}

![](/files/UwbcR9TBya6FZWlLZTlg)

## Registering Additional Preview Devices

If you'd like to receive notification previews on a different device from what you are using to create the notification, you may register additional devices. Please see the tooltip within the platform for information on how to register additional devices.

After accepting permissions:

* If you only have access to one domain you will receive a confirmation message of successful subscription.
* If you have access to multiple domains you will be presented with a dropdown list of domains. Select each domain for which you'd like to receive previews and then tap the `Update Domain Registrations` button to complete preview registration.

When finished, you can use the `Send Personal Preview` functionality from your desktop computer to begin receiving previews on the newly registered device.

{% hint style="warning" %}
The preview functionality will only work on devices that support web push.
{% endhint %}


# Inline Segmentation

Our platform allows you to create and use Segments while in the process of creating Notifications for deployment. If you do not see this option, please contact your Account Manager for access

### Using Inline Segmentation

When selecting your audience, click the Specific Segments button. If the Segment you wish to send to does not currently exist, click the Create New Segment option &#x20;

![](/files/-M4KZmW8Cy7UXWy-nlao)

You will then see the Segmentation tool slide out from the right side of the screen, you can use this tool just as you use the regular Segmentation tool.

![](/files/-MQmi_B1RZ8HBHi-3thA)

Once you complete creating the desired Segment, click Submit and your Segment will be auto-selected for sending. You will also be able to access this Segment again through the Segment page and drop down

### Single-Use Segments

If the segment you are creating will only be used once for this specific notification you are creating you can enable the `Single-Use Segment` switch in the form. Enabling this switch will ensure that the new created segment is only targetable by the current notification you are creating and will not be shown on the Segment List page.


# Notification Templates

Use Notification Templates to pre-populate assets to be used in future Notifications

Navigate to the Notifications page and click the arrow to the right of Schedule Notification. You will see an option to Create Template

<figure><img src="/files/OjGGDklv8hKuvyGG2Gbp" alt=""><figcaption></figcaption></figure>

If you use Pushly for both web and app support, you will have the option to select for which channels the Template is intended

<figure><img src="/files/AsNL171GC3MCtSnYEe0A" alt=""><figcaption></figcaption></figure>

Name your Template and provide a description to help identify the purpose of the Template. In this example, we'll create a breaking news template for quick Notification creation and deployment

<figure><img src="/files/JMwUmPCqPqkYtQfKw5uB" alt=""><figcaption></figcaption></figure>

Input the assets and copy you want to populate when you select the Template for use. In this example, we input a Title and Image, but leave the Body and Landing URL blank so the user can input assets related to the story

<figure><img src="/files/epTcE0zBBuAXeZflvRAK" alt=""><figcaption></figcaption></figure>

Once finished inputting assets, scroll to the bottom and click Create Template

<figure><img src="/files/vTEqnFJ7v1Rp5eUhAxxe" alt=""><figcaption></figcaption></figure>

Once a Template is saved, it will appear on the Template list for future use

<figure><img src="/files/N9qbQvUq9YRfYMqAccUh" alt=""><figcaption></figcaption></figure>

To use existing Templates, there are two options:

1. You can go directly to Templates page and click New Notification

<figure><img src="/files/9aovwotZ4y9fekYNq0E8" alt=""><figcaption></figcaption></figure>

2. From the Notifications page, click Schedule Notification. There will be a new box at the top of the page from which you can select existing Templates

<figure><img src="/files/ikLiHiKKL0er89KvrBHp" alt=""><figcaption></figcaption></figure>

Then input the relevant assets, such as Landing URL and Body, and send your Notification

<figure><img src="/files/c5KCwLhd6rxzLw7amUf8" alt=""><figcaption></figcaption></figure>


# Notification Feeds

Use Feeds to automatically ingest fresh content from your sources and power targeted, timely Notifications

Navigate to the Notifications page and click the arrow to the right of Schedule Notification. You will see an option to Create Feed

<figure><img src="/files/7NUcZkDj23KhqimfJ4Qp" alt="" width="419"><figcaption></figcaption></figure>

Name your Feed, select the appropriate **Feed Type** (RSS, JSON, or XML), and input the Feed URL where the platform will pull articles from. Make sure you set the Feed Status toggle to active so the Feed can begin processing new content. If you would not like to send notifications to your users, keep the Feed Status toggle as Inactive

<figure><img src="/files/be17UXAvgYj1X7HeQVP4" alt=""><figcaption></figcaption></figure>

Set the Publishing Mode to **Auto Publish** if you want Notifications created from the Feed to be automatically queued and sent when new content is available. If you prefer to review each Notification before it is sent, select **Create as Draft** so Notifications remain in a draft state until you approve them. Keep the setting at Auto Publish if you do not want to manually review and publish each Notification.

<figure><img src="/files/LMBhW4ilvSaBgAlYEiRX" alt=""><figcaption></figcaption></figure>

If you use Pushly for both web and app support, you will have the option to select for which channels the Feed is intended

<figure><img src="/files/edZThjdO3VmbjMXVXY5q" alt=""><figcaption></figcaption></figure>

Select the audience Segment you want the Feed to send to. If you want the Feed to send to a specific Segment, create that Segment before configuring the Feed so it appears as an option in the dropdown. If you do not select a specific Segment, the Feed will send to **All Subscribers** by default.

<figure><img src="/files/1OAB10egGHV9eATwH1cN" alt=""><figcaption></figcaption></figure>

Set the **Delivery Strategy** to control when and how often notifications are created from the Feed

<table><thead><tr><th width="197.25">Delivery Strategy</th><th>Description</th></tr></thead><tbody><tr><td>ASAP</td><td>Create a notification every time a new article is discovered, as long as a previous notification has already been sent based on the specified send frequency.</td></tr><tr><td>Fixed Time</td><td>Schedule one notification per day at the specified Send Time in the domain time zone. If no new article is found at that time, the system continues checking for up to 1 hour after the Send Time</td></tr><tr><td>Subscriber Time Zone</td><td>Schedule a notification at the specified Send Time in each subscriber’s time zone. To support all time zones, the article is selected at least 24 hours before the Send Time</td></tr></tbody></table>

Choose the **Send Time and Send Days** to define the exact time and days of the week when notifications from this Feed should be delivered

Configure **Pre-Schedule** to determine how far in advance a notification is created before its Send Time.

* For example, if the Send Time is 2:00 PM and Pre-Schedule is set to 15 minutes, the notification is created at 1:45 PM and scheduled to deliver at 2:00 PM.

Apply **Holiday Filters** if relevant to your use case so notifications are not sent on certain holidays

<table><thead><tr><th width="214.41796875">Holiday Selection</th><th>Description</th></tr></thead><tbody><tr><td>None</td><td>No Holidays are applied and the feed can be configured to send notifications irrespective of holiday schedule</td></tr><tr><td>Country</td><td>Select from a list of countries you would like to align the holidays from</td></tr><tr><td>Financial/ Bank</td><td><p></p><ul><li><strong>NYSE</strong>: Uses NYSE market holidays, which cover all major US exchanges including NASDAQ.</li><li><strong>European Central Bank</strong>: Uses Trans-European market holidays, which cover Euro-based exchanges.</li></ul></td></tr></tbody></table>

Set **Notification Lifespan** to control how long the system will continue attempting to deliver a notification to a subscriber

* **Domain Default**: Use the current default lifespan configured in Domain Settings.
* **Custom**: Specify a custom lifespan that applies to all notifications created by this Feed.

<figure><img src="/files/BeibuqSpJ9pGG7yKAnug" alt=""><figcaption></figcaption></figure>

In **Notification Fields**, add or remove fields as needed and apply constraints to pull specific content from the Feed based on criteria such as Keywords, URL parameters, or other metadata. This allows you to fine tune which articles or items are eligible to generate Notifications from the Feed

The **Page Analysis** toggle, when enabled, will attempt to fetch and override the feed’s notification content from the landing URL’s metadata. This will include the **Title, Body, Image and Keywords** tagged for the landing URL

<figure><img src="/files/wkKfocIutdrRysk72GZE" alt=""><figcaption></figcaption></figure>

Use **Article Constraints** to control which articles from your Feed are eligible to generate Notifications

<table><thead><tr><th width="198.51953125">Setting</th><th>Description</th></tr></thead><tbody><tr><td>Article Age</td><td>Defines the maximum age an article can be in order to be eligible for this Feed. Articles older than the specified age will not be considered</td></tr><tr><td>Required Keywords</td><td>The article must contain at least one of the specified keywords to be eligible for this Feed. Keywords are not case sensitive but must be exact matches. This field does not support regular expression pattern matching</td></tr><tr><td>Restricted Keywords</td><td>If the article contains at least one of the specified keywords it will not be eligible for this Feed. Keywords are not case sensitive but must be exact matches. This field does not support regular expression pattern matching</td></tr><tr><td>URL Allow List</td><td>The article URL must contain at least one of the specified URL patterns in order to be eligible for this Feed. Wildcard matching is supported and this field accepts regular expression syntax for more advanced pattern matching</td></tr><tr><td>URL Deny List</td><td>The article URL cannot contain any of the specified URL patterns in order to remain eligible for this Feed. Wildcard matching is supported and this field accepts regular expression syntax for more advanced pattern matching</td></tr><tr><td>Title Deny List</td><td>The article Title cannot contain any of the specified patterns in order to be eligible for this Feed. Wildcard matching is supported and this field accepts regular expression syntax for more advanced pattern matching</td></tr><tr><td>Allow Article Recycling</td><td>When enabled, the Feed can re-send previously sent articles when no unsent articles are available. Recycled articles are sent in order from the oldest previously sent article to the most recently sent</td></tr></tbody></table>

Once a Feed is submitted, it will appear on the Feeds list for future use

<figure><img src="/files/iyoRxYDlL6EuBmXPbB7c" alt=""><figcaption></figcaption></figure>


# Macros / Liquid Templates

## Macro Basics

Macros allow you to personalize elements of a notification for each subscriber that receives it.

### Using the Macro List

The list of available macros can be shown by clicking the `{}` icon inside of the title, body, or landing URL text box. Once the list is open, click on any of the macros to automatically insert it.

![](/files/-MDzrSYokboEFegL0l_0)

{% hint style="info" %}
If you are sending subscriber properties [via our SDK](https://documentation.pushly.com/sdk/methods#adding-attributes-to-a-subscribers-profile) or a custom integration the macro list will also show these as options under the `Custom` grouping.
{% endhint %}

### Specifying a Default Value

It is always good practice to specify a default replacement value when using a macro. This ensures that even if the subscriber does not have a value for the chosen macro the text displayed will make sense.

To specify a default value add the following after the name of the macro:

```
{{profile.city | default: "your area"}}
```

### Examples

If you are sending a notification focused on a weather event and want to target a geographic area that includes multiple cities but want to ensure that the subscriber's city is displayed in the notification's title you could accomplish this with the following:

```
Tornado Warning in {{profile.city | default: "your area"}}. Take Cover!
```

&#x20;In the above example, if the subscriber who is receiving the notification has a city in their profile the title will be displayed as: `Tornado Warning in Miami. Find Shelter!`. If they do not have a city then the title will read `Tornado Warning in your area. Find Shelter!`.

{% hint style="info" %}
Remember to use our [Preview Functionality](/platform/notifications/notification-previews) to ensure your macros look the way you expect!
{% endhint %}

## Advanced Usage

Our macro functionality supports the use of [Liquid Templates](https://shopify.github.io/liquid/basics/introduction/) to add more customization to the substituted value. We support both the control flow and iteration tags as well as the use of most filters.&#x20;

If you would like help constructing macros for your notification please reach out to your account manager. &#x20;

### Filter: Date Format

You can use the `date` filter to convert a date to any format you desire. For example, to provide the date the subscriber opted in to notifications as part of your landing page URL:

```
https://ex.com/?subscribed={{profile.subscribed_date | date: '%Y-%m-%d'}}
```

The following date format strings are supported:

| Input | Format                                     |
| ----- | ------------------------------------------ |
| %a    | The abbreviated weekday name (E.g., `Sun`) |
| %A    | The full weekday name (E.g., `Sunday`)     |
| %b    | The abbreviated month name (E.g., `Jan`)   |
| %B    | The full month name (E.g., `January`)      |
| %d    | Day of the month (01..31)                  |
| %e    | Day of the month (1..31)                   |
| %H    | Hour of the day, 24-hour clock (00..23)    |
| %I    | Hour of the day, 12-hour clock (01..12)    |
| %k    | Hour of the day, 24-hour clock (0..23)     |
| %l    | Hour of the day, 12-hour clock (0..12)     |
| %m    | Month of the year (01..12)                 |
| %M    | Minute of the hour (00..59)                |
| %p    | Meridian indicator (E.g., `AM` or `PM`)    |
| %S    | Second of the minute (00..60)              |
| %Y    | Year with century                          |
| %%    | Literal `%` character                      |

### Filter: Relative Dates

The `relative_date` filter allows you to provide a date in a wide range of grammatical date formats and have it converted to an absolute date.

For example, if you need to include last Saturday's date in the URL of your notification and format it as `Year/Month/Day` you can provide the following macro:

```
https://ex.com/{{'the saturday before today' | relative_date | date: '%Y/%m/%d'}}/example
```

{% hint style="info" %}
Note the inclusion of the `date` filter to format the date as desired. If not provided, the `relative_date` filter will return an [ISO-8601](https://en.wikipedia.org/wiki/ISO_8601) formatted date (E.g.,`2020-09-10T15:41:41).`
{% endhint %}

Some additional examples of the type of relative dates formats you can use:

```
next thursday
last wednesday
today
tomorrow
yesterday
next week
next month
next year
3 days from now
three weeks ago
```

Additionally, you can prefix any of the above with the following to further alter the computed date:

```
day after
the day before
the monday after
the monday before
2 fridays before
4 tuesdays after
```

{% hint style="warning" %}
Since there is an ambiguity in the grammar of relative date formats always use the [Notification Preview](/platform/notifications/notification-previews) functionality to ensure the relative date is properly interpreted.
{% endhint %}


# Remote Fetch

Explains the remote\_fetch Liquid tag: what it is, how to use it, all supported options, and how errors are handled.

## What is remote\_fetch?

`remote_fetch` is a custom Liquid inline tag that performs an HTTP request at render time and exposes the parsed result under an alias of your choice. It’s built for templated content that needs fresh, per‑user data (e.g., weather, inventory, personalization).

Highlights:

* Simple: fetch a URL and use its result in the template.
* Safe: on error, it soft‑fails by default.
* Composable: you can chain multiple fetches (e.g., use output from the first in the second).
* Flexible: supports headers, timeouts, response size caps.

## Basic syntax

```liquid
{% remote_fetch 'https://api.example.com/users/{{profile.user_id}}' alias:'data' %}
Hello {{ data.first_name }}
```

Key parts:

* URL (required): quoted string; can include Liquid interpolation from the user's profile.
* alias: the variable name under which the parsed response is available. defaults to `response`

Inside the tag, render whatever you want; the alias is available in scope.

## Supported options

* `alias`
  * The variable name bound to the parsed value.
  * Default: `response`
* `timeout_ms`:
  * Per‑request read/connect timeout.
  * Default: 750ms
* `max_bytes`:
  * Hard cap on response size in bytes. Protects from unexpectedly large payloads.
  * Default: 1MB
* `headers`:
  * Quoted JSON object. Header values may include Liquid interpolation.
  * Header names are handled in a case‑insensitive manner.
  * Example: `'{ "Header-Name": "value", "X-Foo": "bar-{{macro}}" }'`

## URL interpolation

You can embed Liquid expressions in the URL:

```liquid
{% remote_fetch 'https://api.example.com/users/{{profile.user_id}}?lang={{profile.locale}}' alias:'user' %}
```

These are resolved before the request is made.

## Request Chaining

The recommended, easy‑to‑reason‑about approach:

* Fetch the first URL and set alias A.
* If A exists, fetch the second URL and set alias B.
* At the end, if A and B exist, render success; otherwise render a single fallback.

Example:

For a given article URL, show a “Read now” CTA if allowed; otherwise “Subscribe to read”.

```liquid
{% remote_fetch 'https://cms.example.com/resolve?path={{landing_url}}' alias:'doc' %}

{% if doc %}
  {% remote_fetch 'https://paywall.example.com/entitlements?user_id={{profile.user_id}}&doc_id={{landing_url}}' alias:'ent' %}
{% endif %}

{% if doc and ent and ent.allowed %}
  Read now
{% else %}
  Subscribe to read
{% endif %}
```

## Headers

Add custom headers via the headers option as a quoted JSON object:

```liquid
{% remote_fetch 'https://api.example.com/secure'
   headers:'{"Authorization":"Bearer {{secrets.api_key.token}}"}',
   alias:'data' %}
```

* Values are interpolated first, then sent.
* Keep plain-text secrets out of templates where possible; prefer managed secrets embedded within the template as outlined in the Secrets documentation.

## Secrets

{% hint style="info" %}
Contact your account manager for details on how to set up Secrets for Remote Fetch.
{% endhint %}

### Summary

* Secrets let you reference sensitive values (like API keys or tokens) in templates without hardcoding them.
* They are injected into the template context under the `secrets` object.
* You can reference simple and structured secrets, use them in remote\_fetch URLs/headers, and combine them with Liquid filters.

### Usage

* Simple secret:
  * `{{ secrets.partner_api_key }}`
* Structured secret (object with multiple fields):
  * `{{ secrets.basic_auth.username }}`
  * `{{ secrets.basic_auth.password }}`
  * `{{ secrets.partner.token }}`

Using secrets with remote\_fetch

* In headers (recommended):
  * Keep credentials out of URLs by using headers when possible.
  * Example:&#x20;

    ```
    {%
      remote_fetch 'https://api.partner.com/v1/data?city={{profile.city }}' 
      headers:'{ "X-Api-Key": "{{ secrets.secret_name.token }}" }', 
      alias:'data' 
    %}
    ```

* In URLs:
  * For providers that require tokens in the query string.
  * Example:<br>

    ```
    {%
       remote_fetch 'https://api.partner.com/v1/data?token={{secrets.partner.token }}&city={{ profile.city }}' 
       alias:'data' 
    %}
    ```

Working with structured secrets

* Basic auth example:
  * Username: `{{ secrets.basic_auth.username }}`
  * Password: `{{ secrets.basic_auth.password }}`
  * Combine in a header: <br>

    ```
    {% 
      remote_fetch 'https://api.example.com/account' 
      headers:'{ "Authorization": "Basic {{ secrets.basic_auth.username }}:{{ secrets.basic_auth.password }}" }', 
      alias:'account'
    %}
    ```

### Best practices

* Prefer headers for credentials
  * Use Authorization or a custom header whenever the provider supports it.
* Use the `default` filter for resilience
  * Always provide a sensible fallback when a secret might be missing or not yet configured.
* Avoid printing secrets in content
  * Don’t include secrets (even partial) in titles, bodies, or output visible to users.
* Missing secrets&#x20;
  * Liquid resolves missing keys to empty by default; use default to provide fallbacks.

## Timeouts and Size Limits

* `timeout_ms` sets a per‑request timeout (connect + read). Choose values that balance user experience and backend SLAs.
* `max_bytes` caps the response body size. Use a safe ceiling for your endpoint; it protects both servers and renderers.

## Caching and Repeated remote\_fetch Calls

Many authors place the same remote\_fetch URL in multiple template properties (for example, title, body, and landing URL). This section explains how caching works so you don’t make unnecessary network calls.

### Summary

* Requests are cached in memory by URL + method + headers.
* The first time a unique request is made, it hits the network and caches the response for a short time.
* Subsequent identical requests (even within the same template render) are served from cache.

### What is cached

* Response body and status for GET requests
* Keyed by:
  * The fully rendered URL (including query string and any Liquid interpolation)
  * HTTP method (GET)
  * Effective headers sent (names are normalized)

If any of those change, it counts as a different cache key and results in a separate request.

### When the cache is used

* Within the same render: The first call populates the cache. Any identical subsequent calls during that render re-use the cached response.
* Across nearby renders: As long as the cache entry hasn’t expired and hasn’t been evicted due to memory pressure, identical requests will be served from cache.

### Scope and lifetime

* Scope: In‑memory cache local to the running process. It’s not shared across servers or regions.
* TTL: Each entry has a short time‑to‑live suitable for “fresh enough” personalization. After TTL, the next identical request will refresh it.
* Capacity: The cache is bounded. When full, expired entries are cleared first; if still full, older entries are evicted.

## Frequently asked questions

* How do I handle a single fallback when chaining multiple calls?
  * Use the sequential pattern: guard the second fetch with `{% if firstAlias %}`, then a single final `{% if firstAlias and secondAlias %} … {% else %} fallback {% endif %}`.
* Can I show custom content on fetch errors?
  * Yes. With the default soft‑fail, the alias is unset. Use a conditional to show your message.
* Can I pass headers that use Liquid values?
  * Yes, via `headers:'{...}'`. Values may include `{{ ... }}`.


# Abandon Delivery

Abort a delivery from within a Liquid template, intentionally and explicitly. Use this when your template logic determines that the notification should not be sent for the current recipient.

* Tag name: `abandon_delivery`
* Effect: immediately aborts template rendering and marks the delivery as Abandoned
* Retries: none (intentional non-send)
* Impressions: not counted

When to use

* A required resource didn’t resolve (e.g., a remote fetch didn’t produce usable data).
* Business rules exclude this recipient (e.g., entitlement or audience checks).

Syntax

* Control tag form:
  * {% abandon\_delivery %}

Notes

* The tag must be executed to take effect; place it in branches that should abort.
* It does not render any output; rendering stops immediately.

Patterns

Abort when content wasn’t found

```
{% remote_fetch 'https://cms.example.com/resolve?path={{landing_url}}' alias:doc %}
{% if doc %}
  Read now
{% else %}
  {% abandon_delivery %}
{% endif %}
```

Abort on entitlement check

```
{% remote_fetch 'https://paywall.example.com/entitlements?user_id={{profile.user_id}}&doc_id={{landing_url}}' alias:ent %}
{% if ent and ent.allowed %}
  Read now
{% else %}
  {% abandon_delivery %}
{% endif %}
```

Abort for audience rule

```
{% if profile.country_code != 'US' %}
  {% abandon_delivery %}
{% endif %}
```

Behavior and observability

* Rendering stops immediately when the tag executes.
* The delivery is recorded as Abandoned (intentional).

Authoring guidance

* Place the tag inside the else (or unless) branch of your decision logic so it only triggers when appropriate.
* Keep decision logic simple and explicit so intent is clear in reviews and analytics.

FAQ

* Can I place it outside an if/else?
  * Yes, but it will always abort. It’s best used conditionally.
* Does it print anything?
  * No; it halts rendering immediately.

Troubleshooting

* “It didn’t abort”: Ensure the branch containing the tag actually executed (verify your conditions and aliases).


# Native App Push Notifications

The first step to sending Native App Notifications from the Pushly platform is to implement it on your site. [Linked here](https://documentation.pushly.com/integration/implementation-steps) you can find that documentation. If you haven’t done so already, please let your Account Manager know you’re implementing Native App so they can enable flags on the Pushly side. &#x20;

### Sending a Native App Notification

To send a Native App Notification, the process will be very similar to what you already know when sending a web-push Notification. In the ‘Create Notification’ page, you’ll see the Delivery Channels listed.

<figure><img src="/files/naLTFR0PIATFG1xQrkRi" alt=""><figcaption></figcaption></figure>

When selecting your Audience, you can hover over the Audience Size in the top right and it’ll show you how many users you’re targeting for each channel.

<figure><img src="/files/74oxwvITkTAUaxo3gb08" alt=""><figcaption></figcaption></figure>

When targeting Native users via Segments, you’ll need to ensure you create Segments specifically for Native channels. All existing Segments will be defaulted to Web Channel only. See the Segments section below to learn more on Segmentation with Native App.

With the Landing URL, if you are sending to all three Delivery Channels, then you have the option to use a different URL for the Native App sends, if needed.

<figure><img src="/files/JJPqXw482EiHk2sB2Oto" alt=""><figcaption></figcaption></figure>

The Title, Body, and Image will all be the same across all Delivery Channels. The character limits for each device will vary, so keep this in mind when creating your content. Like web-push Notifications, we suggest front loading your Title as much as possible. Body copy allows for more characters than Title.

## Native App Segmentation

All existing Segments will automatically have the Web Delivery Channel enabled. To edit an existing Segment to allow for Native iOS and Native Android, the Segment will need to be rebuilt. If you enable/disable a Delivery Channel that has filters saved, you’ll receive an alert about the Segment getting reset and will require you to build out your filters again.

To create a Segment specifically for Native iOS and/or Native Android, enable those toggles and go from there. The filters build out will be the same process as Web.

<figure><img src="/files/hqSiN59Zg5RmganDBq0o" alt=""><figcaption></figcaption></figure>

#### Multi-Domain with Native Channels

When using the Multi-Domain dashboard, it will function the same as the primary dashboard with the Native App Channels included. If you have questions, please reach out to your Account Manager.


# Notification Replacement Behavior

## Notification Replacement Behavior

The **Notification Replacement Behavior** setting gives you control over how notifications behave on a subscriber's device. You can choose whether new notifications should replace previous ones or stack alongside them.

This setting is available under the **Behavior** section when creating or updating manual notifications as well as when creating notifications within journeys and is configurable per notification.

Each notification can be configured with one of three behaviors:

* **Stack:** Each notification appears independently in the subscriber's notification tray. Notifications accumulate and remain visible until the subscriber dismisses them individually.
* **Replace All:** Each new notification replaces all previous notifications in the subscriber's tray. Subscribers always see only the most recent notification.
* **Replacement Group:** Notifications are grouped by a custom label you define. A new notification replaces only the previous notification sharing the same group — allowing different content categories to replace each other independently.

### Setting Notification Replacement Behavior

1. From the Notifications list view, click on **Schedule Notification**.
2. Scroll down to the **Behavior** section of the form.
3. Under **Display / Replacement Behavior**, choose one of the following:
   * **Stack:** notifications accumulate in the tray
   * **Replace All**: each new notification replaces all previous ones
   * **Replacement Group**: enter a custom group label to control which notifications replace each other
4. Send the notification.

If no behavior is explicitly set, notifications default to **Stack**.

### Viewing Replacement Behavior

#### Notification Details

After a notification is sent, you can see its resolved Replacement Behavior on the notification details page under the **Delivery** section. The field displays one of:

* **Stack**
* **Replace All**
* **Replacement Group: \<your group label>**

#### A/B Test Details

When viewing an A/B test, each variant's notification preview card shows its own Replacement Behavior value. This makes it easy to compare behavior across variants — particularly useful when testing different replacement strategies against each other.

#### Insights Reporting

Replacement Behavior is available as a dimension column and filter in notification-level Insights reports.

**Adding the Column**

1. Navigate to **Insights** and select a **Notification Report**.
2. In the column selector, find **Replacement Behavior** under the Dimensions group.
3. Select it to add the column to your report.

The column displays the resolved behavior for each notification row (Stack, Replace All, or Replacement Group: \<key>).

**Filtering by Replacement Behavior**

1. In the report filter bar, click **Filter** and select **Replacement Behavior**.
2. Choose one or more values: **Stack**, **Replace All**, or **Replacement Group**.
3. The report will show only notifications matching the selected behavior(s).

When filtering on **Replacement Group**, all notifications using any replacement group key are included — you don't need to specify individual group labels.

### Example Use Cases

| Scenario                                     | Recommended Setting                                                                                      |
| -------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| Weather forecast updates                     | **Replace All**: subscribers always see the latest forecast                                              |
| Breaking news alerts                         | **Replace All**: subscribers always see the most recent alert                                            |
| Multi-category feed (sports, politics, tech) | **Replacement Group**: use separate groups per category so each replaces only its own prior notification |
| Pollen or air quality alerts                 | **Replace All**: one active alert at a time                                                              |
| Product launch announcements                 | **Stack**: all announcements remain visible                                                              |

**Example:** A weather publisher sends forecast updates throughout the day. By selecting **Replace All**, each new update automatically replaces the previous one in subscribers' notification trays, ensuring they always see the latest forecast rather than a list of outdated ones.

**Example:** A publisher running an A/B test wants to compare Stack vs Replace All performance. After the test completes, they can view each variant's Replacement Behavior on the test details page and use the Insights filter to isolate performance metrics by behavior type.


# Landing URL Validation

### Landing URL Validation

The **Landing URL Validation** setting adds an automated health check to the notification save flow. When a notification is saved, Pushly verifies that the Landing URL returns a healthy response before delivery — helping you catch broken or misconfigured URLs before they reach your subscribers.

The check is **advisory, not a hard gate**. If a problem is detected, you are warned before sending but can always choose to proceed.

#### How It Works

When a notification is saved or scheduled, Pushly performs a server-side request against the Landing URL and checks the HTTP response.

If the URL returns a non-200 response, a warning step is added to the **Delivery Confirmations** modal before the notification is sent. The warning includes:

* The specific HTTP status code returned (e.g., **404**)
* A clickable link to the Landing URL so you can verify it directly
* The final destination URL, if the original URL redirected

You can acknowledge the warning and proceed with sending — the check is non-blocking and advisory only.

<figure><img src="/files/JBnEpR2quGYj3fhN8Gl7" alt=""><figcaption></figcaption></figure>

#### Validation Scenarios

The following conditions will trigger a warning:

* **Non-200 HTTP responses** — for example, 404 Not Found or 500 Server Error
* **Unreachable URLs** — DNS failures, connection timeouts, or SSL errors
* **Malformed protocols** — for example, `http:/example.com` instead of `https://example.com`

If a URL uses a short link or redirect, the redirect chain is followed to its final destination, and that destination is validated.

#### Enabling Landing URL Validation

Landing URL Validation is **disabled by default** for all domains. It can be enabled per-domain.

1. Navigate to **Domain Settings → Notifications → General Settings**.
2. Click **Edit** and open the **Delivery** section.
3. Toggle the **Landing URL Validation** switch on.
4. Save your changes.

The current status (**Enabled** or **Disabled**) is shown in the Delivery section of the General Settings well.

#### Benefits

* Catch broken Landing URLs before notifications reach subscribers
* Receive clear, actionable warnings with direct links to verify the URL
* Stay in control — the check is non-blocking, so you decide whether to proceed
* Opt out per domain in cases where the check produces false positives
* Validate short links and redirects correctly, since redirect chains are followed to their final destination


# App Messages

App messages are units displayed inside of your native applications.  These units are great for many use cases including:

* Special Offers
* Announcements
* Notifications Pre-Permission Prompt

## When to use App Messages

Use App Messages when you want to communicate with users while they are active in your app, including:

* Promotions and limited-time offers
* Product announcements and feature callouts
* Contextual reminders or nudges
* Education and onboarding moments

## Formats

App Message units are highly customizable and can be displayed in several different formats depending on the goal of the message.&#x20;

### Banner

The Banner unit is a smaller unit that can be displayed at the top or bottom of the device's screen and includes body text and a left- or right-aligned  image.

<div align="left"><figure><img src="/files/YOsCxEssH0X3XfONRCB5" alt=""><figcaption></figcaption></figure></div>

### Modal

The Modal unit displays in middle of the device and includes a large image, header, title, and body text. Up to two buttons can be added to this unit.

<div align="left"><figure><img src="/files/v3okKVy8o435ys7Czuno" alt=""><figcaption></figcaption></figure></div>

### Full Screen

The Full Screen unit covers the device's screen and includes a large image, header, title, and body text. Up to two buttons can be added to this unit.

<div align="left"><figure><img src="/files/FjGvc4F4bShRjT1VPa6H" alt=""><figcaption></figcaption></figure></div>

## Build your message

While you build, focus on three things: layout, content, and actions.

#### 1) Layout and appearance

Depending on the selected format, you can configure common visual elements such as:

* Background color
* Image on/off
* Image fit behavior (for example: Cover vs Contain)
* Image position (for example: Top vs Bottom, where supported)

Guidance:

* Use **Contain** when the full image must always be visible.
* Use **Cover** when edge cropping is acceptable and you want a more immersive look.

#### 2) Content

Most formats support a combination of:

* Header (optional)
* Title (recommended)
* Body (recommended)

Guidance:

* Keep titles short so they do not wrap awkwardly on smaller phones.
* Avoid long body text for Banners.
* If the message is instructional, split the content between title and body for scannability.

#### 3) Buttons and calls to action

Modals and Full Screen messages support up to **two buttons**.

Recommendations:

* Use one primary CTA when possible.
* Keep CTA text concise (2 to 4 words is a good target).
* If you include a second button, make it a clear alternative (for example, “Not now” or “Learn more”).

<figure><img src="/files/8jSWEWiuRFgXADtPW882" alt=""><figcaption></figcaption></figure>

### Previews

Previews are integrated throughout the builder so you can validate how your message will look before publishing. Use them to confirm spacing, text length, image behavior, and button visibility across different screen sizes and UI modes.

#### Inline preview panel

The builder includes a preview panel that updates as you edit content and appearance. Use this to iterate quickly on layout and copy.

#### App Message Previews window

For deeper validation, open the **App Message Previews** window. This window lets you preview the same message across multiple device contexts, including:

* **Device type:** Phone or Tablet
* **Platform:** Apple / iOS or Android
* **Orientation:** Portrait or Landscape
* **Theme:** Light or Dark

<figure><img src="/files/MnoWwThuQtd1IFJC1ohr" alt=""><figcaption></figcaption></figure>


# Segments

Segments allow you to group subscribers into separate audiences and target them with relevant notifications.

## Segment Creation & Editing

Each segment has a name and a set of conditions that define the audience to be targeted.

Once you select Create Segment, you'll first choose a name for your segment. This should be something descriptive of the audience.

{% hint style="success" %}
If you're going to be creating several similar segments, consider using a nomenclature that makes them easy to categorize and search. For example, if you are creating many segments that define a user interest perhaps prefix the segments with "Interest: Movies" or "Sports: NFL".
{% endhint %}

Next, if you'd like your segment to override the default icon when it is targeted you may enable the "Override Default icon" option and upload an image. This is optional.

![](/files/-Lrz6lU9oCj6mRim6SQx)

Next, move on to choosing the attributes that will define the audience.

Each new row you add under the "Any of the following are true" (green star) will be OR'd together. I.e., if a subscriber matches ANY of the lines then they will be included in this audience.

Each new row you add under the "All of the following are true" (purple star) will be AND'd together. I.e., a subscriber MUST match at least one of the OR conditions in ALL of the AND groupings.

![](/files/PJPziiWxp0RovbLONway)

## Attribute Definitions

We supports several segmentation attributes out of the box and can add custom attributes using your own 1st and 3rd party data. Contact your account manager for more information on custom attributes.

| Category | Attribute                | Definition                                                                                                                                                                                        |
| -------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Behavior | Allow Referrer           | The URL of the page the subscriber visited before allowing permissions                                                                                                                            |
| Behavior | Allow Age                | The number of days since the subscriber allowed permissions                                                                                                                                       |
| Behavior | Allow Date               | The date the subscriber allowed permissions                                                                                                                                                       |
| Behavior | Allow URL                | The URL of the page the on which the subscriber allowed permissions                                                                                                                               |
| Behavior | Allow Prompt             | The prompt the subscriber saw when opting in                                                                                                                                                      |
| Behavior | Campaign Status          | Target subscribers who are/aren't part of a platform campaign                                                                                                                                     |
| Behavior | Keyword                  | Subscribers who clicked on a notification tagged with the chosen keyword                                                                                                                          |
| Behavior | Keyword Clicked          | Set a time frame for when a user clicked on a a given Notification. For example, users who clicked on "politics" keyword once in the last 30 days                                                 |
| Behavior | Last On Site             | The date of the last time the subscriber was on the website                                                                                                                                       |
| Behavior | Notification Clicked     | Choose a notification to create an audience of subscribers who clicked that notification                                                                                                          |
| Behavior | Notification Click Count | Target subscribers based on how many times they have clicked in a given time frame                                                                                                                |
| Behavior | Page URL                 | Target subscribers based on which URLs they visit when they are on your site                                                                                                                      |
| Behavior | Page Tag                 | Target subscribers based on the meta tags of the article they visit when on your site                                                                                                             |
| Device   | Browser                  | The browser the subscriber was on when they allowed permissions                                                                                                                                   |
| Device   | Subscriber ID / Push ID  | The device's Push ID copied via the [Web / Browser push Instructions](/faq/web-browser-push/retrieve-push-user-id) or [Native App Push instructions](/faq/native-app-push/retrieve-push-user-id). |
| Device   | External ID              | The external ID is only available if you choose to pass this data to Pushly                                                                                                                       |
| Device   | Type                     | The type of device the subscriber was on when they allowed permissions (Desktop, Mobile, or Tablet)                                                                                               |
| Device   | Operating System         | The operating system the subscriber was on when they allowed permissions                                                                                                                          |
| Location | City                     | The city the subscriber was in the last time they visited your website                                                                                                                            |
| Location | Continent                | The continent the subscriber was on the last time they visited your website                                                                                                                       |
| Location | Country                  | The country the subscriber was in the last time they visited your website                                                                                                                         |
| Location | Postal Code              | The postal code the subscriber was in the last time they visited your website                                                                                                                     |
| Location | State / Province         | The state/province/region the subscriber was in the last time they visited your website                                                                                                           |
| Location | Custom Selection         | Use the custom geo tool to select a geographic area which you would like to target. For a walk through of how to use the tool, please click here                                                  |


# Custom Geo Segmentation

Using our Custom Geo Segment tool, you can draw exact geographic regions for targeting. This is a walk through of how to use the tool.

Here is a video which shows the creation of a Custom Geo Segment. You can also follow the step by step instructions below the video if you prefer.&#x20;

{% embed url="<https://vimeo.com/386578008>" %}

After choosing to create a new Segment, click on the attribute list and scroll to "Custom Selection"&#x20;

![](/files/-Lz90sBXHzMO3OdYu1uI)

Choose "Edit in Map"&#x20;

![](/files/-Lz90yOsOE8pBj7db4zu)

You will see a map of the world, which you can navigate by hovering within the map and scrolling on your mouse, using the controls in the top left, or clicking and dragging

![](/files/-Lz90yOr06Mb_ukwCn_F)

Adjust the map until you have a good view of the region you want to select, in this example we are looking to target a section of the middle of the United States

![](/files/-Lz90yOpr50SaEJC588G)

Click on "Create Polygon" at the bottom of the map. Then, click on the point of the map at which you want to begin drawing your selection. Below, you can see a small red dot below Omaha (top left) which indicates a click and the beginning of your selection

![](/files/-Lz90yOoFegc5buPgk2v)

Move the mouse to your second point and click to set another anchor spot. Then, move your mouse to additional anchor points as appropriate, clicking when you are ready. Once you are satisfied with your selection, click your starting point again to close the shape

![](/files/-Lz90yOnz9avQrG-HiTd)

Your mouse will change to a hand, as seen below, confirming the shape is complete

![](/files/-Lz90yOmFr10YnuOsP1R)

If you click "Edit Polygon" you can move the shape around to a different area. You can also choose "Clear Polygon" to remove the shape and start over

![](/files/-Lz90yOlLgQkJqf4fzml)

When you are satisfied with your selection, click "Save Polygon"

![](/files/-Lz90yOvdOtL8KHZp32h)

Click "Save" to set your Segment filter

![](/files/-Lz90yOtzrDBkzsJc4W7)

Name your Segment

![](/files/-Lz90yOuZQltSHj9e0Wy)

Save your Segment and target it as you would any other Segment when sending Notifications

![](/files/-Lz94BKvRtjzgrYaA69j)


# Journeys

Allows users to set up automated, multi-step notification flows that deliver a series of notifications to subscribers based on configured triggers, schedules, and audience criteria

To access Journeys, click **Journeys** in the side panel. The Journeys list displays all journeys in a paginated, modern table view with sticky filters, targeted segment information, and inline performance metrics.

![](/files/OWwrzVkMlmwpUyBTYjsS)

#### Journey Types

There are two journey types from which you can choose:

* **Triggered** — Enrolls subscribers automatically when a specific event occurs (e.g., abandoned cart, viewed item). Ideal for e-commerce publishers and welcome flows.
* **Scheduled** — Enrolls subscribers on a recurring schedule and sends notifications at your chosen cadence. Useful for weekly roundups, recurring content series, or periodic engagement flows.

#### Creating a Journey

To create a new journey, click the **Create Journey** button from the Journeys list page.

**Step 1: Configure Entry Settings**

<figure><img src="/files/t54KH9nc9SusvXpcd1gt" alt=""><figcaption></figcaption></figure>

Enter the following information:

* **Name** — A descriptive name for the journey.
* **Start Date** — The date the journey begins enrolling subscribers.
* **End Date** (optional) — The date the journey stops enrolling new subscribers.
* **Trigger** — For triggered journeys, select the event that enrolls subscribers (e.g., cart abandonment). Configure the delay period before enrollment begins.
* **Schedule** — For scheduled journeys, configure the frequency at which the journey runs:
  * **Schedule Type** — Daily, Weekly, or Monthly.
  * **Day/Time** — The specific day(s) and time the journey enrolls subscribers. Time can be set in the subscriber's timezone.
  * **Next 7 Occurrences** — A preview of upcoming enrollment dates is displayed on the right side.
* **Subscriber Entry Cap** — Limits how often a subscriber can be enrolled in this journey.

**Step 2: Configure Audience**

<figure><img src="/files/wIhhKNRhkE1boo99Agpr" alt=""><figcaption></figcaption></figure>

Choose which subscribers are eligible for enrollment:

* **All Subscribers** — All subscribers are eligible when the trigger fires or schedule runs.
* **Specific Segments** — Only subscribers matching one or more segments are eligible.
* **Exclude Segments** — Remove specific segments from the eligible audience.

**Step 3: Configure Quiet Hours (Optional)**

<figure><img src="/files/Pe5hIBdJ55FCIO6Eyids" alt=""><figcaption></figcaption></figure>

Enable **Quiet Hours** to restrict notifications from being delivered during a specified time window (e.g., 10:00 PM to 6:00 AM). Notifications scheduled during quiet hours will be held and delivered once the window ends.

**Step 4: Build the Journey Flow**

After configuring the entry settings, click **Save & Continue** to open the **Journey Builder**. The builder provides a visual canvas where you add steps to define the subscriber's journey. See the Journey Builder page for full details on building flows with notifications, delays, and split tests.

#### Unsaved Changes Protection

The journey builder detects unsaved changes and provides two safeguards:

* **Navigation Warning** — If you try to navigate away, close the tab, or reload the page while there are unsaved changes, a browser confirmation dialog appears.
* **Dynamic Save Button** — The **Save** button label updates to **Save Changes** as soon as any modification is detected.
* **Reminder Toast** — A toast notification displays after updating a step, reminding you that there are unsaved changes in the journey.

#### Triggered Journeys

<figure><img src="/files/eJgGjVKB0eZKV92l9RFL" alt=""><figcaption></figcaption></figure>

Triggered journeys enroll subscribers automatically when a specific event occurs. Common trigger types include:

* **Cart Abandonment** — Enrolls subscribers when they abandon a shopping cart. Requires the completion of E-Commerce Support for dynamic product copy. Configure the abandonment delay to determine how long to wait before considering the cart abandoned.
* **Welcome** — Enrolls subscribers at the time of notification subscription.
* **Catalog Item Triggers** — Enrolls subscribers based on interactions with catalog items (viewed items, trending items, etc.).

> ℹ️ The subscriber entry cap field limits how often subscribers are eligible for re-enrollment. For abandon cart journeys, limiting entry is not advisable as it will restrict sales opportunities.

#### Scheduled Journeys

Scheduled journeys run on a recurring cadence and enroll all eligible subscribers at each occurrence. Configure the schedule type (Daily, Weekly, Monthly), the specific day(s) and time, and the subscriber entry frequency.

For example, a weekly "Friday Event Guide" journey could be configured to run every Friday at 4:00 PM in the subscriber's timezone, enrolling all subscribers in the "Lifestyle Clickers" segment.

#### Journey Statuses

Journeys can be in the following states:

* **Draft** — The journey is being configured and has not been activated.
* **Active** — The journey is live and enrolling subscribers.
* **Paused** — The journey has been temporarily stopped. Subscribers already in the flow will continue through their remaining steps, but no new subscribers will be enrolled.
* **Completed** — The journey has reached its end date or been manually completed.

##


# Journey Builder

## Journey Builder

The Journey Builder is a visual canvas where you design the flow of steps subscribers will move through after enrollment. The builder uses an automatic layout engine that positions nodes and routes connections cleanly, keeping flows readable even with complex branching paths.

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2FPKWC7ukUmyclFsQh8RiW%2FScreenshot%202026-04-02%20at%201.06.34%E2%80%AFPM.png?alt=media&#x26;token=11db9d7d-e0cd-4181-aa55-4cd2775b4fc3" alt=""><figcaption></figcaption></figure>

**Canvas Overview**

Every journey starts with an **Entry** node (representing the trigger or schedule configuration) and ends with an **Exit** node. Steps are added between these nodes to define the subscriber's path.

**Placeholder Steps**

New journeys start with a visible **placeholder step** between the entry trigger and exit node. The placeholder makes it immediately clear where to add your first step.

Click the placeholder or use the **+** button on adjacent edges to replace it with a real step. Once replaced, the placeholder is removed from the flow automatically.

**Adding Steps**

<figure><img src="https://810756845-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F-Lry9Z27iWOZyQEGAgY7%2Fuploads%2F0sLxufPvB048nVgGvFDi%2FScreenshot%202026-04-02%20at%2010.08.56%E2%80%AFAM.png?alt=media&#x26;token=58d0ade6-e89e-49d0-bd3e-d5e5335f6e39" alt=""><figcaption></figcaption></figure>

To add a step to the journey, click the **+** button on any edge between nodes, or click a placeholder step. The **Add Step Panel** appears as a floating panel with step descriptions organized by category.

**A/B/n Split Testing**

Journeys support **variant split steps** for A/B and multivariate testing directly within the flow. Add a **Split Test** step from the **+** button on any edge to divide subscribers into 2–4 named variant paths — each allocated a configurable percentage of traffic (totaling 100%) — that automatically converge at a downstream join point, so the journey continues as a unified flow after the test.

For the full split-test workflow — configuring variants, reading variant performance, promoting a winner, and viewing revision history — see [Split Testing & Promotion](/platform/journeys/split-testing-and-promotion).


# Sending Recommended Notifications

Provides a list of the recommendation integrations available in the Pushly platform

Recommended Notifications are personalized push notifications that are automatically generated and sent to users based on their individual interests and past behavior. They leverage algorithms and user data to deliver relevant content recommendations, such as articles or products, without manual intervention.

## Recommendation Integrations

### BlueConic

To setup new recommended notifications using the BlueConic integration, please follow the steps listed here - [Configuring your BlueConic Recommended Notification](/platform/platform-integrations/blueconic/using-blueconic-in-campaigns)


# Split Testing & Promotion

## Split Testing & Promotion

A **split test** lets you compare variations of a journey path against each other on live traffic, then **promote** the variant that performs best so every new subscriber follows it. A split divides subscribers into 2–4 named variant paths by percentage; each path is its own sequence of notifications and waits, and all paths converge downstream so the journey continues as one flow afterward.

### Setting up a split test

Add a **Split Test** step from the **+** on any edge, name each variant, and set each variant's percentage (all percentages must total 100%). You can have up to 4 variants; adding one redistributes the percentages evenly. Build each variant's path independently — click the **+** on a variant's edge to add waits, notifications, and other steps to that branch. All branches automatically converge at a shared join point downstream.

For the basics of adding and configuring steps, see [Journey Builder](/platform/journeys/journey-builder).

<figure><img src="/files/pd0XICpYytYChEPlyE26" alt=""><figcaption></figcaption></figure>

### Reading variant performance

Once a journey with a split is running, the builder shows performance for each variant in two places:

* **On-canvas performance cards.** Each variant path shows a compact card at the fork with metrics for the **whole path** (every step in that branch, not just one send): **entrances, impressions, clicks, CTR**, and — when the journey records purchases — **purchases and purchase rate**.
* **The Split Test Results drawer.** Click a variant's performance card to open the drawer: every variant appears as a row with the full metric set, side by side. This is also where you promote a winner.

A few states to expect:

* Until a variant has enough data, its rates show as **“—”** and no leader is suggested.
* A **draft** journey shows no metrics — data appears once the journey is running.
* Journeys that don't track purchases simply omit the purchase columns.

### Promoting a variant

When you've decided, click **Promote** on a variant's row in the Split Test Results drawer. A confirmation dialog explains exactly what will happen. Promotion:

* **Routes all new subscribers** down the promoted variant's path. The split is replaced by that single path going forward.
* **Lets in-flight subscribers finish.** Anyone already in the journey — including on a path you didn't promote — completes their current path uninterrupted. Promotion never moves someone mid-journey.
* **Keeps the losing variants in history.** They're removed from the live journey but preserved in the journey's revision history, where their results remain viewable.

<figure><img src="/files/eTbbS092H87nCKabcPmV" alt=""><figcaption></figcaption></figure>

Keep in mind:

* **Promotion is irreversible.** It creates a new version of the journey; you can't “un-promote” back to the split (you would rebuild the test).
* **Save first.** Promotion acts on the saved journey, so unsaved builder edits block it — save your changes before promoting.

### Viewing past revisions

Promoting (and other structural changes) creates a new **revision** of the journey. Use the **revision selector** in the Journey Builder header to view any past revision **read-only**: the canvas shows that earlier version (for example, the original split), with a banner indicating you're viewing history and a **Back to current revision** button to return. On a promoted journey, the past revision marks the promoted variant with a **🏆**.

<figure><img src="/files/dh9sSdyPIibi67xxnNNt" alt=""><figcaption></figcaption></figure>

### Revision-aware Insights

On the journey's **Insights** tab, a **revision scope** control lets you view reporting for **all revisions** (grouped into collapsible per-revision sections, each with its own subtotal) or scope to a **single revision**. This keeps before/after metrics comparable — for example, the split test's variant results stay attached to the revision that ran the test, separate from the collapsed journey's later performance. The **Split Test Variants** report defaults to the revision that held the split. On journeys with only one revision, no scope control appears.

<figure><img src="/files/9Dk4SjgY13va9waAMjHK" alt=""><figcaption></figcaption></figure>


# Insights

This section of the platform gives users the ability to review, download, save, share and schedule performance data.

## Using Insights

Access the Insights section of the platform via the side panel. Once there, use the drop down menu to select which type of report you would like to run. There are two options available:

* **Delivery Report** - Reporting associated with Notification deployment and performance
* **Acquisition Report** - Reporting associated with the opt-in flow and subscription rates

![](/files/DIIxMz6MgjasnwilAL8m)

### Level

Select the desired level of reporting for your data to choose the primary thing you'll be reporting on:

**Delivery Reports**

* **Domain** - Group all performance data at the Domain level
* **Notification** - Breakout all performance data to the associated Notification
* **A/B Test** - View performance of A/B Tests and associated Notifications
* **Campaign** - Grouping of performance by executed Campaigns
* **Segment** - View performance of all Segments that have ever been targeted

**Acquisition Reports**

* **Domain** - Group all performance data at the Domain level
* **Prompt** - Group data by individual opt-in flows utilized
* **A/B Test** - Break out data to show performance of flows within A/B Tests

![](/files/hJfL9bOZQl2In5JRqRlo)

### Breakdowns

Select the desired breakdown for your data which will further segment the results in to buckets:

* **None** - All data will be associated at the selected "level"
* **Button** - Group by the buttons used in your Notifications (Open URL, Dismiss Notification)
* **Browser** - Group data by the browser on which the subscriber received the Notification (Chrome, Firefox, etc.)
* **Device Type** - Group data based on which device the subscriber received the Notification (Desktop, Mobile, Tablet)
* **OS (Operating System)** - Group data based on the Operating system on which the subscriber received the Notification (Windows, Mac, etc.)

![](/files/biepF7taT1oJffaufkgc)

### Columns

Select the columns which you would like included in your data. Relevant Column definitions can be found below at the end of this page.

![](/files/fe7UUqseSSw3XwtSbDbf)

### Filters

Using filters, you can narrow your search based on a number of different factors. Click the green "+ Filter" button and choose the attribute on which you wish to filter:&#x20;

![](/files/uCCshyrelyjvoosv8tiC)

Once you've chosen the attribute you wish to filter on, select an operator ("contains", for example) and then enter/select your values:&#x20;

![](/files/EoH8WuOedOPXoxontoNk)

Once your values are entered,  click the green check mark. You can also add additional filters by clicking "+ Filter" again and following the same process.

### Result Options

In the top-right corner of the Insights section, you will see the four options below. From left to right, they are:

* **Save** - Keep this report for future use, it will appear on the drop down selector and under the 'Saved Reports' tab moving forward for only your user
* **Share** - Generate a URL to be shared with other team members. Clicking the link will take users to the report. A user must have access to the platform and Insights data to access shared reports
* **Download** - Download the data into a spreadsheet
* **Refresh** - Update the report with the most recent data available

![](/files/-Lrz8X1oU2f0O0q91pcr)

### Saved Reports

Next to the Reporting tab on the Insights page, you'll find a Saves Reports tab. This is where you'll find any reports you've saved. Once you've saved a report, you have the option to Share, Download or even Schedule the report.

![](/files/ufZzkJHtoqxWDTjUw4ju)

Any saved report can be Schedule to send on a daily/weekly/monthly basis to a number of email addresses. You can select the day of the week and time you want it to be delivered. Ensure that you select 'Submit' in order to save your scheduled report.

![](/files/s1Ig0CmPxbm82BT8XS3S)

## Definitions

### Notification Columns/Filters

| Term              | Description                                                                                                                                                                |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Audience Targeted | The segment(s) chosen to receive the Notification.                                                                                                                         |
| Audience Excluded | The segment(s) excluded from receiving the Notification.                                                                                                                   |
| Keywords          | Tag(s) used to identify the type of content sent.                                                                                                                          |
| Campaign Name     | If the Notification was part of a Campaign, the relevant name will be populated.                                                                                           |
| Landing URL       | The URL to which subscribers were sent upon engaging with the Notification                                                                                                 |
| Lifespan          | How long the notification will attempt to be delivered to the subscriber.                                                                                                  |
| Icon              | The URL of the 250x250 pixel image used on the Notification.                                                                                                               |
| Image             | The URL of the large format image used on the Notification.                                                                                                                |
| A/B Test Name     | If the Notification was part of an A/B Test, the name of the test will populate.                                                                                           |
| A/B Test Weight   | The percentage of audience assigned to the Notification as part of an applicable A/B Test.                                                                                 |
| Stage Name        | If the Notification was in a Campaign, the assigned Stage will populate.                                                                                                   |
| Notification Type | "Manual" means the Notification was sent by a team member creating and scheduling it.  "Campaign" means the Notification was part of an automated series of Notifications. |

### Notification Metrics

| Delivery                 | The subscriber’s browser indicated it received the Notification and the subscriber is still valid. |
| ------------------------ | -------------------------------------------------------------------------------------------------- |
| Impression               | The subscriber became available and the browser was able to show the subscriber the Notification.  |
| Clicks                   | The subscriber engaged with the Notification and was taken to the destination URL.                 |
| CTR (Click Through Rate) | The number of clicks divided by the number of impressions.                                         |
| Delivery Rate            | The number of impressions divided by the number of deliveries.                                     |

### Prompt Columns

| Term               | Description                                                               |
| ------------------ | ------------------------------------------------------------------------- |
| Name               | The name of the prompt set when it was created.                           |
| Priority           | The priority of the prompt to show to visitors compared to other prompts. |
| Style              | The look and feel chosen for the prompt (e.g., Native, Slide, Bell).      |
| Visitor Conditions | The conditions chosen to determine when the visitor should be prompted.   |

### Prompt Metrics

| Term                         | Description                                                                                                                                                                           |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Permissions Shown            | Visitor was shown the option to adjust the Notification settings for the domain they are currently visiting.                                                                          |
| Permissions Dismissed        | Visitor clicked the "x" to close the native permission, did not select "Allow" or "Block"                                                                                             |
| Permissions Denied           | Visitor clicked "Block" or other negative option on the Notification settings unit.                                                                                                   |
| Subscriptions                | The rate at which visitors converted to Notification subscribers.                                                                                                                     |
| Subscription Rate (Decimal)  | If utilizing a two-step opt-in flow, the initial opt-in unit was shown to the visitor.                                                                                                |
| Prompts Shown                | If utilizing a two-step opt-in flow, the initial opt-in unit was shown to the visitor.                                                                                                |
| Prompts Dismissed            | If utilizing a two-step opt-in flow, the visitor closed the initial opt-in unit.                                                                                                      |
| Prompts Accepted             | If utilizing a two-step opt-in flow, the visitor responded in the affirmative to the initial opt-in unit, causing the Permission to show (Notification settings unit).                |
| Prompt Accept Rate (Decimal) | If utilizing a two-step opt-in flow, the rate at which visitors responded in the affirmative to the initial opt-in unit, causing the Permission to show (Notification settings unit). |


# User Management

Manage user access and permissions to your domains

Users in the platform can have access to all of their organization's domains or specific properties, this includes the ability to assign different user access levels per domain.

## User List

Access your user list by navigating to the Settings page in the left panel, then selecting the Users tab

![](/files/uSPPCQbuxxga3qnLuTLy)

On this page you can view all current users, their permissions (access level) and their status (Active, Disabled or Invited).

![](/files/F2M2scccB621rcbDMZQa)

The following actions are available for each user:

* Edit: Navigates to the user allowing you to update their details and access
* Reset Password: Send a password reset email to the user
* Disable: Remove platform access for this user

{% hint style="warning" %}
If a user is disabled they will lose access to all of their domains. If you just want to remove individual domain access choose Edit and remove the domains from there.
{% endhint %}

## Access Levels

Users of the platform can be assigned one of three access levels based on their role and usage requirements. Those the three options, which are:

| Level     | Access Limitations                                                                                        |
| --------- | --------------------------------------------------------------------------------------------------------- |
| User      | Ability to deploy Notifications and edit opt-in flow, cannot create new users.                            |
| Admin     | Ability to deploy Notifications, edit opt-in flow, and create new users with domain access assignment.    |
| Read Only | Download reporting from Insights section of Platform, no opt-in flow or Notification creating or editing. |

## Creating a User

If a user needs access to the platform but has not yet been created, an Admin level user or Account Manager can grant the user needed access. Click the Add User button in the top right of the page, you will then see this window, where you can give the user the needed level of access

![](/files/G7hDUj4uayLhJNFyNMqE)

Follow the steps below to create a user:

1. Input the user's email address to be used for access
2. Select the necessary access level both at the Domain level&#x20;
3. Click submit and the user will receive an invitation via email

If you need to edit the access level of a current user, you can click the Edit button next to their name and make needed changes&#x20;

{% hint style="info" %}
You can assign different user access levels per domain by adjusting the access drop down before adding the relevant domain.
{% endhint %}


# Organizations

## Overview

All domains within the platform are part of an Organization. If you, as a user, have access to multiple domains within the platform and an appropriate level of access set by your organization admin you have access to additional features within the platform.&#x20;

## Accessing Organization Context

If you have access to your organization the organization name in the domain list will be clickable. Once clicking the organization name you will be switched to the organization context within the platform.

For example, in the screenshot below, clicking the `Pushly Marketing` gray section within the domain list will swap you to the organization context.

![](/files/UPQZL0w1IYDbGjb69aeA)

The individual features available while in the organization context are:

{% content-ref url="/pages/-MgHWrbrYB4nLCHLZlfv" %}
[Multi-Domain Notifications](/platform/organizations/multi-domain-notifications)
{% endcontent-ref %}

{% content-ref url="/pages/-MgHWupsmnKn340FGIBf" %}
[Multi-Domain Segments](/platform/organizations/multi-domain-segments)
{% endcontent-ref %}


# Multi-Factor Authentication

## Default MFA Settings

All Organizations have Multi-Factor Authentication enabled by default. The default settings allow platform users to remember their browser for up to 30 days and uses an **Adaptive MFA** strategy (see below for details).

## Modify MFA Settings

{% hint style="warning" %}
After modifying MFA settings it make take up to 1 hour before the changes take effect.
{% endhint %}

To view or modify the default MFA settings you can log into the platform and [navigate to your Organization Settings](/platform/organizations#accessing-organization-context) page.

Next, click the `Security` tab, and then note the current settings for **Remember Browser** and **MFA Mode** within the **Multi-Factor Authentication (MFA)** section.

<figure><img src="/files/hYQyMEQzGbTbr7750xsM" alt=""><figcaption></figcaption></figure>

From here you can click the **Edit** button to make changes to these settings.

**Remember Browser**

When enabled, users will be allowed to check a box to skip MFA for 30 days for their current browser.&#x20;

**MFA Mode**

* **Adaptive**: Users will be prompted to complete MFA when using a new device, impossible travel distance is detected, or when they are using a suspicious IP address.
* **Mandatory**: If Remember Browser is enabled, users will be prompted to complete MFA once every 30 days or after 7 days without at least one successful login. If Remember Browser is disabled, users will be prompted for MFA every time they login.

{% hint style="info" %}
If you want your users to complete the MFA process after every successful login you should turn off **Remember Browser** and set **MFA Mode** to **Mandatory.**
{% endhint %}


# Single Sign On

Pushly supports Single Sign On using your company's identity provider. Choose your identity provider below for instructions on how to set up SSO for your Pushly domains.

{% content-ref url="/pages/oHu4O8s4pFe2coZW5X1E" %}
[Okta Workforce](/platform/organizations/single-sign-on/okta-workforce)
{% endcontent-ref %}

{% content-ref url="/pages/3k0gxpdJCnvPYW00cz1Y" %}
[Google Workspace](/platform/organizations/single-sign-on/google-workspace)
{% endcontent-ref %}


# Google Workspace

Instructions for setting up Google Workspace as your Identity Provider

## In Google Workspace: Create OAuth 2.0 Client Credentails

An administrator for your company's Google Workspace needs to log in to the [API Console](https://console.developers.google.com/) and follow the steps below to create new OAuth 2.0 Client Credentials.

From the projects list, select an existing project or create a new one.

If the `APIs & Services` page isn't already open, open the console left side menu and select APIs & services.

On the left, click `Credentials`.

<figure><img src="/files/8Mn1KHMAr2QpEeUMdsGi" alt=""><figcaption></figcaption></figure>

Click `Create Credentials`, then select `OAuth client ID`

Choose `Web Application` for `Application type` and name it "Pushly"

Under `Authorized JavaScript origins` enter the following value:&#x20;

```
https://identity.pushly.com
```

Under `Authorized redirect URIs` enter the following value:

```
https://identity.pushly.com/login/callback
```

Click the `Create` button. A modal will show on the next page that includes the `Client ID` and `Client Secret`. Copy down both of these values to be used in the next step.

## Pushly: Create the Google Workspace Identity Provider

Now that you have your `Client ID`, and `Client Secret` you can log into the platform and [navigate to your Organization Settings](/platform/organizations#accessing-organization-context) page.

Next, click the `Security` tab, and click the `Add Provider` button.

Choose `Google Workspace` from the `Provider Type` dropdown.

Provide the `Google Domain` which is the Google Workspace domain name for your organization.

Fill in the `Client ID` and `Client Secret` that was generated in the first step of this documentation.

Within the `Associated Domains` text area enter each domain name that your organization will be using to to log in via Google Workspace on a separate line. For example, if your email address is `example@yourdomain.com` then you would enter `yourdomain.com`

<figure><img src="/files/sjOZH5x4jh6NHmbdn4C3" alt=""><figcaption></figcaption></figure>

Once the provider has been successfully created you can begin [inviting users](/platform/user-management#creating-a-user) to the platform. Any user invited that has an email address that matches one of the emails provided in `Email Domains` will be sent through Google Workspace for authentication.




---

[Next Page](/llms-full.txt/1)

