# `Chime.API`
[🔗](https://github.com/hattoriai/chime/blob/v0.1.0-dev/lib/chime/api.ex#L1)

The feed and the preferences as JSON, for a frontend that Phoenix does
not render: React, Vue, Svelte, a mobile app. Mounted with
`Chime.Router.chime_api/2`:

    scope "/api" do
      pipe_through :api_authenticated
      chime_api "/notifications"
    end

Mount it behind the app's own API pipeline, the one that knows who is
signed in (a session, a bearer token of the app's): the resolver reads
the person from the conn (`c:Chime.Resolver.resolve_recipient/1`). A
request with no person is a 401. A request may instead carry a token of
`Chime.Token`, as `Bearer <token>`, for pages on another origin; and
`GET /stream` takes it as `?token=` too, as an `EventSource` cannot send
headers. Elsewhere a token in the address is not read: it would stay in
logs for its day.

## The routes

  * `GET /` - the person's feed, the latest first:
    `{"notifications": [...], "more": bool, "unseen": n, "unread": n}`.
    `?before=<id>` the page after a notification; `?limit=` up to 100
    (20); `?filter=` `inbox` (not archived, the default), `unread`,
    `archived` or `all`.
  * `GET /counts` - `{"unseen": n, "unread": n}`: one row, cheap enough
    to ask often.
  * `POST /seen` - the feed seen: the dot goes. The counts.
  * `POST /read` - every notification read. The counts.
  * `POST /:id/read`, `/:id/unread`, `/:id/archive`, `/:id/unarchive` -
    one notification. The counts; a 404 for one the person does not
    have.
  * `POST /:id/actions/:action` - takes an action of the notification
    (`Chime.act/3`): the counts, with `action` and `url` (where to go
    next, or null); a 404, or a 422 with `"unknown_action"`,
    `"already"` (an action was taken), or `"failed"` and the notifier's
    `reason`.
  * `GET /push` - `{"vapidPublicKey": key, "services": ["web", ...]}`,
    the app's public key for a browser's `pushManager.subscribe/1`.
  * `GET /devices` - the person's devices: `{"devices": [{"id", "kind",
    "name", "enabled", "lastUsedAt", "insertedAt"}]}`.
  * `POST /devices` - registers one, with `{"kind": "web", "token":
    subscription.endpoint, "keys": subscription.keys, "name": "..."}`, or
    `{"kind": "apns" | "fcm", "token": "..."}`: a 201 with `{"device"}`,
    or a 422 with `"unknown_kind"`, `"invalid_token"`,
    `"invalid_subscription"`.
  * `PATCH /devices/:id` - `{"enabled": false}` turns it off, and on;
    `{"name": "..."}` names it.
  * `DELETE /devices/:id` - removes it.
  * `GET /preferences` - the person's settings:
    `{"categories": [{"key", "label", "description", "required",
    "settings": {"feed": "on", "email": "daily"}}], "channels": [{"key":
    "email", "settings": ["on", "daily", "weekly", "off"]}]}`.
  * `PUT /preferences` - changes settings, with
    `{"preferences": {"comments": {"email": "daily"}}}` as the body:
    the settings as `GET` gives them, or a 422 with
    `{"error": code, "category", "channel"}` (`"required"`,
    `"unknown_category"`, `"unknown_channel"`, `"invalid_setting"`) and
    nothing changed.
  * `GET /stream` - the person's live news, as server-sent events
    (`text/event-stream`): `counts` at once, then `notified` when a
    notification comes and `updated` when they see or read in another
    tab, each with `{"unseen": n, "unread": n}` as its data, and a
    comment every 25 seconds to keep the connection.
  * `POST /token` - a token of `Chime.Token` for the person of the
    request: `{"token": "...", "expiresIn": seconds}`.

A notification is:

    {
      "id": "0193…",
      "kind": "new_comment",
      "category": "comments",
      "title": "Ana and 2 others commented on Tokyo",
      "body": "Lovely light.",
      "url": "/stories/42",
      "icon": "comment",
      "actions": [{"key": "reply", "label": "Reply"}],
      "action": null, "actedAt": null, "clickedAt": null,
      "extra": {},
      "count": 3,
      "actors": [{"id": "user:7", "name": "Ana"}],
      "seen": true, "read": false, "archived": false,
      "seenAt": "2026-10-25T14:03:00Z", "readAt": null, "archivedAt": null,
      "insertedAt": "2026-10-25T14:02:11Z", "updatedAt": "2026-10-25T14:09:40Z"
    }

`title`, `body`, `url` and `icon` are the notifier's `feed/2`, rendered
for the request (its language, through the app's own plugs); `extra`
has whatever else it gave.

An error is `{"error": code}`: `"unauthorized"` (a 401, with
`"token_expired"` for a token past its time), `"not_found"`.

Behind the app's `protect_from_forgery`, a `POST` needs the CSRF token
of the session in the `x-csrf-token` header.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
