The API

There’s one entrypoint: POST /api/ with a JSON body whose action field names what to do. One endpoint keeps the surface small — there’s no sprawl of routes to discover, one place to authenticate, one place to rate-limit.

Authentication: no password theatre

Two ways in, neither of them a password:

Magic link (people). An editor requests a one-time code by email and exchanges it for a token. No password to choose, forget, reuse, or leak.

POST /api/  { "action": "request_code", "email": "editor@example.com" }
→ { "success": true, "expires": "2026-06-04T12:00:00+00:00" }

POST /api/  { "action": "verify_code", "email": "editor@example.com", "code": "123456" }
→ { "success": true, "token": "eyJ…", "actor": { … } }

Bearer token (systems). For programmatic access, mint a WebAPI key. The 64-character token is shown once, at creation, and carries explicit permissions — a view-only key reads everything and writes nothing.

Authorization: Bearer {token}

Both modes resolve to an actor with a permission set; every action is checked against it.

Calling an action

Most actions take a JSON body. A few read-only ones also accept a GET path, which is handy for a browser or a health check:

GET /api/health
→ { "status": "ok", "space": "demo", "checks": { … } }

A typical write — create, then edit, then publish:

POST /api/
{
  "action": "create",
  "type": "place",
  "path": "fikkie",
  "initial": { "name": "Fikkie" }
}
 { "success": true, "id": "ctxr:a1b2c3d4", "type": "place", "path": "fikkie" }
POST /api/
{ "action": "save", "id": "ctxr:a1b2c3d4", "property": "description", "value": "A walking spot on the Maas." }

POST /api/
{ "action": "approve", "id": "ctxr:a1b2c3d4" }

approve is the moment the compiler runs — references resolve, derived values compute, the cascade fires.

The data is the API

The query action is read-only and returns sanitised live data — no drafts, no internal _-fields, no @build. What comes back is valid JSON-LD: the same structured data a search engine or AI would consume. You don’t serialise a response model; the node is the response.

POST /api/
{ "action": "query", "method": "byType", "type": "place", "fields": "name,geo" }
 { "success": true, "count": 12, "results": [ { "@type": "Place", "@id": "ctxr:a1b2c3", "name": "Fikkie", "geo": {  } } ] }

Query methods: byType, byIdentifier, byAppearsIn, resolve, byPath. The fields parameter projects results to the keys you ask for (@type and @id always included). Because output is JSON-LD by construction, the API is AI-friendly by design — an external system can ask “what do you have about my ID 1234?” and get back a node it already understands.

Hardening

The single endpoint is also the single place to enforce safety:

Control Effect
CORS scoped to the domain family Only the space’s own front-ends may call from a browser
Bearer-only auth No cookies, so no CSRF — a forged cross-site request carries no token
Rate limiting Per-actor and per-IP ceilings blunt abuse and runaway scripts
Typed confirmation Destructive actions require an explicit name match

Errors return an error field with the relevant HTTP status — 400 validation, 401 unauthenticated, 403 permission, 404 not found, 409 conflict, 429 rate limit. Every action is reached through this one endpoint; the build folder guide walks the common developer flows end to end.