# REST API

The daemon's HTTP endpoints.

Base URL `http://localhost:7331` (bound to `127.0.0.1` only).

## Authentication

Every route except `/status` requires `Authorization: Bearer <token>` whenever a
token is configured — which is always, when the daemon is started with
`bookmark-context serve`. A missing or wrong token returns
`401 {"detail": "Unauthorized"}`.

Requests with a body larger than 10 MB return `413`. CORS is open
(`Access-Control-Allow-Origin: *`); the token, not CORS, is what protects the
daemon.

## Endpoints

| Method | Path | Body | Success | Errors |
|---|---|---|---|---|
| GET | `/status` | — | `200 {status, version}` — no auth | — |
| GET | `/stats` | — | `200` process + disk stats (see below) | 401 |
| GET | `/collections` | — | `200 CollectionResponse[]` | 401 |
| POST | `/collections` | `{name, description?}` | `201 CollectionResponse` | 401, 422 |
| PATCH | `/collections/{id}` | `{name, description?}` | `200 CollectionResponse` | 404 |
| DELETE | `/collections/{id}` | — | `204` — also drops the Chroma collection | 404 |
| GET | `/bookmarks?url=` | — | `200 BookmarkResponse[]` across all collections | 401 |
| GET | `/collections/{id}/bookmarks` | — | `200 BookmarkResponse[]` | 401 |
| POST | `/collections/{id}/bookmarks` | `{url, title?, html?, favicon_url?}` | `201 BookmarkResponse`, then indexes in the background — **or `200 ScanWarning`**, see below | 404, 422 |
| DELETE | `/bookmarks/{id}` | — | `204` — also removes the bookmark's vector chunks | 404 |
| POST | `/bookmarks/{id}/reindex` | — | `200 BookmarkResponse` — no-op if already `indexing` | 404 |

## Saving a bookmark

`POST /collections/{id}/bookmarks` takes an optional `?force=` query parameter.

With `force=false` (default) the daemon fetches the page and runs the
prompt-injection scan *before* creating the bookmark. If the page looks like an
unrendered JavaScript shell, or any chunk trips the scanner, it returns
`200 ScanWarning` and **does not create the bookmark**:

```json
{
  "status": "scan_warning",
  "risk_score": 0.85,
  "signals": ["direct_override"],
  "matches": ["...ignore all previous instructions and..."]
}
```

With `force=true` the fetch-and-scan gate is skipped and the bookmark is created
immediately.

The `url` is validated: it must use `http`/`https`, have a host, and not resolve
to a private or internal address (loopback is allowed, so bookmarking your own
dev server works). A bad URL returns `422`.

## `GET /stats`

Authenticated. Returns:

```json
{
  "cpu_percent": 0.0,
  "memory_bytes": 734003200,
  "memory_processes": [ { "client": "cursor", "started_at": "...", "ppid": 1234, "parent_name": "Cursor", "uptime_seconds": 812 } ],
  "disk_bytes": 10485760,
  "git": { "branch": "master" },
  "trace_summary": { "enabled": true, "log_dir": "...", "process_log_bytes": 0, "tool_log_bytes": 0 }
}
```

`memory_processes` lists every project process, including one entry per MCP
server. `git` is `null` unless the daemon runs from a source checkout.
`trace_summary` is absent when tracing is off. These figures are kept off the
unauthenticated `/status` so no page you visit can fingerprint the install.

## Schemas

**CollectionResponse**

```
id, name, description, created_at, updated_at,
bookmark_count, favicon_previews: [{ url, favicon_url }]
```

**BookmarkResponse**

```
id, collection_id, url, title, favicon_url,
added_at, indexed_at | null, index_status, error_message?
```

**index_status**: `pending → indexing → done | error`
