# API reference This is the public HTTP API of librenotes. All endpoints return JSON unless otherwise noted. Base URL: whatever you configured as `LIBRENOTES_BASE_URL` (for the cloud: `https://librenot.es`). ## Authentication librenotes uses passwordless email magic links plus short-lived JWTs. The flow is: 1. `POST /auth/login` with an email — server emails a one-time token. 2. The recipient clicks the link, which is `GET /auth/verify?token=...`. 3. The verify endpoint returns a session JWT (24-hour lifetime). 4. Subsequent calls send `Authorization: Bearer `. ### `POST /auth/login` Request: ```http POST /auth/login Content-Type: application/json { "email": "you@example.com" } ``` Response: | Status | Meaning | | ------ | ------- | | `202 Accepted` | Email accepted; magic link sent (or rate-limited silently). | | `400 Bad Request` | Body is malformed. | | `429 Too Many Requests` | Rate limit hit (5 per 15 min per email). | The response body is `{"status":"sent"}` on 202. The endpoint deliberately returns 202 even for unknown addresses to avoid account enumeration. ### `GET /auth/verify?token=` and `POST /auth/verify?token=` Consumes a magic-link token. Single-use. 15-minute expiry. ```json { "jwt": "eyJhbGciOiJIUzI1NiIsInR5cCI6...", "user_id": "f7b2e2c3-4a51-4d7c-bdc0-...", "email": "you@example.com", "expires_at": 1777411035 } ``` | Status | Meaning | | ------ | ------- | | `200 OK` | Verified; JWT issued. | | `400 Bad Request` | `token` parameter missing. | | `401 Unauthorized` | Token unknown, expired, or already used. | ## Notes All `/api/*` endpoints require `Authorization: Bearer `. A 401 means the token is missing/invalid; the client should redirect to the login page. ### `GET /api/whoami` Returns the verified tenant identity. ```json { "user_id": "f7b2e2c3-...", "email": "you@example.com" } ``` ### `GET /api/notes` List all notes for the current tenant. ```json [ { "id": "ideas", "title": "Ideas", "updated_at": 1777400000 }, { "id": "shopping", "title": "Shopping", "updated_at": 1777300000 } ] ``` ### `GET /api/notes/{id}` Read a single note. ```json { "id": "ideas", "title": "Ideas", "content": "A few directions...\n", "updated_at": 1777400000 } ``` | Status | Meaning | | ------ | ------- | | `200 OK` | Found. | | `400 Bad Request` | Invalid ID (must match `^[a-z0-9][a-z0-9_-]{0,127}$`). | | `404 Not Found` | No such note for this tenant. | ### `PUT /api/notes/{id}` Create or update a note. Supports optimistic locking via `?base=`: the server checks the current `updated_at` against `base` and returns 409 if the note has been modified since the client last saw it. Request: ```http PUT /api/notes/ideas?base=1777400000 Content-Type: application/json { "title": "Ideas", "content": "A few directions...\n" } ``` | Status | Meaning | | ------ | ------- | | `200 OK` | Saved; body returns the new `updated_at`. | | `400 Bad Request` | Bad ID, missing fields, or invalid `base`. | | `409 Conflict` | Note has been modified after `base`; body returns the current note so the client can resolve. | ### `DELETE /api/notes/{id}` Delete a note. Same `?base=` semantics as PUT. | Status | Meaning | | ------ | ------- | | `204 No Content` | Deleted (or already gone). | | `409 Conflict` | Modified after `base`. | ## Health ### `GET /healthz` Unauthenticated. Returns `{"status":"ok"}` when the binary is serving requests. Used by load balancers and the Docker HEALTHCHECK. ## Errors Beyond the per-endpoint codes above, all endpoints may return `500` for unhandled internal errors. Bodies are plain text (`internal server error`), not JSON, to avoid leaking internals. ## Rate limits `POST /auth/login` is the only currently rate-limited endpoint: 5 requests per 15-minute window per email address. The window resets on the oldest attempt expiring. ## Versioning The current public API surface is unversioned. Breaking changes will be signalled via a major version tag in the project as a whole, with a deprecation window announced in `CHANGELOG.md`.