API
The HTTP surface: authentication, the workspace header, idempotency, errors and OpenAPI.
Base URL and discovery
https://api.fastpad.ai| Path | Serves |
|---|---|
GET /openapi.json | the OpenAPI 3.1 document, strong-ETagged — a polling client costs a 304 |
GET /.well-known/oauth-protected-resource | RFC 9728 protected-resource metadata |
GET /health | liveness plus a database round trip |
POST /mcp | the agent tool surface |
Authenticating
curl https://api.fastpad.ai/v1/notes \
-H 'Authorization: Bearer fpk_live_…' \
-H 'x-fastpad-workspace: 01JB2K…'Everything is authenticated by default. The only routes reachable without a credential are health, the OpenAPI document, the well-known metadata, the two device-login routes, and published notes under /v1/public/.
Requests are rate limited per credential. Each agent has its own per-minute limit, 120 unless it was set otherwise. Each signed-in person may make 1,200 requests a minute, and ask 20 questions a minute on POST /v1/ask, shared across their session and every CLI token they hold. The two device-login routes have no credential to count against, so they are limited per address: 20 starts and 60 polls a minute. A refusal is a 429 with retry_after_ms; wait that long and try again.
x-fastpad-workspace names which of your workspaces to act in. It is not an authorization decision: naming a workspace you are not a member of returns nothing rather than someone else’s notes.
Request shape
- Identifiers are ULIDs — 26 characters, sortable by creation time.
- Timestamps are ISO 8601 with an offset.
- Lists are keyset-paginated; follow
next_cursorrather than counting pages. - Bodies are capped at 64 KiB on public routes, 8 MiB on authenticated routes and
/mcp. A body over 64 KiB must finish arriving within 60 seconds and, after the first 5 seconds, keep up a minimum average rate that scales with its size: 64 KiB a second for a maximal 8 MiB body, proportionally less for a smaller one. A body that falls short gets a retryable 408, whosemin_bytes_per_secsays which floor applied. Large imports are presigned to object storage instead. - No array in a request may hold more than 10,000 elements, and no request more than 50,000 values in all. A 422 lists at most 50
field_errors, andfield_errors_truncatedcounts the rest. - Every mutation takes
idempotencyKey. Send one.
An idempotency key replays the first response verbatim. A body that differs under the same key is a 409, never a quiet replay of the wrong answer.
Errors
Every failure is application/problem+json per RFC 9457, with a stable type URI, retryable, retry_after_ms where it applies, field_errors[] and a docs_url that resolves.
{
"type": "https://fastpad.ai/errors/stale-frontier",
"title": "Stale frontier",
"status": 409,
"retryable": true,
"docs_url": "https://fastpad.ai/errors/stale-frontier"
}Every problem names the next call to make. The full catalog is at fastpad.ai/errors.
Writing a note
Note content is a collaborative document, so a write is a batch of CRDT operations plus the state you computed them against.
POST /v1/notes/{id}/ops
{ "ops": "<base64 Loro update>", "peerId": "…", "expectedFrontier": "fp1:…", "idempotencyKey": "…" }If the note moved since you read it, you get stale-frontier (409) naming both states — re-read and recompute rather than overwriting. The operation log and the queryable projection are written in one transaction, so a write is visible to a search the instant it is committed.
One save is checked before anything imports it. ops must be a Loro update export, never a snapshot, of at most 1 MiB once decoded. It may add at most 100,000 atoms of change the note does not already hold (an atom is one character inserted or deleted), or 200,000 when it is one peer’s edit built on the note’s current version; ops the note already has are not counted. A save past any of these gets a 422 validation-failed that is not retryable and names max_bytes, max_atoms or mode. The same bytes are refused every time, so keep the change, re-read the note, and send it again as smaller updates, each exported from the version the server last acknowledged.