← The Karsa Manual

The Karsa API

You don't have to memorize a big endpoint list. Karsa's API is one JSON-RPC transport, uniform and generated: the moment you expose a collection, it gets full CRUD operations, a schema, and CRUD events — automatically. Every call is the same shape:

POST ${KARSA_API_BASE}/rpc
{ "method": "karsa/{resource}/{verb}", "params": { … } }
→ { "result": … }        on success
→ { "error": { "code", "message } }   on failure

$KARSA_API_BASE is already project-scoped — never hard-code a host or /api/.... When you're an agent, it also carries your token, so every call you make through it is you: keep using it as is.

Who a call is: sessions and tokens

Every call is someone, checked against what they hold (spec_undefinedMundefinedNundefinedKWSundefinedGX):

Params are checked before anything runs. An operation that declares an input schema (the same schema MCP tools/list shows) refuses unknown fields, wrong types and over-long values with code: "invalid" and data.errors: [{path, message}], so fix the named field and retry. Collection writes are checked against the collection's schema the same way.

The uniform CRUD pattern

For any collection {c} (its slug — spec, task, deal, …), five operations exist automatically:

karsa/{c}/list      params: optional { q }                 → array of resources
karsa/{c}/get       params: { id }                        → one resource
karsa/{c}/create    params: { frontmatter, body? }        → the created resource
karsa/{c}/update    params: { id, patch, body? }          → the updated resource
karsa/{c}/delete    params: { id }                        → { ok }

Filter, sort and limit any list: q

Every list takes q, a small SQL-ish query over the resource's fields (its schema: karsa/type/get):

karsa/exec/list {"q": "status = 'failed' and started_at > -1d order by started_at desc limit 20"}
karsa/task/list {"q": "title contains 'invoice' and status in ('open', 'blocked')"}
karsa/exec/list {"q": "fn_name = 'fetch_rates' and input.base = 'USD'"}

[where] <condition> [order by <field> [asc|desc], …] [limit <n>]. Conditions: =, !=, <, <=, >, >=, contains (case-insensitive), like 'pre%', in (…), is [not] null, with and, or, not and parentheses. Values: 'quoted' strings (or bare words), numbers, true/false/null, and times relative to now: -15m, -1h, -7d, -2w. Dotted fields reach inside objects (input.base). A query that doesn't parse is refused (invalid) with where it went wrong. The run list (exec) reads newest first and stops once it has enough matches, so filtering a long history stays fast.

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 |

The four provenance fields are _-prefixed because they're server-owned — the server derives them and strips any you send. _created_by/_updated_by is the acting user (user_…, e.g. an agent user) or user for a direct write — a fn's write carries the fn's id (or the user it runs as). That's what a subscriber's _updated_by: { $ne: "$self" } filter keys on: $self is the subscriber, its fn, and the user it runs as (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:

karsa/type/list                → all types in this project
karsa/type/get  { id: 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   — karsa/{c}/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.

Beyond plain CRUD

Still the same rpc transport, different methods:

The exceptions — not JSON-RPC by nature (the only non-rpc endpoints):

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}/rpc { "method": "karsa/secret/create", "params": { "frontmatter": { "title": "Resend API key", "description": "Send deal-flow notification emails.", "policy": { "via": "http", "allow": [ { "host": "api.resend.com" } ] } } } } → { "result": { "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 karsa/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}/rpc { "method": "karsa/egress/create", "params": { "method": "POST", "url": "https://api.resend.com/emails", "headers": { "Authorization": "Bearer {{secret_…}}" }, "body": { … } } } ` or for a tool: { "method": "karsa/tool-run/create", "params": { "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). karsa/secret/list 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 operation 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 (it catalogs the rpc operations). But for everyday work: the uniform CRUD pattern over the one rpc transport is the whole game.