Inbox & Audience Views

The owner's side of an Agent Notify project — the universal inbox as sends with read counts, one device's or subscriber's read state, removing a send from every inbox, and who is registered (devices, subscribers, masked tokens, token health).

These are the account-side reads behind the dashboard's Push Notifications → Inbox and Audience tabs for an Agent Notify project — the data an owner (or their agent) needs to answer "did it arrive?", "did they read it?" and "why didn't they get it?". The device-facing inbox that apps and the web bell read is on Web Bell & Inbox; these routes read the same entries from the owner's side.

All routes are account routes: a dashboard session or an MCP access token (Authorization: Bearer nnmcp_…) — an app token is refused with 403. Every call re-checks that the account (or its team) can reach the app. A classic Expo app answers 409 classic_inbox / 409 classic_audience: its inbox and subscribers are the classic ones (Mass Notification Inbox, Indie Push Notifications).

GET    https://app.nativenotify.com/api/agent/apps/:appId/inbox                             — sends + read state
DELETE https://app.nativenotify.com/api/agent/apps/:appId/inbox/sends/:sendId               — remove a send from every inbox
GET    https://app.nativenotify.com/api/agent/apps/:appId/audience/universal                — who is registered
GET    https://app.nativenotify.com/api/agent/apps/:appId/audience/universal/lookup         — one subscriber or device
GET    https://app.nativenotify.com/api/agent/apps/:appId/audience/universal/token-health   — token health

Inbox — sends and read state

GET /api/agent/apps/:appId/inbox?take=20&skip=0&environment=production&subscriberId=reader-8241
QueryNotes
takeSends per page, 1–100 (default 20). A larger value is a 400, never a silent cap.
skipSends to skip (default 0).
environmentOnly sends made in production, staging or development (default: every environment).
deviceId or subscriberIdOptional lookup: every row then says whether that device — or any device of that subscriber — got it and read it. Not both.

Response — 200, newest send first:

{
  "ok": true,
  "pipeline": "universal",
  "take": 20,
  "skip": 0,
  "total": 38,
  "sends": [
    {
      "id": 1000000412,
      "sendId": 1000000412,
      "title": "Story hour this Saturday",
      "body": "Bring the family to the children's corner at 10:00.",
      "data": { "url": "/events/story-hour" },
      "sentAt": "2026-09-24T14:03:11.000Z",
      "audienceType": "all",
      "audience": "All devices",
      "environment": "production",
      "source": "schedule",
      "devices": 1284,
      "targeted": 1284,
      "read": 311,
      "deletedByDevice": 12,
      "lookup": { "state": "read", "targeted": 2, "read": 1, "deleted": 0, "readAt": "2026-09-24T15:10:02.000Z" }
    }
  ],
  "lookup": {
    "by": "subscriber",
    "id": "reader-8241",
    "registered": true,
    "devices": [
      {
        "deviceId": "A1B2C3D4-install-key",
        "platform": "ios",
        "environment": "production",
        "subscriberId": "reader-8241",
        "appVersion": "2.4.0",
        "lastSeenAt": "2026-09-25T08:14:00.000Z",
        "registeredAt": "2026-09-01T12:00:00.000Z"
      }
    ],
    "total": 18,
    "unread": 3
  }
}
FieldMeaning
sendIdThe send's stable id — the same id a send response returns, the pushed notification carries, and a schedule run records. id repeats it.
audienceType / audienceall, devices, subscribers or group, and its label.
sourceWho sent it — for example api (the send endpoint), schedule (a schedule rule) or composer (the dashboard).
devicesDevices the send's audience resolved to.
targetedInbox entries written — one per targeted device.
readEntries marked read.
deletedByDeviceEntries a device removed from its own inbox.
lookup.stateFor the looked-up device or subscriber: read (read on at least one of their devices), unread, deleted (removed from every inbox they got it in) or not_sent (the send did not target them).
lookup (top level)Who the id is — their registered devices (registered: false when none) and their inbox total / unread. null without a lookup.
  • total counts every listed send (for this environment), so page with skip until you have read total rows.
  • A send is listed once at least one inbox entry carries its id; a send that matched no device reached no inbox and is not listed.
  • Reading changes nothing — no entry is marked read.

Remove a send from every inbox

DELETE /api/agent/apps/:appId/inbox/sends/:sendId
{ "ok": true, "deleted": 1000000412, "rows": 1272 }

The notification disappears from every device's inbox. It is a soft delete: each entry is hidden and the send leaves the dashboard lists, while the send record and its delivery and open stats are kept. There is no undo route. rows is how many inbox entries were hidden (0 when every device had already removed it).

  • Owner or admin only — a teammate with the developer role gets 403 admin_only.
  • An unknown or already deleted send is 404 not_found; a malformed id is 400 bad_id.

Audience — who is registered

