# Rich Notifications

Send notifications with modern Expo message fields from the native-notify SDK — subtitle, badge, ttl, interruption level, iOS category, Android channel, collapse id, sound, and more.

Native Notify's send helpers pass the modern **Expo message fields** through to every send path — mass, single subscriber, group, and followers. The same fields are accepted directly by the REST endpoints, and the dashboard and [MCP server](/docs/mcp-server) send them too.

These helpers ship in the next `native-notify` release — install `native-notify@latest` to get them (see [Upgrading to v5](/docs/upgrading-to-v5)).

> **Sends are live:**
>
> Every helper sends through your Native Notify account the moment it is called — there is no test mode, and a send cannot be recalled. Unset rich fields keep the server's defaults.

## Send helpers

| Helper                                                              | Sends to                                                                       |
| ------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `sendMassNotification(title, body, options)`                        | Every registered device of the app.                                            |
| `sendIndieNotification(subID, title, message, options)`             | One individual-push [subscriber](/docs/indie-push-notifications/registration). |
| `sendIndieGroupNotification(subIDs, title, message, options)`       | A list of subscriber ids you already hold.                                     |
| `sendNotificationToFollowers(masterSubID, title, message, options)` | Every follower of one follow-master subID.                                     |

All four take the same options object, resolve with the server's response, and throw when the server rejects the send. `sendIndieGroupNotification` also throws when `subIDs` is not a non-empty array — an empty audience would otherwise be a silent no-op.

## Quick start

```ts
import { sendMassNotification, sendIndieNotification } from 'native-notify';

// Every registered device (mass):
await sendMassNotification('Service update', 'We are back online.', {
  pushData: { url: '/status' },
  subtitle: 'All systems normal',
  channelId: 'alerts',          // Android channel — create it first (below)
  interruptionLevel: 'active',  // iOS
  sound: 'chime.wav',
});

// One subscriber (indie):
await sendIndieNotification(userId, 'Your order', 'It shipped!', {
  pushData: { url: '/orders/' + orderId },
  badge: 1,
  categoryId: 'ORDER_UPDATE',
});

// A custom audience, or the followers of a follow-master:
await sendIndieGroupNotification(subIds, 'Group news', 'Hello all', { channelId: 'news' });
await sendNotificationToFollowers(masterSubId, 'New post', 'Read it', { mutableContent: true });
```

## Options

Every helper accepts the same options object:

| Option               | Type                                                     | What it does                                                                                 |
| -------------------- | -------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `pushData`           | `object`                                                 | The data payload your app reads on tap (deep links, ids).                                    |
| `bigPictureURL`      | `string`                                                 | Android big-picture image / iOS attachment source.                                           |
| `subtitle`           | `string` (max 200 chars)                                 | iOS subtitle shown under the title.                                                          |
| `badge`              | `number`                                                 | iOS app-icon badge count — `0` clears the badge.                                             |
| `ttl`                | `number`                                                 | Seconds the push may be delivered for.                                                       |
| `interruptionLevel`  | `'passive' \| 'active' \| 'timeSensitive' \| 'critical'` | iOS interruption level — use `timeSensitive` / `critical` sparingly.                         |
| `categoryId`         | `string`                                                 | iOS notification category (your action buttons).                                             |
| `channelId`          | `string`                                                 | Android channel id — create the channel first.                                               |
| `collapseId`         | `string`                                                 | APNs collapse id — a newer push with the same id replaces the older one.                     |
| `contentAvailable`   | `boolean`                                                | iOS background delivery (silent data update).                                                |
| `mutableContent`     | `boolean`                                                | iOS notification service extension may modify the payload (e.g. attach an image).            |
| `sound`              | `string \| false`                                        | Custom sound file name — or `false` for a silent notification.                               |
| `appId` / `appToken` | `number \| string`                                       | Optional overrides; normally set once with `NativeNotify.init()` / `<NativeNotifyProvider>`. |

Worth knowing:

- **Only the options you pass are sent.** Unset fields keep the server's defaults, and `sound: false`, `badge: 0` and `contentAvailable: false` are real values, not omissions.
- **The server validates them up front.** An invalid value (an unknown `interruptionLevel`, a non-boolean `mutableContent`, …) fails the request with a `400` instead of being rejected per recipient by Expo.
- **Raw REST calls accept one more field:** `expiration` (a unix timestamp) as an alternative to `ttl`. The SDK helpers expose `ttl`.
- **Open analytics work with these sends.** Mass and single-indie sends carry the hidden notification id that [Sessions & Push Opens](/docs/analytics/sessions-and-opens) reports on — enable it with `analytics: { opens: true }`.

## Android notification channels: `setAndroidNotificationChannel`

A `channelId` in your send has to match a channel the app has created on the device. `setAndroidNotificationChannel(channelId, options)` is a thin wrapper over expo-notifications' `setNotificationChannelAsync()`:

```ts
import * as Notifications from 'expo-notifications';
import { setAndroidNotificationChannel } from 'native-notify';

await setAndroidNotificationChannel('alerts', {
  name: 'Alerts',
  importance: Notifications.AndroidImportance.HIGH,
  sound: 'chime.wav',
  vibrationPattern: [0, 250, 250, 250],
});

// Then send to it:
await sendMassNotification('Heads up', 'The service is back', { channelId: 'alerts' });
```

Create the channel before sending a push with a matching `channelId`, so the notification routes to it. The channel's name, sound, importance, and vibration are user-visible in Android's per-app notification settings.

Options — only the keys you set are used:

| Option             | Type                              | Notes                                                                       |
| ------------------ | --------------------------------- | --------------------------------------------------------------------------- |
| `name`             | `string`                          | Human-readable name shown in Android settings (defaults to the channel id). |
| `description`      | `string`                          | Long description shown in Android settings.                                 |
| `importance`       | `Notifications.AndroidImportance` | `MAX` / `HIGH` / `DEFAULT` / `LOW` / `MIN`.                                 |
| `sound`            | `string \| null`                  | Custom sound file name (omit for the platform default).                     |
| `vibrationPattern` | `number[]`                        | Vibration pattern in ms, e.g. `[0, 250, 250, 250]`.                         |
| `lightColor`       | `string`                          | Notification LED color, e.g. `'#FF231F7C'`.                                 |
| `enableLights`     | `boolean`                         | Enable the notification LED.                                                |
| `enableVibrate`    | `boolean`                         | Enable vibration for this channel.                                          |
| `showBadge`        | `boolean`                         | Show a badge dot on the app icon for this channel's notifications.          |

`setAndroidNotificationChannel()` resolves `true` on Android; it resolves `false` on iOS/web (Android channels don't exist there) or when the call fails — it never throws, so callers keep their default channel.
