---
title: "JSON API — Notifications for Laravel | Roundly"
description: "The opt-in JSON API — all 11 routes for the authenticated user’s inbox, preferences and types, with filters, bodies, errors and resources."
url: https://roundly-consulting.com/open-source/docs/notifications-for-laravel/json-api
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Notifications for Laravel](https://roundly-consulting.com/open-source/docs/notifications-for-laravel.md)

# JSON API

Off by default. Switch it on before boot:

```bash
NOTIFICATIONS_ROUTES_ENABLED=on
```

```php
// config/notifications.php
'routes' => [
    'enabled' => env('NOTIFICATIONS_ROUTES_ENABLED', 'off'),
    'prefix' => 'notifications',
    'middleware' => ['api'],
    'auth_middleware' => ['auth:sanctum'],
],
```

Every endpoint acts on $request->user() only — there is no recipient parameter. No user, even with auth\_middleware emptied, is a 401. Inputs go through FormRequests, and rate limits are yours to add. Route names start with notifications. To change the routes, publish the file — the copy then replaces the package’s:

```bash
php artisan vendor:publish --tag="notifications-routes"   # routes/notifications.php replaces the package's file
```

## Routes

| Method + path | Name | Does |
| --- | --- | --- |
| `GET /inbox` | `inbox.index` | filter\[status\]=active\|unread\|read\|archived\|all, filter\[context\], filter\[category\], filter\[type\], per\_page → paginated InboxItemResource. |
| `GET /inbox/counts` | `inbox.counts` | InboxCountsResource: unread, total, contexts\[{key, label, unread}\]. |
| `POST /inbox/read-all` | `inbox.read-all` | Body context?, category?, type? → {"marked": n}. |
| `POST /inbox/{item}/read` | `inbox.read` | Mark read → {"data": item}. |
| `DELETE /inbox/{item}/read` | `inbox.unread` | Mark unread → {"data": item}. |
| `POST /inbox/{item}/archive` | `inbox.archive` | Archive → {"data": item}. |
| `DELETE /inbox/{item}/archive` | `inbox.unarchive` | Restore from the archive → {"data": item}. |
| `POST /inbox/{item}/acknowledge` | `inbox.acknowledge` | Acknowledge the item’s subject and type → {"acknowledged": n}; 404 when the user can no longer see the subject; {"acknowledged": 0} for an item without a subject. |
| `GET /preferences` | `preferences.show` | PreferencesResource: types with channels (enabled, default, locked, locked\_reason), digest, quiet\_hours, timezone. |
| `PATCH /preferences` | `preferences.update` | channels\[<type>\]\[<channel>\]=bool, categories\[<category>\]\[<channel>\]=bool, digest{frequency,time}, quiet\_hours{start,end}\|null, timezone\|null — all or nothing. |
| `GET /types` | `types.index` | TypeResource list for a settings screen: key, category, label, description, channels, mandatory, priority, digestible. |

```http
GET    /notifications/inbox?filter[status]=unread&filter[context]=company:3&per_page=20
GET    /notifications/inbox/counts
POST   /notifications/inbox/read-all          {"context": "company:3", "category": "requests"}
POST   /notifications/inbox/12/read
DELETE /notifications/inbox/12/read
POST   /notifications/inbox/12/archive
DELETE /notifications/inbox/12/archive
POST   /notifications/inbox/12/acknowledge
GET    /notifications/preferences
PATCH  /notifications/preferences             {"channels": {"comment.posted": {"mail": false}}, "quiet_hours": {"start": "22:00", "end": "07:00"}}
GET    /notifications/types
```

{item} is resolved inside the user’s inbox, never by global route-model binding: another user’s id or a malformed id is a 404:

```json
{ "message": "The notification was not found.", "code": "inbox_item_not_found" }
```

POST /inbox/{item}/acknowledge re-checks access first: a user who lost access to the subject — or whose item’s subject is gone or type unregistered — gets the same 404, so an old item never silences an escalation the others rely on.

## PATCH /preferences

All or nothing: the request builds one PreferencesUpdate and calls preferences($user)->update(). Category toggles: false mutes, true clears the mute; quiet\_hours: null and timezone: null clear them:

```json
{
  "channels": { "comment.posted": { "mail": false } },
  "categories": { "contracts": { "mail": false } },
  "digest": { "frequency": "daily", "time": "08:00" },
  "quiet_hours": { "start": "22:00", "end": "07:00" },
  "timezone": "Europe/Bratislava"
}
```

Every part is checked before anything is written; a refused part is a Laravel validation error under its field and nothing is saved:

```json
{ "message": "The notification \"security.alert\" is required and cannot be switched off. (and 1 more error)",
  "errors": { "channels.security.alert.mail": ["The notification \"security.alert\" is required and cannot be switched off."],
              "categories.junk": ["The notification category \"junk\" is not used by any registered type."] } }
```

| Field | Refused when |
| --- | --- |
| `channels.<type>` | An unregistered type, or a channel other than inbox / mail. |
| `channels.<type>.<channel>` | A mandatory type switched off. |
| `categories.<category>` | A category no registered type uses, or an unknown channel. |
| `quiet_hours / timezone` | A bad time or an unknown timezone; null clears them. |

## Errors

| Status | Meaning |
| --- | --- |
| 401 | No authenticated user — even with auth\_middleware emptied. |
| 404 inbox\_item\_not\_found | Another user’s item, a malformed id, or an acknowledge the user may no longer make. |
| 422 | Laravel validation errors; domain exceptions with a translated message and a stable code — mandatory\_notification, invalid\_quiet\_hours, invalid\_timezone, unknown\_type, unknown\_notification\_category, invalid\_data. |

## An inbox item

```json
{
  "id": 12, "type": "request.awaiting_approval", "category": "requests", "title": "…", "body": null,
  "url": "https://app.test/requests/9", "count": 10, "data": {"reference": "PR-9"},
  "actor": {"id": 4, "type": "User"}, "subject": {"id": 9, "type": "PurchaseRequest"},
  "context": {"key": "company:3", "label": "Acme s.r.o.", "id": 3, "type": "Company"},
  "priority": {"value": "normal", "label": "Normal"}, "read_at": null, "archived_at": null,
  "first_event_at": "2026-11-09T10:00:00+00:00", "last_event_at": "2026-11-09T10:04:31+00:00"
}
```

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/notifications-for-laravel.md)
