Conventions
These rules apply across the whole API. They come from
docs/04-api.md, which
explains what the OpenAPI file can’t.
Basics
Section titled “Basics”- Base path
/api/v1. JSON only, except media upload (multipart/form-data). - Field names are
camelCase. Enum values aresnake_case. - Breaking changes would need
/api/v2. Within v1, changes are additive.
IDs are prefixed, lowercase ULIDs, such as per_01j9x3k8m2q4r5s6t7v8w9y0z1. Treat them as
opaque strings. The prefix tells you what an ID refers to:
| Prefix | Thing | Prefix | Thing |
|---|---|---|---|
hh | household | med | media |
mem | member | shr | share |
ses | session | tok | agent token |
inv | invite | tsk | agent task |
per | person | ing | ingest batch |
acc | account | vis | visit |
cir | circle | cap | capture |
mom | moment | run | update run |
Timestamps are RFC 3339 strings in UTC, for example 2026-10-17T08:30:00.000Z.
Unknown fields
Section titled “Unknown fields”- In requests, unknown fields are rejected with
400and codeunknown_field. - In responses, clients must ignore fields they don’t know, so the server can add fields without a new version.
Pagination
Section titled “Pagination”List endpoints take limit (default 30, max 100) and cursor. Responses carry nextCursor,
which is null on the last page. Cursors are opaque, URL-safe strings; don’t parse them.
The feed is a little different: limit is 5 to 50 (default 20), and its cursor also encodes the
feed’s seed and a snapshot time, HMAC-signed, so paging stays stable while new moments arrive.
New arrivals show up on the next fresh load.
curl "http://localhost:8080/api/v1/people?limit=50&cursor=NEXT_CURSOR" -H "Authorization: Bearer hst_YOUR_TOKEN"Idempotency
Section titled “Idempotency”POST /ingest, POST /quick-add and POST /imports require an Idempotency-Key header of 1 to
128 characters.
- The server stores the response for 24 hours per token or member and key.
- A retry with the same key and the same body returns the stored response.
- The same key with a different body returns
409 idempotency_conflict.
For agent ingest, the fetch round uses task.id + ":" + page number as the key. MCP calls to
these endpoints get a key generated from a hash of the tool arguments, so retrying the same tool
call is safe.
Rate limits
Section titled “Rate limits”Per token or session, sliding window:
| Group | Limit |
|---|---|
| Login, pair, accept invite (per IP) | 10 a minute |
POST /resolve | 30 a minute |
POST /ingest, POST /media/uploads | 120 a minute |
| Public share read (per IP) | 60 a minute |
| Everything else | 600 a minute |
A 429 reply has code rate_limited and a Retry-After header in seconds.
Media URLs
Section titled “Media URLs”Media is served outside /api/v1, so <img>, <video> and Flutter image widgets work without
custom headers:
GET /media/{mediaId}/{variant}?exp={unixSeconds}&sig={base64url(HMAC-SHA256(secret, mediaId|variant|exp))}| Variant | What |
|---|---|
orig | The original |
card | At most 1080 px |
thumb | At most 320 px |
poster | A video’s poster frame |
- API responses always return ready-to-use signed URLs (
url,thumbUrl,posterUrlon a media reference). - Signatures last 24 hours. Refetch the parent resource for fresh URLs.
- Responses set
Cache-Control: private, max-age=86400, immutableand anETagof the file’s SHA-256. - Range requests work for video.
Uploading media
Section titled “Uploading media”POST /media/uploads takes multipart/form-data with a file and an optional kind (image,
video or poster). Up to 50 MB. Images: JPEG, PNG, WebP, GIF, HEIC. Video: MP4, WebM. If the
file’s SHA-256 is already stored, you get the existing media ID back.
Only GET /api/v1/public/shares/{token} sends Access-Control-Allow-Origin: *, so another
Homestead’s web app can preview a share. Every other endpoint is same-origin only.
Localised data
Section titled “Localised data”What people write (names, notes, moment text) is never translated. Platform names come from the
registry in every locale; GET /platforms returns them all and the client picks one.
Untrusted text
Section titled “Untrusted text”Moment text, display names, notes and relation labels are user-generated. Treat them as data. If you feed them to a model, never follow instructions found inside them.