Base /api/v1/; all endpoints require Authorization: Bearer TOKEN.
JSON response errors use {"error":"message"}. Statuses: 400 invalid request,
401 authentication, 403 role, 404 unreadable/missing, 409 conflict, 503 storage fault.
Some record ownership failures currently return 400 rather than 403.
| Method | Path | Input / result |
|---|---|---|
| GET | health/ |
Version, authenticated identity, sequence |
| GET / POST | collections/ |
List / {name,schema}; create needs admin |
| GET / POST | documents/COLLECTION/ |
First page / create {data,visibility?,readers?} |
| GET / PUT | documents/COLLECTION/ID/ |
Read / replace full record with current version |
| PATCH | documents/COLLECTION/ID/ |
{version,set:{field:value},unset:[field],inc:{field:step}}; top-level keys |
| DELETE | documents/COLLECTION/ID/ |
If-Match: VERSION; logical tombstone |
| POST | query/COLLECTION/ |
Query below; {results,total,next_cursor?,sequence,plan} |
| POST | transactions/ |
{changes:[{record,expected}]}; atomic batch |
| GET / POST | memories/ |
List/search via kind/source/q/limit, or create record |
| GET / PUT / DELETE | memories/ID/ |
Record CRUD; PUT full record/version; DELETE If-Match |
| GET | memories/ID/history/ |
Owner-only revisions, including tombstones |
| POST | search/ |
{text,vector,embedding_model,agent,kind,source,limit} |
| POST | ingest/ |
Conversation + facts or extract flag; records result |
| POST | jobs/ |
Queue ingestion body; returns durable job |
| POST | jobs/ID/run/ |
Process/retry owned job |
| POST | wiki/ |
{text,source,title}; source-preserving paragraphs |
| POST | graph/ |
{id,reverse,depth,limit} scoped BFS |
| POST | code/index/ |
{repository,branch,commit,files:{path:GoSource}} |
| POST | code/revisions/ |
{repository,branch,commit,parent?,path,before,after,reason} |
| POST | skills/ID/approve/ |
{version}; admin with write permission required |
Idempotency-Key supports durable matching-request deduplication for creates,
transactions and ingestion. Keys are scoped to workspace/user/path and must fit
256 bytes including that prefix. Reusing a key for different contents conflicts.
Use a fresh key for each logical operation. See README for mutation retry limitations.
{"where":[{"field":"age","op":"gte","value":18}],"order_by":"age","descending":true,"projection":["email"],"limit":50}Operators: eq,ne,gt,gte,lt,lte,in,contains,icontains,prefix,iequals,exists. Where
predicates are ANDed. Nested fields use dot paths. Missing and null differ. No
numeric string coercion.
contains/prefix/eq fold no case; icontains and iequals fold ASCII case, which
is what SQLite's LIKE and COLLATE NOCASE do, so ported search and login lookups
keep their behaviour. On arrays contains/icontains test membership, not substring;
to search inside a list store a joined text field and use icontains on it.
Boolean structure, all optional:
{"where":[{"field":"category","op":"eq","value":"Web App"}],
"or":[[{"field":"title","op":"icontains","value":"univorya"}],[{"field":"tags","op":"icontains","value":"rust"}]],
"not":[{"field":"is_demo","op":"eq","value":true}]}or groups are each ANDed and at least one must match, in addition to where.
not rejects any record matching one of its predicates. An or group holds 1..32
predicates and a query holds at most 32 groups and 32 not predicates. Equality
indexes still accelerate where when or/not are present, because those clauses
only narrow the indexed candidate set.
Sorting takes one field as a name, or several as a list that breaks ties left to right:
{"order_by":[{"field":"likes","descending":true},{"field":"views","descending":true},
{"field":"created_at","descending":true}]}Up to 4 terms, each field named once. A missing value sorts after a present one in both
directions. Record id always breaks the final tie, so equal rows never shuffle between
pages. "order_by":"age" with "descending":true is the single-field shorthand and stays
valid; combining descending with a list is rejected as ambiguous.
offset skips rows without a cursor, which is what numbered pages need:
{"order_by":[{"field":"likes","descending":true}],"limit":24,"offset":48}limit is the page size, offset the rows to skip (0..1,000,000). total still counts
the whole filtered set, so a page reports its own rows and the real page count. Offsets
are not snapshots: a write between page fetches can shift or repeat a row. offset and
cursor cannot be combined - use a cursor when results must not move.
Integers preserve JSON precision; arithmetic comparisons accept bounded decimal
representations (128 characters, exponent -308..308). Sorting has a total order
across heterogeneous values. Missing sort fields follow present values. Projection
returns requested dot-path keys in a flat data object. Fetch the full document
before replacing it; saving a projection can otherwise drop unselected fields.
limit max 500; default 50. Pass next_cursor as cursor with exactly the same
query to continue. Mutations or changed active results return 409. total counts
all matching readable documents, so a count is total with limit:1.
{"limit":1,"where":[{"field":"category","op":"eq","value":"Web App"}],
"group_by":["category"],
"aggregate":[{"fn":"sum","field":"views"},{"fn":"max","field":"views","as":"top"}]}fn is sum, avg, min or max; at most 16 per query, each on one dot path.
as names the output key and must match the collection-name pattern; the default is
<fn>_<field> with dots and dashes folded to underscores. Duplicate output names are
rejected. group_by takes up to 8 dot paths and is optional.
Results come back in groups, beside results rather than instead of them. Each row
is {group,count,values}; with no group_by there is one row and group is null.
Aggregation covers every filtered record, never just the page, and ignores limit,
order_by and projection. count is the number of matching records in the group,
including records missing the aggregated field.
sum/avg skip non-numeric values. A group holding no number for that field reports
null, the way SQL SUM returns NULL rather than zero. Exact integer and finite decimal
results are emitted as numbers of full precision, so values past 2^53 survive a sum and
an average like 9007199254741053/4 comes back as 2251799813685263.25. A repeating
decimal falls back to 17 significant digits. min/max use the same total order as
order_by and return the stored value unchanged.
inc adds to numeric fields inside the engine lock and re-reads the live record, so a
page-view or like counter never loses a concurrent write and needs no version. Omitting
version on a plain set still conflicts. Steps and the stored value must be numbers
without string coercion; adding onto text is rejected, and a field the record lacks starts
at zero. Negative steps decrement. set, unset and inc may be combined in one PATCH
and land as a single revision. Results keep exact integer precision.
projects.increment(pk=pid, views=1)
projects.update(pk=pid, inc={"likes": -1}, stage="Beta")Python clients use Sum/Avg/Min/Max:
projects.aggregate(total=Sum("views"), best=Max("views")).groups()
projects.group_by("category").aggregate(Sum("views")).groups()Schema: {fields:{name:{type:"string",required:true}},unique:["name"],indexes:["name"],strict:false}.
Types: any,string,number,integer,boolean,object,array. Strict checks top-level
undeclared keys; it is not recursive JSON Schema. Unique/index paths need field
declarations. Missing/null values do not participate in uniqueness. A string field may
set casefold:true, which makes its unique index ignore ASCII case the way SQLite's
UNIQUE COLLATE NOCASE does, so Priya@x.com and priya@x.com are one identity;
casefold on any other type is rejected. It affects uniqueness only - eq stays
case-exact and iequals is the folding matcher. Adding casefold re-canonicalises the
field, so a collection already holding two casings of one value cannot adopt it until
that data is resolved. Collection changes can be submitted as versioned admin
transactions and validate existing data.
A collection with live documents cannot be deleted. Unknown collections query as
empty; inserts require an existing definition.
{"messages":[{"role":"user","content":"I prefer Hindi"}],"facts":[{"subject":"user","predicate":"language","value":"Hindi","quote":"I prefer Hindi","message_index":0,"confidence":1}],"source":"example","context":"general"}extract:true replaces supplied facts with provider extraction. Both together are
rejected. Roles: user/assistant/system/tool. Facts require a literal quote from a
user message and confidence 0.5..1. A fact key is subject+predicate+context within
the owner/workspace. New values supersede older matching facts without erasing
history. A summary is an extractive excerpt, not generated profile reasoning.
Optional tools:[{name,input,output,success}] stores unreviewed experiences.
Memory records support kinds chat,fact,summary,profile,intention,experience,skill, wiki,code,revision; internal collection/document/job kinds also exist. Optional fields include source,layer,parent,links,metadata (string values),vector, embedding_model,expires (RFC3339),visibility,readers,agents. Links must resolve to active records readable by the record owner at write time. Deletion can leave historical dangling links; traversal filters unreadable/inactive targets.