Tupshar exposes a small REST API over HTTPS. The base URL for the research preview is:
https://api.tupshar.housecarl.cloud
Every /v1/* request must carry an API key as a Bearer token:
Authorization: Bearer tupk_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
See API keys for how to get one. Examples below assume:
export TUPSHAR_HOST="https://api.tupshar.housecarl.cloud"
export TUPSHAR_KEY="tupk_…"
Documents are identified by a server-assigned ULID (e.g. 01JZ8K3M9QF2YV7X4B6N0CWE5T), not by filename. The filename is metadata you can search on; the ULID is the address.
Create a document — POST /v1/file
Send a JSON body. filename and contents are required; tags, properties, and links are optional.
curl -sS -X POST "$TUPSHAR_HOST/v1/file" \
-H "Authorization: Bearer $TUPSHAR_KEY" \
-H "Content-Type: application/json" \
-d '{
"filename": "notes/planning.md",
"contents": "Quarterly planning. Owners: Paul, Dana. Review in two weeks.",
"tags": ["meeting", "planning"],
"properties": {"project": "tupshar"}
}'
201 Created returns the full document metadata (and contents):
{
"id": "01JZ8K3M9QF2YV7X4B6N0CWE5T",
"filename": "notes/planning.md",
"tags": ["meeting", "planning"],
"properties": {"project": "tupshar"},
"links": [],
"size_bytes": 60,
"token_count": 9,
"unique_terms": 9,
"sha256": "9f2b…",
"analyzer_version": "v1",
"version": 1,
"created_at": "2026-06-12T21:00:00Z",
"updated_at": "2026-06-12T21:00:00Z",
"accessed_at": "2026-06-12T21:00:00Z",
"etag": "\"sha256:9f2b…|1|1789…\"",
"contents": "Quarterly planning. Owners: Paul, Dana. Review in two weeks."
}
Pass Idempotency-Key: <uuid> to make a retried create safe. Save the id — it addresses the document for every other call.
Read a document — GET /v1/file/{id}
curl -sS "$TUPSHAR_HOST/v1/file/01JZ8K3M9QF2YV7X4B6N0CWE5T" \
-H "Authorization: Bearer $TUPSHAR_KEY"
200 OK returns the same shape as create. Options:
?include=metadata— return metadata only, omitcontents.If-None-Match: "<etag>"— return304 Not Modifiedif the document hasn't changed (the response always carries the currentETag).
HEAD /v1/file/{id} returns the same headers (ETag, Last-Modified, Content-Length) with an empty body — useful for an existence/version check.
Replace a document — PUT /v1/file/{id}
Updates are optimistic-concurrency controlled. You must send the current ETag in If-Match; without it you get 428 Precondition Required, and if it's stale you get 412 Precondition Failed.
curl -sS -X PUT "$TUPSHAR_HOST/v1/file/01JZ8K3M9QF2YV7X4B6N0CWE5T" \
-H "Authorization: Bearer $TUPSHAR_KEY" \
-H "Content-Type: application/json" \
-H 'If-Match: "sha256:9f2b…|1|1789…"' \
-d '{"filename": "notes/planning.md", "contents": "Revised plan. Review next Friday."}'
200 OK returns the new metadata with an incremented version and a new etag.
Delete a document — DELETE /v1/file/{id}
curl -sS -X DELETE "$TUPSHAR_HOST/v1/file/01JZ8K3M9QF2YV7X4B6N0CWE5T?cascade=true" \
-H "Authorization: Bearer $TUPSHAR_KEY"
204 No Content. Idempotent — deleting an absent document still returns 204. ?cascade=true also removes inbound links from other documents that pointed at this one.
Search — GET /v1/query
One endpoint, four modes selected by which parameter you pass. All are GET with query-string parameters (never a POST body). Default page size is 25, max 100; next_cursor is reserved (currently always null).
Full-text — ?token=
BM25-ranked search over document contents. Terms are normalized by the analyzer and OR-matched (a document matching any term is returned, ranked by summed BM25 score). Up to 32 terms; more returns 422 too_many_terms. There is no phrase, AND, or negation syntax in this preview.
curl -sS -G "$TUPSHAR_HOST/v1/query" \
-H "Authorization: Bearer $TUPSHAR_KEY" \
--data-urlencode "token=planning review"
{
"mode": "token",
"results": [
{"id": "01JZ8K…", "filename": "notes/planning.md", "tags": ["meeting","planning"], "updated_at": "2026-06-12T21:00:00Z", "score": 3.42}
],
"limit": 25,
"total_estimate": 1,
"next_cursor": null
}
Filename — ?filename= with ?match=
match is exact (default), prefix, or glob. Glob supports */?; the metacharacters []{}|\()^$+ are rejected with 422 unsupported_glob_metachar.
curl -sS -G "$TUPSHAR_HOST/v1/query" -H "Authorization: Bearer $TUPSHAR_KEY" \
--data-urlencode "filename=notes/" --data-urlencode "match=prefix"
Tag — ?tag=
Case-insensitive exact tag match. Order with ?sort=updated (default) or ?sort=accessed.
curl -sS -G "$TUPSHAR_HOST/v1/query" -H "Authorization: Bearer $TUPSHAR_KEY" \
--data-urlencode "tag=planning"
Property — ?property=
?property=key matches documents that have the property; ?property=key=value matches an exact value.
curl -sS -G "$TUPSHAR_HOST/v1/query" -H "Authorization: Bearer $TUPSHAR_KEY" \
--data-urlencode "property=project=tupshar"
Filename, tag, and property results omit score; otherwise the response envelope is the same (mode, results, limit, next_cursor).
Backlinks — GET /v1/query/backlinks
Discover inbound edges: which documents link to a given document?
curl -sS -G "$TUPSHAR_HOST/v1/query/backlinks" \
-H "Authorization: Bearer $TUPSHAR_KEY" \
--data-urlencode "id=01JZ8K3M9QF2YV7X4B6N0CWE5T"
Optional parameters: rel (filter by relation type), limit (default 100, max 500).
{
"target_id": "01JZ8K3M9QF2YV7X4B6N0CWE5T",
"backlinks": [
{"source_id": "01JZ9M…", "rel": "references", "dangling": false},
{"source_id": "01JZA1…", "rel": "supersedes", "dangling": false}
]
}
Owner-scoped: the target file must belong to the calling key's tenant.
Compound query — POST /v1/query/compound
Intersect tag, property, and full-text token filters in a single query. At least one filter is required; all supplied filters are AND-combined.
curl -sS -X POST "$TUPSHAR_HOST/v1/query/compound" \
-H "Authorization: Bearer $TUPSHAR_KEY" \
-H "Content-Type: application/json" \
-d '{
"tag": "decision",
"property": "project=tupshar",
"token": "migration rollback",
"sort": "updated",
"limit": 10
}'
All fields are optional, but at least one of tag, property, or token must be present (otherwise 400).
{
"mode": "compound",
"filters": {"tag": "decision", "property": "project=tupshar", "token": "migration rollback"},
"results": [
{"id": "01JZ8K…", "filename": "decisions/db-migration.md", "tags": ["decision"], "properties": {"project": "tupshar"}, "updated_at": "2026-06-12T21:00:00Z", "score": 4.12}
],
"limit": 10
}
score is present only when a token filter is active. property uses key (existence) or key=value (exact match) syntax.
Graph traversal — POST /v1/query/traverse
BFS walk from a starting document, following typed links up to N hops. Returns all reachable documents with their depth and the relation that reached them.
curl -sS -X POST "$TUPSHAR_HOST/v1/query/traverse" \
-H "Authorization: Bearer $TUPSHAR_KEY" \
-H "Content-Type: application/json" \
-d '{
"start_id": "01JZ8K3M9QF2YV7X4B6N0CWE5T",
"direction": "outbound",
"rels": ["references", "derived_from"],
"max_depth": 3,
"max_results": 50
}'
| Field | Default | Max | Notes |
|---|---|---|---|
start_id | — | — | Required. ULID of the starting document. |
direction | outbound | — | outbound follows links away from start; inbound follows links pointing at start. |
rels | all | — | Filter to specific relation types. Omit to follow all. |
max_depth | 3 | 10 | Maximum BFS hops from the start node. |
max_results | 100 | 500 | Stop after collecting this many nodes. |
{
"mode": "traverse",
"start_id": "01JZ8K3M9QF2YV7X4B6N0CWE5T",
"direction": "outbound",
"max_depth": 3,
"results": [
{"id": "01JZ9M…", "depth": 1, "rel": "references"},
{"id": "01JZA1…", "depth": 2, "rel": "derived_from"}
]
}
Owner-scoped: the start node and all traversed neighbors must belong to the calling key's tenant. Cross-tenant edges are silently skipped. BFS is bounded by a visited-set cap of 1000 nodes and a per-node fanout cap of 128 edges to prevent resource exhaustion.
File operations — POST /v1/file/{id}/invoke
Body: {"action": "<name>", "params": {…}}. Available actions:
| Action | Scope | Effect |
|---|---|---|
stats | read | Live token statistics for the document |
score | read | Per-term BM25 breakdown |
retokenize | write | Re-run the analyzer and refresh the full-text index |
set_properties | write | Merge/replace entries in the property cloud |
add_link | write | Add a typed link to another document |
remove_link | write | Remove a link |
summary, links_graph, and diff are reserved and return 501 with code: "reserved_for_future_version".
Links
Links are typed and directional. rel is one of references (default), supersedes, derived_from, related. A document may have up to 256 outbound links.
curl -sS -X POST "$TUPSHAR_HOST/v1/file/01JZ8K…/invoke" \
-H "Authorization: Bearer $TUPSHAR_KEY" -H "Content-Type: application/json" \
-d '{"action": "add_link", "params": {"target_id": "01JZ9M…", "rel": "supersedes"}}'
List your documents — GET /httpserver/files
Metadata-only listing of the documents owned by the calling key. Params: page (default 0), limit (default 50, max 500), include.
curl -sS -G "$TUPSHAR_HOST/httpserver/files" -H "Authorization: Bearer $TUPSHAR_KEY" \
--data-urlencode "page=0" --data-urlencode "limit=50"
{"files": [ /* FileMetadata rows */ ], "pagination": {"page": 0, "limit": 50, "offset": 0}}
Health
GET /healthz→200 {"status":"ok"}whenever the process is alive (liveness).GET /readyz→200 {"status":"ready"}once the database is connected,503 {"status":"starting"}before then (readiness).
These are unauthenticated. Prometheus metrics live on a separate port — see Monitoring.
Errors
Every error uses one envelope and a documented code. See the full table in Errors:
{"error": {"code": "precondition_required", "message": "If-Match header is required for updates", "request_id": null}}
See also
- Quotas & limits — size caps, rate limits, page sizes.
- MCP integration — the same operations as tools for Claude and other AI clients.