Groups (Universal)
Universal push groups — subscribe subscriber ids and/or device keys to a keyed group (topics, segments, interests) from any framework, read the members, and send to the whole group with one call.
A group is an app-scoped, keyed audience — weekly-news, bakery-specials, volunteers — that any client can subscribe to. It is the universal push service's topic/segment model: subscribe once, then send to the group by its key.
Each member is one of two kinds:
- A subscriber id — an identity. It reaches every universal 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 (mass devices, browsers).
Subscribing is idempotent: subscribing an existing member is a no-op (joined: false), so a client can subscribe on every launch without ever duplicating.
Not the classic topic groups:
Universal groups are separate from the Expo path's topic groups and from dashboard segments. They use a different audience and send pipeline, and they do not interact.
All endpoints use your app id + app token, like every universal push endpoint — 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; you can mix all four member fields in one call (a subscriber and a device are two memberships). More than 500 ids of one kind is a 400 too_many_members — the batch is rejected, never truncated.
Response — 201:
{
"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). It answers 200 with removed per member (false when the member was not in the group) and the new counts. An unknown key answers 404 group_not_found. Removing a subscriber id stops the group reaching every device registered under it.
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
- List —
{ "ok": true, "groups": [ … ] }: every group on the app, oldest first, each withid,key,name,description,createdBy,createdAt,updatedAtandcounts. - One group —
{ "ok": true, "group": { … } }, by key (or by its numericidwhen no group has that key — an all-digit key always wins).404 group_not_foundwhen it does not exist. Read a single group for itsprunableDevicescount. - Members — one page of membership rows, oldest first:
{
"ok": true,
"group": { "id": 17, "key": "weekly-news", "name": "Weekly News", "counts": { "subscribers": 1, "devices": 0, "resolvableDevices": 2, "prunableDevices": 0 } },
"total": 1,
"take": 100,
"skip": 0,
"members": [
{
"memberId": 311,
"subscriberId": "user_8241",
"deviceId": null,
"source": "api",
"joinedAt": "2026-09-24T15:01:22.000Z",
"resolvedDevices": 2
}
]
}
take defaults to 100 and must be an integer 1..200 — a larger value is a 400 invalid_take, never a silent cap. total is the group's full membership count, so page with skip until you have read total rows.
What the counts mean
| 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 only reaches the ones in its own environment). |
prunableDevices | Device memberships whose device is not registered (never registered yet, or deregistered). They are never removed automatically; the dashboard's Prune action removes exactly those rows. Subscriber memberships are never prunable. |
Send to a group
A group is one audience of the ordinary universal send — inbox entries, analytics and token health included:
{
"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 device 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 is not an error — the response reports devices: 0, so check it.
Errors
Errors use the envelope { "error": { "code", "message", "field"? } }:
| 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 key 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 | 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. |
502 | subscribe_failed, unsubscribe_failed | The change could not be saved — retry. |
Managing groups from the dashboard
The dashboard's Segments tab lists universal groups next to classic segments, and can create a group (409 key_taken for a duplicate key), delete it (memberships go with it; devices, inbox and history are untouched), add or remove members, and prune stale device memberships. Those session routes live under /api/agent/apps/:appId/groups.
For agents
The MCP server exposes the public surface as tools: list_universal_groups, get_universal_group, list_universal_group_members (with all: true to page through every member), subscribe_to_universal_group, unsubscribe_from_universal_group and send_notification_to_universal_group.