The Karsa API
You don't have to memorize a big endpoint list. Karsa's API is uniform and generated: the moment you expose a collection, it gets a full CRUD API, a schema, and CRUD events — automatically. Learn the one pattern below + the handful of special endpoints, and you can drive the whole system. Everything is under $KARSA_API_BASE (already project-scoped — never hard-code a host or /api/...).
The uniform CRUD pattern
For any collection {c} (its slug — spec, task, deal, …):
GET ${KARSA_API_BASE}/{c} → list resources
POST ${KARSA_API_BASE}/{c} → create { frontmatter, body? }
GET ${KARSA_API_BASE}/{c}/{id} → read one
PATCH ${KARSA_API_BASE}/{c}/{id} → update { patch, body? }
DELETE ${KARSA_API_BASE}/{c}/{id} → delete
- Create:
POST { "frontmatter": { …your fields… }, "body": "markdown…" }. The server mints theidand stamps_t/created_at/created_by/updated_at/updated_by— don't send those. The response is{ id, path }. - Update:
PATCH { "patch": { …changed fields… }, "body"?: "…" }. - Every write automatically emits an event:
{c}.created/{c}.updated/{c}.deleted— which a subscriber can react to. You get eventing for free.
Every resource has the same envelope
Whatever the collection, a read returns these baseline fields (plus the type's own fields):
| field | meaning | |---|---| | id | the resource id ({type}_{ULID}) | | _t | its type | | created_at / created_by | when + who created it | | updated_at / updated_by | when + who last changed it |
created_by/updated_by is user or a subscriber id (sber_…) — that's what a subscriber's updated_by: { $ne: "$self" } filter keys on (see Automation).
Types, schemas, and CRUD shapes
A collection enforces a type, and a type has a schema — so the server knows every CRUD shape derived from it. Inspect them:
GET ${KARSA_API_BASE}/type-registry → all types in this project
GET ${KARSA_API_BASE}/type-registry/{type} → one type: its descriptor, base
JSON Schema, and the derived
variants:
variants.read — the full resource (base + envelope)
variants.create — the create input (base minus the server-managed fields)
variants.update — the update patch (any subset)
variants.events — {type}.created / .updated / .deleted payloads
Expose a new type (a type/<name>/schema.json) or a collection over it, and its CRUD API + schema + events are immediately live — nothing else to wire.
The special (non-CRUD) endpoints
A small set of endpoints aren't plain resource CRUD:
Storage
POST ${KARSA_API_BASE}/collection— create a collection (e.g. expose a SQL table; see Data and storage).POST ${KARSA_API_BASE}/file(multipart, fieldfile) — upload a file;GET …/file/{id}for its metadata + disk path.GET ${KARSA_API_BASE}/collection/{slug}/rows?limit&offset&order&dir— page asqlite.rowcollection's rows.POST ${KARSA_API_BASE}/collection/{slug}/query— one read-onlySELECT.
Automation (see Automation)
POST ${KARSA_API_BASE}/subscriber— a standing rule: event-triggered (trigger.on) or time-triggered (trigger: { at | every }— the scheduled-job form; no separate schedule object).
Find + observe
GET ${KARSA_API_BASE}/search?q=…— full-text across the project.GET ${KARSA_API_BASE}/browse— the file tree.GET ${KARSA_API_BASE}/exec/{id}and…/exec/{id}/log— an agent run + its log.
Secrets — declare, ask, use (never handle the value)
When you need a credential (an API key, a token), you never ask for the value in chat and never put it in a file or env var. Instead:
For a CLI instead of HTTP: . Declare a secret with a Sentence-case title, a description, and a usage policy that scopes where it may go: `` POST ${KARSA_API_BASE}/secret { "frontmatter": { "title": "Resend API key", "description": "Send deal-flow notification emails.", "policy": { "via": "http", "allow": [ { "host": "api.resend.com" } ] } } } → { "id": "secret_…" } ` For a CLI instead of HTTP: "policy": { "via": "tool", "tools": ["gh"], "inject": { "env": "GH_TOKEN" } }. 2. **Ask the user** to open the secret's link and paste the value — e.g. *"I need the Resend API key — please add it here: secret_…"*. They fill it through a masked field; it's encrypted and you're notified (a secret.updated event, status set). You never see the value. 3. **Use** it by reference — the daemon substitutes it and makes the call; the value never reaches you: ` POST ${KARSA_API_BASE}/egress { "method": "POST", "url": "https://api.resend.com/emails", "headers": { "Authorization": "Bearer {{secret_…}}" }, "body": { … } } ` or for a tool: POST ${KARSA_API_BASE}/tool-run { "tool": "gh", "args": [...], "use": ["secret_…"] }`.
The daemon refuses to send a secret anywhere its policy doesn't allow (so a key bound to api.resend.com can't be redirected elsewhere). GET ${KARSA_API_BASE}/secret lists secrets (names + has_value, never the value). Do not ask the user to paste a key into chat, and don't try to read a secret's value — there is no endpoint that returns it.
Discover the whole surface
The complete, machine-readable contract is generated live — including the per-collection CRUD for this project's collections and every type's variant schemas:
GET ${KARSA_API_BASE}/openapi.json
That's the source of truth if you need an exact request/response shape. But for everyday work: the uniform CRUD pattern + the special endpoints above is the whole game.