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.