Groups
Push groups — subscribe subscriber ids and/or device keys to a keyed group from any framework, read the members, and send to the whole group with one call. Every app also has three always-on groups (Lapsed, Active, New), filled automatically from app opens.
A group is an app-scoped, keyed audience — weekly-news, bakery-specials, volunteers — that any client can subscribe to. Subscribe once, then send to the group by its key.
The three always-on groups
Every app always has three groups. There is nothing to create and nothing to turn on:
| Key | Who is in it | Rule |
|---|---|---|
lapsed | No app open in the last 30 days | lapsed_30d |
active | Opened the app in the last 7 days | active_7d |
new | First app open in the last 7 days | new_7d |
Membership is computed live, by subscriber id, from app opens: a device re-registers on every launch, so its subscriber counts as active the moment the app opens. What the counts report is what a send reaches — every member holds at least one live token.
Always-on groups are read-only: they cannot be subscribed to, edited or deleted, and their keys are reserved. Send to them — "audience": { "type": "group", "key": "lapsed" } — or schedule against them like any other group.
The groups you create yourself are manual groups — the rest of this page is about them.
A manual group's members are one of two kinds:
- A subscriber id — an identity. It reaches every device registered under that
subscriberId, now and in the future (new phones and reinstalls included). - A device key — one registered
deviceId, for anonymous audiences that have no subscriber identity (devices registered without asubscriberId, such as browsers).
Subscribing is idempotent: subscribing an existing member is a no-op (joined: false), so a client can subscribe on every launch.
Not the classic topic groups:
Universal groups are separate from the classic Expo path's topic groups: a different audience and send pipeline, and they do not interact.
All endpoints use your app id + app token — in the body for the POSTs, in the URL for the GETs.
Subscribe
POST https://app.nativenotify.com/api/universal/groups/subscribe
{
"appId": 123,
"appToken": "yourAppToken",
"key": "weekly-news",
"name": "Weekly News",
"subscriberId": "user_8241"
}
| Field | Required | Notes |
|---|---|---|
appId, appToken | yes | Your app's pair. |
key | yes | 1–64 characters: lowercase letters, digits, dashes and underscores, starting with a letter or digit. Aliases: groupKey, group. |
subscriberId | one member | One subscriber id (max 256 chars). Aliases: subscriber_id, subId, sub_id. |
deviceId | one member | One registered device key (max 200 chars). Alias: device_id. |
subscribers | one member | Bulk subscriber ids, up to 500 per call. Aliases: subscriberIds, subscriber_ids. |
devices | one member | Bulk device keys, up to 500 per call. Aliases: deviceIds, device_ids. |
name, description | no | Used only when this call creates the group (max 120 / 500 chars). The name defaults to the key. |
create | no | Default true: an unknown key creates the group. Pass false to answer 404 group_not_found instead. |
Send at least one member; the four member fields can be mixed in one call. More than 500 ids of one kind is a 400 too_many_members — the batch is rejected, never truncated.
{
"ok": true,
"group": { "id": 17, "key": "weekly-news", "name": "Weekly News", "created": true },
"results": { "subscribers": [{ "subscriberId": "user_8241", "joined": true }], "devices": [] },
"counts": { "subscribers": 1, "devices": 0, "resolvableDevices": 2, "prunableDevices": 0 }
}
Unsubscribe
POST https://app.nativenotify.com/api/universal/groups/unsubscribe
Same body as subscribe (key plus members). Answers 200 with removed per member (false when the member was not in the group) and the new counts; an unknown key is 404 group_not_found.
Read groups and members
GET https://app.nativenotify.com/api/universal/groups/:appId/:appToken
GET https://app.nativenotify.com/api/universal/groups/:appId/:appToken/:key
GET https://app.nativenotify.com/api/universal/groups/:appId/:appToken/:key/members?take=100&skip=0
The list returns every group on the app, oldest first, each with id, key, name, description, createdBy, createdAt, updatedAt and counts. One group returns a single group by key (or by its numeric id when no group has that key), or 404 group_not_found. Members returns one page of membership rows — { memberId, subscriberId, deviceId, source, joinedAt, resolvedDevices } — with total, take and skip. take defaults to 100 and must be an integer 1..200 (400 invalid_take), and total is the full membership count, so page with skip until you have read every row. For an always-on group the page is its computed members: read-only, source: "auto", memberId is null, and rows are ordered by subscriber id.
| Count | Meaning |
|---|---|
subscribers / devices | Membership rows of each kind. |
resolvableDevices | Distinct registered devices with at least one live token that the memberships reach right now (across all environments — a send reaches only the ones in its own environment). |
prunableDevices | Device memberships whose device is not registered. Never removed automatically; the dashboard's Prune action removes exactly those rows. Subscriber memberships are never prunable. |
For an always-on group, subscribers and resolvableDevices are its computed member count — it stores no membership rows, and devices is always 0.
Send to a group
A group is one audience of the ordinary send:
{
"appId": 123,
"appToken": "yourAppToken",
"title": "This week's newsletter",
"message": "New recipes from the bakery and the library's reading list.",
"audience": { "type": "group", "key": "weekly-news" }
}
At send time the memberships resolve to live tokens in the send's environment (production when omitted). A device reached by both a subscriber membership and a device membership gets the push once. An unknown or empty group reports devices: 0, so check it.
Sending to a group is a Pro plan feature: on Premium the send answers 403 plan_upgrade_required and nothing is sent. Subscribing, unsubscribing and reading groups work on every plan.
Errors
| Status | Code | Meaning |
|---|---|---|
400 | invalid_body, missing_field, invalid_field | Bad body, no key, a bad appId, or a name / description that is too long. |
400 | invalid_group_key | The key does not match the rules above. |
400 | invalid_subscriber_id, invalid_device_id | An empty or too-long member id, or a member list that is not an array. |
400 | no_members, too_many_members | No member at all, or more than 500 of one kind. |
400 | auto_group | A subscribe/unsubscribe named one of the always-on groups (lapsed, active, new) — they fill themselves. |
400 | reserved_group_key | A group key reserved for the always-on groups was used to create a manual group. |
400 | invalid_take, invalid_skip | Bad member-page parameters. |
404 | app_not_found, group_not_found | The app is not one this caller can access, or the key does not exist. |
409 | key_reserved, system_group | The dashboard-facing codes: a create named a reserved key, or an edit/delete targeted an always-on group. |
502 | subscribe_failed, unsubscribe_failed | The change could not be saved — retry. |
The dashboard's Groups tab lists every group — the three always-on ones first, with live counts — and can create, edit, and prune manual groups; the MCP server exposes the same surface as tools (list_universal_groups, subscribe_to_universal_group, send_notification_to_universal_group, …).