GET /api/agent/apps/:appId/audience/universal?q=&environment=&platform=&take=25&skip=0
{
  "ok": true,
  "pipeline": "universal",
  "summary": {
    "devices": 1349,
    "subscribers": 97,
    "anonymousDevices": 1228,
    "byPlatform": { "ios": 640, "android": 689, "web": 20 },
    "byEnvironment": { "production": 1344, "staging": 4, "development": 1 },
    "tokens": {
      "apns": { "live": 639, "dead": 3, "struck": 1 },
      "fcm": { "live": 687, "dead": 5, "struck": 2 },
      "web": { "live": 20, "dead": 0, "struck": 0 }
    }
  },
  "take": 25,
  "skip": 0,
  "total": 97,
  "subscribers": [
    {
      "subscriberId": "reader-8241",
      "devices": 2,
      "platforms": ["ios", "web"],
      "environments": ["production"],
      "lastSeenAt": "2026-09-25T08:14:00.000Z",
      "firstSeenAt": "2026-09-01T12:00:00.000Z"
    }
  ]
}
  • summary covers the whole project: registered devices, distinct subscribers, anonymousDevices (registered without a subscriberId), devices by platform and by environment, and the token census per type — live, dead (retired) and struck (live, but failed at least once).
  • subscribers is one page of subscriber ids, most recently seen first. q (a subscriber-id fragment), environment and platform (ios, android, web) filter this page and its total.
  • take is 1–100 (default 25) — a larger value is a 400, never a silent cap.

One subscriber or device

GET /api/agent/apps/:appId/audience/universal/lookup?subscriberId=reader-8241&days=30

Pass exactly one of subscriberId (every device registered under it) or deviceId (one device). days is the failure window: 1–365, default 30 (a larger value reads 365 days).

{
  "ok": true,
  "pipeline": "universal",
  "by": "subscriber",
  "id": "reader-8241",
  "devices": [
    {
      "deviceId": "A1B2C3D4-install-key",
      "subscriberId": "reader-8241",
      "platform": "ios",
      "environment": "production",
      "appVersion": "2.4.0",
      "timezone": "America/New_York",
      "registeredAt": "2026-09-01T12:00:00.000Z",
      "lastSeenAt": "2026-09-25T08:14:00.000Z",
      "tokens": [
        {
          "type": "apns",
          "tokenMasked": "…9d8e7f",
          "strikes": 0,
          "dead": false,
          "registeredAt": "2026-09-01T12:00:00.000Z",
          "lastSeenAt": "2026-09-25T08:14:00.000Z"
        }
      ]
    }
  ],
  "inbox": { "total": 18, "unread": 3 },
  "failures": {
    "windowDays": 30,
    "reasons": [{ "errorCode": "APNs:BadDeviceToken", "count": 1, "lastSeen": "2026-09-12T09:30:04.000Z" }]
  },
  "cleanupEnabled": true
}
  • Token values are always masked (…9d8e7f; a browser keeps its push-service host, fcm.googleapis.com/…a1b2c3) — support needs the type and health, never the value.
  • failures.reasons are the transport answers recorded for these tokens (APNs:…, FCM:…, WebPush:…) — see Verify Delivery for what each means.
  • Nothing registered under the id is 404 subscriber_not_found / device_not_found — the app registers a subscriber's devices with its subscriberId at login (Device Registration).

Token health

GET /api/agent/apps/:appId/audience/universal/token-health?days=30
{
  "ok": true,
  "pipeline": "universal",
  "windowDays": 30,
  "cleanupEnabled": true,
  "byType": {
    "apns": { "live": 639, "dead": 3, "struck": 1 },
    "fcm": { "live": 687, "dead": 5, "struck": 2 },
    "web": { "live": 20, "dead": 0, "struck": 0 }
  },
  "struck": [
    {
      "type": "fcm",
      "tokenMasked": "…c4d5e6",
      "strikes": 1,
      "dead": false,
      "lastSeenAt": "2026-09-20T18:02:11.000Z",
      "deviceId": "android-install-7",
      "subscriberId": null,
      "platform": "android",
      "environment": "production"
    }
  ],
  "failures": [{ "errorCode": "FCM:UNREGISTERED", "count": 4, "lastSeen": "2026-09-24T14:03:12.000Z" }]
}
  • struck lists up to 50 tokens that have failed with a dead-token answer or were retired — live ones with the most strikes first, then retired ones.
  • failures are the top 20 transport failure codes of the last days (1–365, default 30).
  • cleanupEnabled says whether this server retires dead tokens at all (after 2+ separate dead-token failures) — see Tokens & Receipts.

Errors

Errors use the envelope { "error": { "code", "message", "field"? } }:

StatusCodeMeaning
400invalid_fieldA bad take, skip or platform, or both deviceId and subscriberId on the inbox.
400invalid_environmentNot production, staging or development.
400invalid_device_id, invalid_subscriber_id, missing_fieldA too-long id, or the lookup without exactly one id.
400bad_app_id, bad_idA malformed app id or send id.
403admin_onlyA developer tried to remove a send from every inbox.
404app_not_found, not_found, subscriber_not_found, device_not_foundThe app, the send, or the registration does not exist.
409classic_inbox, classic_audienceA classic Expo app — use its classic inbox and subscriber APIs.

For agents

MCP toolRoute
list_universal_inboxGET …/inbox (device_id / subscriber_id for the lookup)
delete_universal_inbox_sendDELETE …/inbox/sends/:sendId
get_universal_audienceGET …/audience/universal
lookup_universal_audienceGET …/audience/universal/lookup
get_universal_token_healthGET …/audience/universal/token-health

A typical support answer chains them: lookup_universal_audience (is the person registered, in which environment, are their tokens healthy?) → list_universal_inbox with their subscriber_id (did the send reach them, did they read it?). The in-dashboard agent answers the same questions from the same data. Agents are instructed to confirm with you before removing a send from every inbox.