> ## Documentation Index
> Fetch the complete documentation index at: https://docs.elanotes.com/llms.txt
> Use this file to discover all available pages before exploring further.

# API overview

> Host path, auth, errors, filters, field masks, pagination, rate limits, idempotency, custom methods, and deprecations.

The authoritative contract is OpenAPI 3.1: `GET /openapi.json`, committed
at `contracts/openapi/elanotes.v1.json`, copied to this site's
`openapi.json`. Generated reference pages sit under **API reference**.

Style authority is [Google AIP](https://google.aip.dev). Recorded
deviations: envelope outer shape, vault-implicit paths, `Idempotency-Key`
header, receipts instead of AIP-151 Operations.

There is no `/v1/daily`, `/v1/tasks`, or `folder_id`. Daily notes are
`filter=kind = "daily"`. Tasks are checklist items in note bodies.
Folders are vault-relative `path` strings.

## Host path

|            | Origin                     | Notes                                                      |
| ---------- | -------------------------- | ---------------------------------------------------------- |
| Local      | `http://127.0.0.1:4000`    | Clone / `pnpm --filter @elanotes/api dev`                  |
| Production | `https://api.elanotes.com` | `GET /healthz` and `GET /openapi.json` are unauthenticated |

```bash theme={null}
curl -sf https://api.elanotes.com/healthz
curl -sS -o /dev/null -w "%{http_code}\n" https://api.elanotes.com/openapi.json
curl -sS -o /dev/null -w "%{http_code}\n" https://api.elanotes.com/v1/vault
```

`/healthz` returns `{"status":"ok"}`. `/openapi.json` returns `200`.
Unauthenticated `/v1/vault` is `401` `AUTH_MISSING`, not `503`. The API
reference server list is production, then local.

`/v1` is the stable path. `GET /openapi.json` and `GET /healthz` are
unauthenticated. Auth protects only `/v1` and `/mcp`.

## Auth and `WWW-Authenticate`

Send `Authorization: Bearer <token>`. Open locally when no auth is
configured. The vault is implicit: the bearer binds `vault_id`. Resource
bodies may carry AIP-122 `name` (`vaults/{vault_id}/notes/{slug}`); paths
stay `/v1/notes/{note}`.

`401` / `403` on `/v1` and `/mcp` add RFC 6750
`WWW-Authenticate: Bearer realm="elanotes"`:

* missing token → no `error=` pair (challenge only)
* presented but invalid → `error="invalid_token"`
* forbidden → `error="insufficient_scope"` plus `scope` naming the missing
  permission (`notes.write` / `notes.review`)

When `WORKOS_CLIENT_ID` or `OAUTH_TEST_HMAC_SECRET` is set, the challenge
includes `resource_metadata` and
`GET /.well-known/oauth-protected-resource` serves RFC 9728 metadata.
Otherwise that well-known path is `404` with a hint.

## Errors, reasons, hints

Envelope:

```json theme={null}
{
  "error": "conflict",
  "reason": "VERSION_MOVED",
  "message": "version moved: current is 3f2c… — re-read before editing",
  "hint": "GET /v1/notes/{note} to read the current version_id, then retry :edit with that version_id and a fresh Idempotency-Key.",
  "help": "https://docs.elanotes.com/api/errors#version_moved",
  "statusCode": 409,
  "requestId": "…",
  "details": []
}
```

`error`, `message`, `statusCode`, `requestId` keep their historic meaning.
`reason` is UPPER\_SNAKE. `hint` is one imperative sentence naming the next
request. `help` is this site's [errors](/api/errors) page with a
lowercase-reason anchor. `details` is the AIP-193 list (`ErrorInfo`,
`BadRequest`, `RetryInfo`, `QuotaFailure`, `Help`). Follow `hint`.

## Filters

AIP-160 `filter` + AIP-132 `order_by` on `GET /v1/notes`, `/v1/search`,
`/v1/grep`, `/v1/writes`, and `/v1/notes/{note}:listRevisions`.
`GET /v1/tags` rolls up frontmatter tags (body `#hashtags` are not tags).

```http theme={null}
GET /v1/notes?filter=kind = "daily" AND updated_at >= "2026-09-01T00:00:00Z"
GET /v1/notes?filter=tags:decision
GET /v1/notes?filter=path = "work/*"
```

Comparators `=`, `!=`, `<`, `<=`, `>`, `>=`; `:` (has) for repeated
fields; `AND`, `OR`, `NOT` / `-`; parentheses; double-quoted strings;
RFC 3339 timestamps; `*` wildcard inside string literals.

Note fields: `slug`, `path`, `folder`, `title`, `tags`, `kind`
(`regular` | `daily`), `status` (`active` | `archived`), `visibility`,
`daily_date`, `created_at`, `updated_at`, `observed_at`.

Pending-write fields: `status`, `kind`, `agent_id`, `created_at`,
`decided_at`.

Unknown field or operator → `400` `INVALID_FILTER` with a hint listing
allowed fields. Coverage echoes `filter`.

`POST /v1/query` stays for property aggregation. The list path is
`GET /v1/notes?filter=`.

## Field masks

```http theme={null}
PATCH /v1/notes/{note}?update_mask=tags
PATCH /v1/vault/write-settings?update_mask=write_policy
```

AIP-161 paths: `title`, `content`, `folder`, `tags`, `status`,
`frontmatter.<key>`. Body is the partial resource. Missing or unknown
mask → `400` `INVALID_UPDATE_MASK`. Writes still need `Idempotency-Key`
and honor review / budget.

## Pagination

Canonical AIP-158: `page_size`, `page_token`, `next_page_token`.
Legacy `limit` / `cursor` / `next_cursor` / `prefix` / `archived` /
`status` stay accepted and emitted through v1 and are marked deprecated
in the spec. Default order stays deterministic.

`order_by` examples: `updated_at desc`, `created_at`, `observed_at`,
`title`, `daily_date desc`.

## Rate limits

Abuse protection, **not** cost metering. Reads are never metered.

| Policy | Default   | Applies to                                      |
| ------ | --------- | ----------------------------------------------- |
| Read   | 600 / min | `GET /v1/*`, `/mcp`                             |
| Write  | 60 / min  | `POST` / `PATCH` / `PUT` / `DELETE` under `/v1` |

`0` disables a policy. Responses carry IETF draft `RateLimit-Limit` /
`RateLimit-Remaining` / `RateLimit-Reset`. Every `429` includes
`Retry-After`. Envelope `reason` is `RATE_LIMITED` (abuse) or
`WRITE_BUDGET_EXCEEDED` (per-agent budget, separate quota).

## Idempotency

Writes require `Idempotency-Key`. Replay with the same key and body
returns the original receipt (`idempotency-replayed: true`). A different
body with the same key is `409` `IDEMPOTENCY_KEY_REUSED`. SDKs and the
CLI mint a UUID unless you supply one.

## Custom methods

Canonical AIP-136 form (colon verb):

| Method | Path                             |
| ------ | -------------------------------- |
| POST   | `/v1/notes/{note}:edit`          |
| POST   | `/v1/notes/{note}:move`          |
| POST   | `/v1/notes/{note}:rename`        |
| POST   | `/v1/notes/{note}:rollback`      |
| GET    | `/v1/notes/{note}:listRevisions` |
| POST   | `/v1/writes/{id}:approve`        |
| POST   | `/v1/writes/{id}:reject`         |

Revisions expose AIP-162 `revision_id` / `revision_create_time` and keep
`version_id` as a v1 alias.

Create / PATCH receipts embed `note` when `status = "applied"`. Queued
receipts are `GET /v1/writes/{id}`.

## Deprecations

Legacy slash forms (`/edit`, `/move`, `/rename`, `/restore`, `/approve`,
`/reject`, `/versions`) and `PUT /v1/vault/write-settings` remain through
v1. They answer with `Deprecation: true` and
`Link: <canonical>; rel="successor-version"`. Prefer the colon verbs and
`PATCH` + `update_mask`.
