# 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](/docs/universal-push/web-inbox#the-inbox-api-behind-it); these routes read the same entries from the owner's side.

All routes are account routes: a dashboard session or an [MCP access token](/docs/mcp-server) (`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](/docs/mass-notification-inbox), [Indie Push Notifications](/docs/indie-push-notifications)).

```text
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

```text
GET /api/agent/apps/:appId/inbox?take=20&skip=0&environment=production&subscriberId=reader-8241
```

| Query                            | Notes                                                                                                                       |
| -------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `take`                           | Sends per page, `1`–`100` (default `20`). A larger value is a `400`, never a silent cap.                                    |
| `skip`                           | Sends to skip (default `0`).                                                                                                |
| `environment`                    | Only sends made in `production`, `staging` or `development` (default: every environment).                                   |
| `deviceId` **or** `subscriberId` | Optional 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:

```json
{
  "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
  }
}
```

| Field                       | Meaning                                                                                                                                                                                                                   |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sendId`                    | The send's stable id — the same id a [send](/docs/universal-push/sending) response returns, the pushed notification carries, and a [schedule run](/docs/universal-push/scheduling#run-statuses) records. `id` repeats it. |
| `audienceType` / `audience` | `all`, `devices`, `subscribers` or `group`, and its label.                                                                                                                                                                |
| `source`                    | Who sent it — for example `api` (the send endpoint), `schedule` (a schedule rule) or `composer` (the dashboard).                                                                                                          |
| `devices`                   | Devices the send's audience resolved to.                                                                                                                                                                                  |
| `targeted`                  | Inbox entries written — one per targeted device.                                                                                                                                                                          |
| `read`                      | Entries marked read.                                                                                                                                                                                                      |
| `deletedByDevice`           | Entries a device removed from its own inbox.                                                                                                                                                                              |
| `lookup.state`              | For 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

```text
DELETE /api/agent/apps/:appId/inbox/sends/:sendId
```

```json
{ "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

```text
GET /api/agent/apps/:appId/audience/universal?q=&environment=&platform=&take=25&skip=0
```

```json
{
  "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](/docs/universal-push/environments), 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

```text
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).

```json
{
  "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](/docs/universal-push/verification#reading-the-failures) 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](/docs/universal-push/registration)).

## Token health

```text
GET /api/agent/apps/:appId/audience/universal/token-health?days=30
```

```json
{
  "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](/docs/tokens-and-receipts).

## Errors

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

| Status | Code                                                                     | Meaning                                                                                 |
| ------ | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------- |
| `400`  | `invalid_field`                                                          | A bad `take`, `skip` or `platform`, or both `deviceId` and `subscriberId` on the inbox. |
| `400`  | `invalid_environment`                                                    | Not `production`, `staging` or `development`.                                           |
| `400`  | `invalid_device_id`, `invalid_subscriber_id`, `missing_field`            | A too-long id, or the lookup without exactly one id.                                    |
| `400`  | `bad_app_id`, `bad_id`                                                   | A malformed app id or send id.                                                          |
| `403`  | `admin_only`                                                             | A developer tried to remove a send from every inbox.                                    |
| `404`  | `app_not_found`, `not_found`, `subscriber_not_found`, `device_not_found` | The app, the send, or the registration does not exist.                                  |
| `409`  | `classic_inbox`, `classic_audience`                                      | A classic Expo app — use its classic inbox and subscriber APIs.                         |

## For agents

| MCP tool                      | Route                                                        |
| ----------------------------- | ------------------------------------------------------------ |
| `list_universal_inbox`        | `GET …/inbox` (`device_id` / `subscriber_id` for the lookup) |
| `delete_universal_inbox_send` | `DELETE …/inbox/sends/:sendId`                               |
| `get_universal_audience`      | `GET …/audience/universal`                                   |
| `lookup_universal_audience`   | `GET …/audience/universal/lookup`                            |
| `get_universal_token_health`  | `GET …/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.

## Related

- [Web Bell & Inbox](/docs/universal-push/web-inbox) — the device-facing inbox API and widget.
- [Verify Delivery](/docs/universal-push/verification) — test-send, health counts and failure reasons.
- [Scheduling](/docs/universal-push/scheduling) — every schedule run links its `sendId`.
