This page walks through the full lifecycle of a document: create → read → search → update → delete. It explains the two concepts that trip people up most: ULID addressing and ETag-guarded updates.
Setup
You need an API key and curl. See API keys.
export TUPSHAR_HOST="https://api.tupshar.housecarl.cloud"
export TUPSHAR_KEY="tupk_…"
Step 1: Create a document
POST /v1/file with a JSON body. filename and contents are required; tags and properties are optional.
curl -sS -X POST "$TUPSHAR_HOST/v1/file" \
-H "Authorization: Bearer $TUPSHAR_KEY" \
-H "Content-Type: application/json" \
-d '{
"filename": "notes/onboarding.md",
"contents": "Welcome to the team. Read the handbook before your first standup.",
"tags": ["onboarding", "hr"],
"properties": {"team": "engineering"}
}'
201 Created:
{
"id": "01JZ8K3M9QF2YV7X4B6N0CWE5T",
"filename": "notes/onboarding.md",
"tags": ["onboarding", "hr"],
"properties": {"team": "engineering"},
"links": [],
"version": 1,
"etag": "\"sha256:9f2b…|1|1789…\"",
"created_at": "2026-06-12T21:00:00Z",
"updated_at": "2026-06-12T21:00:00Z",
"contents": "Welcome to the team. Read the handbook before your first standup."
}
The id is how you address this document. The filename is searchable metadata — useful, but not the address. Copy the id and etag; you'll need both.
Step 2: Read the document
DOC_ID="01JZ8K3M9QF2YV7X4B6N0CWE5T"
curl -sS "$TUPSHAR_HOST/v1/file/$DOC_ID" \
-H "Authorization: Bearer $TUPSHAR_KEY"
200 OK returns the same shape as the create response. To fetch metadata only (no contents):
curl -sS "$TUPSHAR_HOST/v1/file/$DOC_ID?include=metadata" \
-H "Authorization: Bearer $TUPSHAR_KEY"
If you already have the ETag and want to avoid a full download when nothing has changed:
curl -sS "$TUPSHAR_HOST/v1/file/$DOC_ID" \
-H "Authorization: Bearer $TUPSHAR_KEY" \
-H 'If-None-Match: "sha256:9f2b…|1|1789…"'
# → 304 Not Modified if unchanged
Step 3: Search
GET /v1/query — always GET, always query-string params, never a POST body.
Full-text search
curl -sS -G "$TUPSHAR_HOST/v1/query" \
-H "Authorization: Bearer $TUPSHAR_KEY" \
--data-urlencode "token=handbook standup"
Terms are OR-matched and BM25-ranked. Up to 32 terms per query. No phrase, AND, or negation syntax in this preview.
{
"mode": "token",
"results": [
{
"id": "01JZ8K3M9QF2YV7X4B6N0CWE5T",
"filename": "notes/onboarding.md",
"tags": ["onboarding", "hr"],
"updated_at": "2026-06-12T21:00:00Z",
"score": 3.17
}
],
"limit": 25,
"total_estimate": 1,
"next_cursor": null
}
Search results return the id but not contents — use GET /v1/file/{id} to fetch the full document.
Tag and property search
# All documents tagged "onboarding"
curl -sS -G "$TUPSHAR_HOST/v1/query" \
-H "Authorization: Bearer $TUPSHAR_KEY" \
--data-urlencode "tag=onboarding"
# Documents where team=engineering
curl -sS -G "$TUPSHAR_HOST/v1/query" \
-H "Authorization: Bearer $TUPSHAR_KEY" \
--data-urlencode "property=team=engineering"
Step 4: Update (with If-Match)
Updates use optimistic concurrency control. You must send the current etag in If-Match; the server rejects stale writes.
DOC_ETAG='"sha256:9f2b…|1|1789…"' # from the create or read response
curl -sS -X PUT "$TUPSHAR_HOST/v1/file/$DOC_ID" \
-H "Authorization: Bearer $TUPSHAR_KEY" \
-H "Content-Type: application/json" \
-H "If-Match: $DOC_ETAG" \
-d '{
"filename": "notes/onboarding.md",
"contents": "Welcome to the team. Read the handbook and schedule your onboarding week."
}'
200 OK — the response contains a new etag and an incremented version:
{
"id": "01JZ8K3M9QF2YV7X4B6N0CWE5T",
"version": 2,
"etag": "\"sha256:c41a…|2|1789…\""
}
If you omit If-Match: 428 Precondition Required — the server refuses to update without a guard.
If your ETag is stale (someone else updated first): 412 Precondition Failed — re-read the document, get its fresh ETag, and retry.
Step 5: Delete
curl -sS -X DELETE "$TUPSHAR_HOST/v1/file/$DOC_ID" \
-H "Authorization: Bearer $TUPSHAR_KEY"
204 No Content. Deleting an already-deleted document also returns 204 — the operation is idempotent. Pass ?cascade=true to also remove inbound links from other documents that pointed at this one.
Key concepts recap
| Concept | Detail |
|---|---|
| ULID | Server-assigned ID (e.g. 01JZ8K3M9QF2YV7X4B6N0CWE5T). The document's address. Filename is searchable metadata, not the address. |
| ETag | An opaque version fingerprint returned on every write. Pass it in If-Match on the next update. |
| If-Match rule | Always required for PUT. Missing → 428. Stale → 412. |
| Search | Always GET /v1/query with query-string params. Results contain id and metadata, not contents. |
What's next?
- HTTP API — complete endpoint reference with all parameters