3.5 KiB
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
/apiprefix. Paths appended directly to the base. - Auth:
x-api-key: <key>header on every request. - World selection:
clientIdquery 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 →400listing 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 refreshbuildsname-uuid.jsonviasearchJournalEntries()(zero downtime). The docker-stop LevelDB read is only for the full dashboardindexAll(needs full entry docs, not minified).cmd pushfetches the live entry viagetEntry(uuid), builds the diff{ name, "flags.campaign-codex": cc }(dot-path merge preserves other flags), and callsupdateEntry(uuid, diff).
Notes for the TS client
clientIdis 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-pathflags.campaign-codexto avoid wiping sibling flags. - Envelopes are inconsistent: parse per-endpoint (
data/entity/results). - WS round-trip default timeout ~10s →
408/504on timeout.