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. 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.
List the inbox
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
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
const res = await fetch(
`https://app.nativenotify.com/api/universal/inbox/${appId}/${appToken}?deviceId=${deviceId}&take=20`
);
const { entries, total, unread } = await res.json();
The answer holds this device's entries, newest first:
{
"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
}
]
}
takeis1–200(default50). A larger value is a400, never a silent cap.skippages.totalandunreadcover 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: truewrites none.
Unread badge
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:
{
"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.
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. |