Quickstart
Python
No third-party packages: urllib.request for ordinary calls, http.client to read the subscribe stream incrementally. Python 3.9+.
import json, urllib.request, urllib.error, http.client
API = "http://127.0.0.1:7001/v1"
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(API + path, data=data, method=method,
headers={"content-type": "application/json"} if data else {})
try:
with urllib.request.urlopen(req) as res:
return json.load(res) if res.status != 204 else None
except urllib.error.HTTPError as e:
err = json.load(e)["error"] # uniform shape: {code, message, details?}
raise RuntimeError(f"{e.code} {err['code']}: {err['message']}") from None
1. Create a space
space = call("POST", "/spaces", {"name": "Notebook"})
SPACE = space["id"] # "bafyreig…"
2. Create an object
obj = call("POST", f"/spaces/{SPACE}/objects", {
"types": ["page"],
"initialProperties": {"any": {"name": "Reading list"}},
})
OBJ = obj["objectId"]
3. Query
page = call("POST", f"/spaces/{SPACE}/objects/query", {
"filter": {"any.types": "page"},
"sort": ["-modifiedAt"],
"limit": 20,
"includeTotal": True,
})
print(page["total"], [r["any"]["name"] for r in page["records"]])
from datetime import datetime
modified = datetime.fromisoformat(page["records"][0]["modifiedAt"]["$date"].replace("Z", "+00:00"))
Filters that compare a timestamp must use the same wrapper: {"modifiedAt": {"$gte": {"$date": "2026-08-01T00:00:00Z"}}}. A bare string does not error — it compares by type rank and matches everything or nothing (Data types).
4. Subscribe
http.client gives a response object whose readline() blocks until the next line arrives, which is all an SSE parser needs.
def sse(path, body):
conn = http.client.HTTPConnection("127.0.0.1", 7001)
conn.request("POST", "/v1" + path, body=json.dumps(body),
headers={"content-type": "application/json", "accept": "text/event-stream"})
res = conn.getresponse()
if res.status != 200:
raise RuntimeError(json.load(res)["error"]["code"])
event, data = "message", []
for raw in res: # one line per iteration
line = raw.decode().rstrip("\n")
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
data.append(line[5:].strip())
elif line == "": # blank line ends a frame
if data:
yield event, json.loads("".join(data))
event, data = "message", []
# lines starting with ":" are keepalive comments — ignored
window = {} # id → record
for event, data in sse(f"/spaces/{SPACE}/objects/query/subscribe",
{"filter": {"any.types": "page"}, "sort": ["-modifiedAt"], "limit": 20}):
if event == "ready":
continue
if event == "snapshot":
window = {r["id"]: r for r in data["records"]}
elif event == "changes":
for ch in data:
for r in ch["added"] + ch["updated"]:
window[r["id"]] = r["doc"]
for r in ch["removed"]:
window.pop(r["id"], None)
elif event == "closed":
print("closed:", data["reason"]) # reopen for a fresh snapshot
break
print(sorted(r["any"]["name"] for r in window.values()))
Rename the object from another shell and the loop prints the new name:
curl -s -X POST http://127.0.0.1:7001/v1/spaces/$SPACE/properties/$OBJ/set/any \
-H 'content-type: application/json' -d '{"name":"Reading list 2026"}'
Per-object datasets
The same body shape reads any dataset of one object — chat messages, editor blocks, a runtime dataset you declared — via the sibling per-object path:
msgs = call("POST", f"/spaces/{SPACE}/query", {
"objectId": CHAT, "dataset": "chat_messages",
"sort": ["-_ver.id"], "limit": 50,
})
Page history with an absolute cursor rather than offset, which floats under writes:
older = call("POST", f"/spaces/{SPACE}/query", {
"objectId": CHAT, "dataset": "chat_messages",
"sort": ["-_ver.id"], "limit": 50,
"filter": {"_ver.id": {"$lt": msgs["records"][-1]["_ver"]["id"]}},
})
Writes
Writes return the change, not the record:
r = call("POST", f"/spaces/{SPACE}/objects/{CHAT}/chat/messages", {"text": "hello"})
r["recordIds"][0] # the new message id
r["versionId"] # stamp it to recognise your own delta
Note. Keep the subscription loop on its own thread or task;
resblocks on the socket between frames, and the server sends a: keepalivecomment every 25 s during silence so an idle stream never looks dead.
Next: anyrt runs this kind of code inside the database as a sandboxed program.