# API Conventions The HTTP contract lives in the code: route inventory in `backend/internal/handler/handler.go` (`SetupRouter`), response shapes in `backend/internal/model/model.go` (JSON tags), request bodies in the handler files (`createFeedRequest`, `createBookmarkRequest`, ...), and the frontend call surface in `frontend/src/lib/api/index.ts`. This page records only the cross-cutting rules that are distributed across handlers, so they do not need to be rediscovered per endpoint. If code and this page disagree, the code wins — fix the page. ## Base path and authentication - All endpoints are under `/api`. Two exceptions: the Fever compatibility API (`/fever*`, see `docs/fever-api.md`) and a bare `GET /oidc/callback` registered only when OIDC is configured, for deployments whose redirect URI omits `/api`. - Authentication is a session cookie named `session` (`POST /api/sessions` with `{"password": ...}` sets it; `DELETE /api/sessions` clears it, 204). - Anonymous mode: when both password and OIDC are unconfigured, the auth middleware lets every request through. - Login attempts are rate limited: excess attempts get `429` with a `Retry-After` header (seconds). - OIDC: `GET /api/oidc/enabled` always exists; `/api/oidc/login` and `/api/oidc/callback` are registered only when OIDC is configured. Login returns `{data: {auth_url}}` for the client to navigate to; the callback redirects (307) to `/` on success or `/login?error=oidc_failed` on failure. ## Response envelopes - Single resource: `{"data": }` - Group and feed lists (small, fully returned): `{"data": [...], "total": n}` - Item and bookmark lists (cursor-paginated): `{"data": [...], "total": n, "next_cursor": }`; `next_cursor` is `null` when there are no more pages. ## Cursor pagination - Pass the previous page's `next_cursor` as the `before` query parameter; omit it on the first page. - Cursor format: `"_"` (two int64s, underscore-separated). The value is `pub_date` (unix) for items and `created_at` (unix) for bookmarks; `id` breaks ties. - The items cursor is keyed on `pub_date`, so `before` is only valid with the default ordering: combining `order_by=created_at` with `before` is rejected with 400. Only the first page can use `created_at`. - Ordering is always DESC. Items support `order_by=pub_date|created_at` (default `pub_date`); any other value silently falls back to `pub_date`. - `limit` is capped at 100 on every endpoint that accepts one (items, bookmarks, search); `maxListLimit` in `backend/internal/handler/item.go`. ## Errors Handlers report failures as `{"error": ""}`: - `400` — invalid input (message varies) - `401` — `"unauthorized"` - `404` — `" not found"` - `429` — `"too many login attempts"` (plus `Retry-After`) - `500` — `"internal server error"` (details are logged server-side only) Three cases return the status with an empty body instead: unknown routes (404), CORS origin rejection (403), and panic recovery (500). ## Status codes - `200` — any response with a body: reads, creates, and updates - `204` — body-less success: mark items read/unread, all deletes, logout, and the `GET /api/sessions` validity probe (valid → 204, invalid → 401) - `202` — feed refresh requests (`/feeds/refresh`, `/feeds/{id}/refresh`): refresh happens asynchronously, no body ## Request quirks worth knowing - `POST /api/bookmarks` has two modes: `{item_id}` snapshots the bookmark from that item server-side (preferred), or a full `{link, title, content, feed_name, pub_date?}` snapshot when there is no item. Sending `item_id` makes the server ignore all other fields. - `PATCH /api/items/-/read` and `-/unread` accept 1–1000 ids (`maxBatchUpdateIDs`). - `refresh_interval_seconds` (create and update feed): on **create**, absent or `0` means "use the global interval" (stored as `NULL`, never as `0`); on **update**, absent or `null` leaves the stored value unchanged and `0` clears it back to the global interval; any other value must be in the closed set returned by `GET /api/app` (`allowed_refresh_intervals`). - `PATCH /api/settings` is a partial update: an absent field leaves the stored value unchanged, and there is deliberately no way to clear a preference back to null. Closed sets: locale `en|zh|de|fr|es|ru|pt|sv`, theme `light|dark|system`, `auto_mark_read` `off|open|5|10|30`, `article_page_size` 1–100.