Files
obsidian-foundry-sync/docs/relay-api.md
2026-06-20 19:15:38 +00:00

3.5 KiB

ThreeHats foundryvtt-rest-api-relay — API shapes

Source: github.com/ThreeHats/foundryvtt-rest-api-relay (Go relay in go-relay/; docs at foundryrestapi.com). Confirmed against route definitions + integration tests. Our instance: vtt-relay.damascusfront.net (a self-hosted ThreeHats relay).

Global mechanics

  • Base URL: no /api prefix. Paths appended directly to the base.
  • Auth: x-api-key: <key> header on every request.
  • World selection: clientId query parameter (NOT a header). Omit only when exactly one Foundry client is connected to the key (relay auto-resolves); with 0 → 404 {"error":"No connected Foundry clients found"}, with >1 → 400 listing clients.
  • Pass-through: the relay forwards params to the Foundry module over WebSocket and returns the module's response verbatim. Envelopes are module-defined (per-endpoint).
  • Relay-level errors: flat {"error": "..."} with non-2xx status. Timeouts → 408/504 {"error":"Request timed out"}.

Endpoints (the ones we use)

Endpoint Method Path Identifies via Body 200 envelope
get GET /get ?uuid= { data: <doc> }
update PUT /update ?uuid= { data: <diff> } { entity: [<doc>, ...] }
create POST /create body entityType+data { entityType, data } { uuid, data: <doc> }
search GET /search ?filter=documentType:JournalEntry (omit query to list all) { query?, results: [...] }

GET /get

GET /get?clientId=<id>&uuid=JournalEntry.<id>
x-api-key: <key>
→ 200 { data: { name, type, _id, uuid, folder, pages, ownership, flags, ... } }

PUT /update (scope: entity:write)

data is a diff — dot-path keys merge (e.g. "flags.campaign-codex" updates only that sub-flag, preserving other flags); full keys replace. No entityType field (the uuid carries the type).

PUT /update?clientId=<id>&uuid=JournalEntry.<id>
x-api-key: <key>
{ "data": { "name": "...", "flags.campaign-codex": { type, image, data } } }
→ 200 { entity: [ <updated doc>, ... ] }

POST /create (scope: entity:write)

POST /create?clientId=<id>
{ "entityType": "JournalEntry", "data": { "name": "My Entry" } }
→ 200 { uuid: "JournalEntry.<newId>", data: { ... } }

GET /search (scope: search) — list ALL journal entries, zero downtime

GET /search?clientId=<id>&filter=documentType:JournalEntry&excludeCompendiums=true&limit=500&minified=true
→ 200 { results: [ { uuid, id, name, img, documentType } ] }

This is how we build the name→uuid map live (no Foundry stop). minified=true trims each result to { uuid, id, name, img, documentType }.

How we use it

  • cmd refresh builds name-uuid.json via searchJournalEntries() (zero downtime). The docker-stop LevelDB read is only for the full dashboard indexAll (needs full entry docs, not minified).
  • cmd push fetches the live entry via getEntry(uuid), builds the diff { name, "flags.campaign-codex": cc } (dot-path merge preserves other flags), and calls updateEntry(uuid, diff).

Notes for the TS client

  • clientId is always a query param; never a header.
  • For update, send a minimal diff, not the full document — never echo _id/pages/ ownership (those would clobber the live entry). Use dot-path flags.campaign-codex to avoid wiping sibling flags.
  • Envelopes are inconsistent: parse per-endpoint (data / entity / results).
  • WS round-trip default timeout ~10s → 408/504 on timeout.