# Mobile notification inbox

List a device's notifications, show an unread badge, and mark entries read or delete them.

## Endpoints

The inbox is keyed by the `deviceId` you [registered](/docs/push/registration). Calls take your app id and app token.

| Call          | Endpoint                                                                      |
| ------------- | ----------------------------------------------------------------------------- |
| List          | `GET /api/universal/inbox/:appId/:appToken?deviceId=&take=&skip=`             |
| Unread count  | `GET /api/universal/inbox/:appId/:appToken/unread-count?deviceId=`            |
| Mark one read | `POST /api/universal/inbox/read` — `{ appId, appToken, deviceId, entryId }`   |
| Mark all read | `POST /api/universal/inbox/read-all` — `{ appId, appToken, deviceId }`        |
| Delete one    | `POST /api/universal/inbox/delete` — `{ appId, appToken, deviceId, entryId }` |
| Clear         | `POST /api/universal/inbox/clear` — `{ appId, appToken, deviceId }`           |

> **Plans:**
>
> On Premium every inbox call answers `403 plan_upgrade_required`. Sends still write entries, so they appear after an upgrade. See [what each plan includes](/docs/billing#what-each-plan-includes).

## List the inbox

<!-- tabs -->

#### Swift

```swift
let url = URL(string: "https://app.nativenotify.com/api/universal/inbox/\(appId)/\(appToken)?deviceId=\(deviceId)&take=20")!
let (data, _) = try await URLSession.shared.data(from: url)
// decode: total, unread, entries
```

#### Kotlin

```kotlin
val url = "https://app.nativenotify.com/api/universal/inbox/$appId/$appToken?deviceId=$deviceId&take=20"
// GET with any HTTP client, such as OkHttp or Ktor
```

#### React Native

```js
const res = await fetch(
  `https://app.nativenotify.com/api/universal/inbox/${appId}/${appToken}?deviceId=${deviceId}&take=20`
);
const { entries, total, unread } = await res.json();
```

<!-- /tabs -->

The answer holds this device's entries, newest first:

```json
{
  "ok": true,
  "deviceId": "device-abc",
  "take": 20,
  "skip": 0,
  "total": 12,
  "unread": 3,
  "entries": [
    {
      "entryId": 4181,
      "appId": 123,
      "deviceId": "device-abc",
      "subscriberId": null,
      "environment": "production",
      "title": "New service times",
      "body": "This Sunday: 9am and 11am.",
      "data": { "url": "/news/service-times" },
      "audienceType": "all",
      "source": "api",
      "sentAt": "2026-09-24T14:03:11.000Z",
      "readAt": null,
      "read": false
    }
  ]
}
```

- `take` is `1`–`200` (default `50`). A larger value is a `400`, never a silent cap. `skip` pages.
- `total` and `unread` cover the whole inbox, so "load more" is exact.
- Entries are written in the background, so one can appear a moment after the send. A send with `skipInbox: true` writes none.

## Unread badge

```js
const { unread } = await fetch(
  `https://app.nativenotify.com/api/universal/inbox/${appId}/${appToken}/unread-count?deviceId=${deviceId}`
).then((r) => r.json());
```

Poll while the app is open, or refresh when a push arrives.

## Mark read and delete

| Call          | Response                                                                                        |
| ------------- | ----------------------------------------------------------------------------------------------- |
| Mark one read | `{ "ok": true, "entryId", "read": true, "readAt" }`. Marking it again keeps the first `readAt`. |
| Mark all read | `{ "ok": true, "deviceId", "marked" }` — how many entries changed.                              |
| Delete one    | `{ "ok": true, "entryId", "removed" }` — `removed: false` when it was already deleted.          |
| Clear         | `{ "ok": true, "deviceId", "removed" }` — how many entries were cleared.                        |

Delete and clear are soft and idempotent, and never touch another device's inbox.

## Tap to open

Put a `url` in the send's `pushData`. The entry returns it as `data.url`:

```json
{
  "title": "New service times",
  "message": "This Sunday: 9am and 11am.",
  "pushData": { "url": "/news/service-times" }
}
```

The inbox never navigates on its own — your app decides what the `url` means. See [Push data & taps](/docs/setup/push-data).

## Errors

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

| Status | Code                                             | Meaning                                                          |
| ------ | ------------------------------------------------ | ---------------------------------------------------------------- |
| `400`  | `missing_field`, `invalid_field`, `invalid_body` | A missing or bad field, such as `take` over `200`.               |
| `400`  | `invalid_device_id`, `invalid_entry_id`          | A malformed id.                                                  |
| `401`  | —                                                | The app id and app token don't match. The body is plain text.    |
| `403`  | `plan_upgrade_required`                          | The account is on Premium.                                       |
| `404`  | `entry_not_found`                                | The entry isn't this device's, or you marked a deleted one read. |
| `404`  | `app_not_found`                                  | A dashboard-session or MCP caller has no access to that app id.  |
