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"
}
FieldRequiredNotes
appId, appTokenyesYour app's pair.
keyyes1–64 characters: lowercase letters, digits, dashes and underscores, starting with a letter or digit. Aliases: groupKey, group.
subscriberIdone memberOne subscriber id (max 256 chars). Aliases: subscriber_id, subId, sub_id.
deviceIdone memberOne registered device key (max 200 chars). Alias: device_id.
subscribersone memberBulk subscriber ids, up to 500 per call. Aliases: subscriberIds, subscriber_ids.
devicesone memberBulk device keys, up to 500 per call. Aliases: deviceIds, device_ids.
name, descriptionnoUsed only when this call creates the group (max 120 / 500 chars). The name defaults to the key.
createnoDefault 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 with id, key, name, description, createdBy, createdAt, updatedAt and counts.
  • One group — { "ok": true, "group": { … } }, by key (or by its numeric id when no group has that key — an all-digit key always wins). 404 group_not_found when it does not exist. Read a single group for its prunableDevices count.
  • 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

CountMeaning
subscribers / devicesMembership rows of each kind.
resolvableDevicesDistinct 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).
prunableDevicesDevice 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"? } }:

StatusCodeMeaning
400invalid_body, missing_field, invalid_fieldBad body, no key, a bad appId, or a name / description that is too long.
400invalid_group_keyThe key does not match the key rules above.
400invalid_subscriber_id, invalid_device_idAn empty or too-long member id, or a member list that is not an array.
400no_members, too_many_membersNo member at all, or more than 500 of one kind.
400invalid_take, invalid_skipBad member-page parameters.
404app_not_found, group_not_foundThe app is not one this caller can access, or the key does not exist.
502subscribe_failed, unsubscribe_failedThe 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.