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).
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.