{
  "components": {
    "parameters": {
      "ActorSearchACMax": {
        "description": "Maximum armor class.",
        "in": "query",
        "name": "ac_max",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "ActorSearchACMin": {
        "description": "Minimum armor class.",
        "in": "query",
        "name": "ac_min",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "ActorSearchCRMax": {
        "description": "Maximum challenge rating. Valid D\u0026D 5e values: 0, 0.125, 0.25, 0.5, integer 1..30.",
        "in": "query",
        "name": "cr_max",
        "schema": {
          "type": "number"
        }
      },
      "ActorSearchCRMin": {
        "description": "Minimum challenge rating. Valid D\u0026D 5e values: 0, 0.125, 0.25, 0.5, integer 1..30.",
        "in": "query",
        "name": "cr_min",
        "schema": {
          "type": "number"
        }
      },
      "ActorSearchChaMax": {
        "description": "Maximum Charisma score.",
        "in": "query",
        "name": "cha_max",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "ActorSearchChaMin": {
        "description": "Minimum Charisma score.",
        "in": "query",
        "name": "cha_min",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "ActorSearchConMax": {
        "description": "Maximum Constitution score.",
        "in": "query",
        "name": "con_max",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "ActorSearchConMin": {
        "description": "Minimum Constitution score.",
        "in": "query",
        "name": "con_min",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "ActorSearchCreatureType": {
        "description": "Filter by D\u0026D 5e creature type (humanoid, beast, dragon, ...). Repeat for OR.",
        "explode": true,
        "in": "query",
        "name": "creature_type",
        "schema": {
          "items": {
            "enum": [
              "aberration",
              "beast",
              "celestial",
              "construct",
              "dragon",
              "elemental",
              "fey",
              "fiend",
              "giant",
              "humanoid",
              "monstrosity",
              "ooze",
              "plant",
              "undead"
            ],
            "type": "string"
          },
          "type": "array"
        }
      },
      "ActorSearchCurrentHPMax": {
        "description": "Maximum current HP.",
        "in": "query",
        "name": "current_hp_max",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "ActorSearchCurrentHPMin": {
        "description": "Minimum current HP.",
        "in": "query",
        "name": "current_hp_min",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "ActorSearchDexMax": {
        "description": "Maximum Dexterity score.",
        "in": "query",
        "name": "dex_max",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "ActorSearchDexMin": {
        "description": "Minimum Dexterity score.",
        "in": "query",
        "name": "dex_min",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "ActorSearchDisposition": {
        "description": "Filter by token disposition. Repeat for OR.",
        "explode": true,
        "in": "query",
        "name": "disposition",
        "schema": {
          "items": {
            "enum": [
              "hostile",
              "neutral",
              "friendly",
              "secret"
            ],
            "type": "string"
          },
          "type": "array"
        }
      },
      "ActorSearchFolderID": {
        "description": "Filter by Foundry folder id. Either `folder_id` or `folder_name` is required when filtering by folder.",
        "in": "query",
        "name": "folder_id",
        "schema": {
          "minLength": 1,
          "type": "string"
        }
      },
      "ActorSearchFolderName": {
        "description": "Filter by Foundry folder name. Matched against the folder's display name.",
        "in": "query",
        "name": "folder_name",
        "schema": {
          "minLength": 1,
          "type": "string"
        }
      },
      "ActorSearchFolderRecursive": {
        "description": "When true, descends into sub-folders. Default false (only direct children of the matched folder).",
        "in": "query",
        "name": "folder_recursive",
        "schema": {
          "type": "boolean"
        }
      },
      "ActorSearchHasPlayerOwner": {
        "description": "Filter actors by whether they have a player owner.",
        "in": "query",
        "name": "has_player_owner",
        "schema": {
          "type": "boolean"
        }
      },
      "ActorSearchIntMax": {
        "description": "Maximum Intelligence score.",
        "in": "query",
        "name": "int_max",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "ActorSearchIntMin": {
        "description": "Minimum Intelligence score.",
        "in": "query",
        "name": "int_min",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "ActorSearchLevelMax": {
        "description": "Maximum character level.",
        "in": "query",
        "name": "level_max",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "ActorSearchLevelMin": {
        "description": "Minimum character level (PC class level).",
        "in": "query",
        "name": "level_min",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "ActorSearchMaxHPMax": {
        "description": "Maximum max HP.",
        "in": "query",
        "name": "max_hp_max",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "ActorSearchMaxHPMin": {
        "description": "Minimum max HP.",
        "in": "query",
        "name": "max_hp_min",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "ActorSearchName": {
        "description": "Substring match on actor name (case-insensitive, module-side).",
        "in": "query",
        "name": "name",
        "schema": {
          "minLength": 1,
          "type": "string"
        }
      },
      "ActorSearchSize": {
        "description": "Filter by creature size code. Repeat for OR.",
        "explode": true,
        "in": "query",
        "name": "size",
        "schema": {
          "items": {
            "enum": [
              "tiny",
              "sm",
              "med",
              "lg",
              "huge",
              "grg"
            ],
            "type": "string"
          },
          "type": "array"
        }
      },
      "ActorSearchStrMax": {
        "description": "Maximum Strength score.",
        "in": "query",
        "name": "str_max",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "ActorSearchStrMin": {
        "description": "Minimum Strength score.",
        "in": "query",
        "name": "str_min",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "ActorSearchType": {
        "description": "Filter by actor type. Repeat for OR semantics: `?type=character\u0026type=npc`.",
        "explode": true,
        "in": "query",
        "name": "type",
        "schema": {
          "items": {
            "enum": [
              "character",
              "npc",
              "vehicle",
              "group"
            ],
            "type": "string"
          },
          "type": "array"
        }
      },
      "ActorSearchWisMax": {
        "description": "Maximum Wisdom score.",
        "in": "query",
        "name": "wis_max",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "ActorSearchWisMin": {
        "description": "Minimum Wisdom score.",
        "in": "query",
        "name": "wis_min",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      },
      "PaginationLimit": {
        "description": "Page size, 1..200. Default 50.",
        "in": "query",
        "name": "limit",
        "schema": {
          "maximum": 200,
          "minimum": 1,
          "type": "integer"
        }
      },
      "PaginationOffset": {
        "description": "Zero-based offset into the result set. Default 0.",
        "in": "query",
        "name": "offset",
        "schema": {
          "minimum": 0,
          "type": "integer"
        }
      }
    },
    "responses": {
      "BadGateway": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        },
        "description": "Bad gateway: the Foundry module received the command but returned an error envelope. The body's `error` carries the module's reason (prefixed with our sentinel marker, e.g. `foundry command failed: world data not loaded`)."
      },
      "Conflict": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        },
        "description": "Conflict: the request is well-formed but conflicts with the connected world's configuration. Two cases: the command is not supported by the world's game system (a `/pf2e/*` endpoint against a dnd5e world or vice versa), or a feature the command needs is switched off in the bridge module's settings (a script-type macro while **Allow Script Macros** is off — the user enables it in Foundry; the API cannot). The body's `error` carries the module's reason verbatim."
      },
      "GatewayTimeout": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        },
        "description": "Gateway timeout: the command was delivered to the Foundry module but no response arrived within the configured timeout. The module may be unresponsive or the operation may be slow on the Foundry side."
      },
      "InternalError": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        },
        "description": "An unexpected error occurred while processing the request. Retry later; contact support with the `X-Request-ID` header value if the problem persists."
      },
      "NotFound": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        },
        "description": "Not found: the addressed entity (actor / item / scene / etc.) does not exist in the user's world. The body's `error` carries the module's exact reason (e.g. `foundry command failed: Actor not found: ai4Vgy4kPRwgBlUn`)."
      },
      "ServiceUnavailable": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        },
        "description": "Service unavailable: the user's Foundry module is not currently connected to the WebSocket bridge. Either the module isn't installed, the user hasn't opened a world, or the connection dropped while the request was in flight. Retry once a connection re-establishes."
      },
      "TierForbidden": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/TierForbiddenResponse"
            }
          }
        },
        "description": "Forbidden: the API key resolved successfully but its tier is below the floor for this endpoint. Clients use the structured fields to render an upgrade UI without parsing the human-readable message."
      },
      "Unauthorized": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ErrorResponse"
            }
          }
        },
        "description": "Authentication required: missing, malformed, or invalid API key."
      },
      "Unprocessable": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ValidationErrorResponse"
            }
          }
        },
        "description": "Request validation failed. The body's `error` is a summary; `validation` enumerates each invalid field with a stable per-field message (e.g. `cr_min: must be a valid D\u0026D 5e CR`)."
      }
    },
    "schemas": {
      "ActivateSceneResponse": {
        "description": "Activation confirmation envelope.",
        "properties": {
          "data": {
            "properties": {
              "active": {
                "description": "Always true on success — Foundry confirmed activation.",
                "type": "boolean"
              },
              "scene_id": {
                "type": "string"
              },
              "scene_name": {
                "type": "string"
              }
            },
            "required": [
              "scene_id",
              "scene_name",
              "active"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "Actor": {
        "description": "Detailed view of a single Foundry actor — metadata, game-system data, and inventory. Identity is the Foundry actor id, scoped to the calling tenant's world.",
        "properties": {
          "id": {
            "description": "Foundry actor id, unique within the tenant's world.",
            "type": "string"
          },
          "image": {
            "description": "Path or URL to the actor portrait. Pass-through from the Foundry module.",
            "type": "string"
          },
          "items": {
            "description": "Actor's inventory. Empty array (`[]`) if none — never null.",
            "items": {
              "$ref": "#/components/schemas/Item"
            },
            "type": "array"
          },
          "name": {
            "description": "Human-readable actor name.",
            "type": "string"
          },
          "system": {
            "additionalProperties": true,
            "description": "Game-system-specific data blob (dnd5e: `abilities`, `attributes.hp`, `attributes.ac`, ...; pf2e: ...). Schema depends on the world's game system; clients that know the system traverse keys directly.",
            "type": "object"
          },
          "type": {
            "description": "Foundry actor type. Common values: `character`, `npc`, `vehicle`, `group`. Custom game systems define their own — free-form, no enum to avoid breaking on custom worlds.",
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "type",
          "image",
          "system",
          "items"
        ],
        "type": "object"
      },
      "ActorCreateRequest": {
        "description": "From-scratch creation payload for a new actor. `name` and `type` are required; everything else is optional and `system` is shallow-merged with the world-system defaults. `type` is game-system-aware — see the `POST /v1/actors` description.",
        "properties": {
          "folder": {
            "description": "Folder id to place the actor in. Omit for root.",
            "minLength": 1,
            "type": "string"
          },
          "image": {
            "description": "Portrait path / URL. Omit to accept the world-system default.",
            "minLength": 1,
            "type": "string"
          },
          "name": {
            "description": "Display name shown in the actor sidebar.",
            "minLength": 1,
            "type": "string"
          },
          "system": {
            "additionalProperties": true,
            "description": "Game-system data partial. Shallow-merged into the world-system defaults — pass only the keys you want to override (e.g. `{\"attributes\": {\"hp\": {\"value\": 7, \"max\": 7}}}`).",
            "type": "object"
          },
          "type": {
            "description": "Game-system-specific actor type (dnd5e: `character`/`npc`/`vehicle`/`group`; pf2e: `character`/`npc`/`loot`/`familiar`/`hazard`/`vehicle`; etc.). Module rejects unknown types as 502.",
            "minLength": 1,
            "type": "string"
          }
        },
        "required": [
          "name",
          "type"
        ],
        "type": "object"
      },
      "ActorRef": {
        "description": "Minimal actor reference returned from search — id + name only. Follow up with `GET /v1/actors/{id}` for full details.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name"
        ],
        "type": "object"
      },
      "ActorResponse": {
        "description": "Single-actor response. Wraps the actor under `data` to match the envelope convention every public endpoint follows.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Actor"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "ActorSearchResponse": {
        "description": "Paginated list of actor refs matching a search query. Empty results return `data: []` (never null).",
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/ActorRef"
            },
            "type": "array"
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        },
        "required": [
          "data",
          "pagination"
        ],
        "type": "object"
      },
      "ActorUpdateRequest": {
        "description": "Partial update payload for an actor — every field independently optional. `system` is shallow-merged into the existing actor's system data; deep nested paths work (e.g. `{\"system\": {\"attributes\": {\"hp\": {\"value\": 25}}}}` updates HP only). Actor `type` is immutable in Foundry and cannot be set here.",
        "properties": {
          "folder": {
            "description": "New folder id to move the actor to. Omit to leave unchanged.",
            "minLength": 1,
            "type": "string"
          },
          "image": {
            "description": "New portrait path / URL. Omit to leave unchanged.",
            "minLength": 1,
            "type": "string"
          },
          "name": {
            "description": "New actor name. Omit to leave unchanged.",
            "minLength": 1,
            "type": "string"
          },
          "system": {
            "additionalProperties": true,
            "description": "Game-system data partial. Module shallow-merges this map into the existing actor's `system`. Pass only the keys/paths you want to change.",
            "type": "object"
          }
        },
        "type": "object"
      },
      "AddCombatantRequest": {
        "description": "Add an actor to combat. `token_id` resolves ambiguity when an actor has multiple tokens. `initiative` pre-set is unusual — usually rolled later via /v1/dnd5e/dice/roll-initiative.",
        "properties": {
          "actor_id": {
            "minLength": 1,
            "type": "string"
          },
          "combat_id": {
            "minLength": 1,
            "type": "string"
          },
          "hidden": {
            "type": "boolean"
          },
          "initiative": {
            "format": "double",
            "type": "number"
          },
          "token_id": {
            "minLength": 1,
            "type": "string"
          }
        },
        "required": [
          "actor_id"
        ],
        "type": "object"
      },
      "ChatAuthor": {
        "description": "User who sent the chat message (distinct from speaker — speaker is who appears to talk, e.g. an actor).",
        "properties": {
          "name": {
            "type": "string"
          },
          "user_id": {
            "type": "string"
          }
        },
        "required": [
          "user_id",
          "name"
        ],
        "type": "object"
      },
      "ChatExportResponse": {
        "description": "Serialised chat-log dump. `content` is a single string — text or JSON depending on `format`. Can be large.",
        "properties": {
          "data": {
            "properties": {
              "content": {
                "type": "string"
              },
              "message_count": {
                "type": "integer"
              }
            },
            "required": [
              "content",
              "message_count"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "ChatMessage": {
        "description": "Single chat-log entry. `content` is plain-text — Foundry strips HTML on the read side even when the message was originally sent as HTML, so round-trip fidelity is not guaranteed.",
        "properties": {
          "author": {
            "$ref": "#/components/schemas/ChatAuthor"
          },
          "content": {
            "description": "Plain text — HTML stripped.",
            "type": "string"
          },
          "flavor": {
            "nullable": true,
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "is_roll": {
            "type": "boolean"
          },
          "is_whisper": {
            "type": "boolean"
          },
          "rolls": {
            "description": "Populated only when the request set `include_rolls=true`. Each entry is one roll embedded in the message; messages may contain multiple rolls (multi-attack chains, etc.).",
            "items": {
              "$ref": "#/components/schemas/ChatRollSummary"
            },
            "type": "array"
          },
          "speaker": {
            "$ref": "#/components/schemas/ChatSpeaker"
          },
          "style": {
            "description": "Message style: `ic` (in-character), `ooc` (out-of-character), `emote`, `other`. The wire also exposes `roll` for messages that ARE pure rolls.",
            "enum": [
              "ic",
              "ooc",
              "emote",
              "other",
              "roll"
            ],
            "type": "string"
          },
          "timestamp": {
            "description": "Unix epoch milliseconds.",
            "format": "int64",
            "type": "integer"
          },
          "whisper": {
            "description": "User IDs the message was whispered to. Empty for public messages.",
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "id",
          "timestamp",
          "author",
          "speaker",
          "content",
          "style",
          "is_roll",
          "whisper",
          "is_whisper"
        ],
        "type": "object"
      },
      "ChatMessageListResponse": {
        "description": "Paginated list of chat messages. `total` is the FULL chat log size (not just this page); `has_more` accounts for the limit boundary.",
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/ChatMessage"
            },
            "type": "array"
          },
          "has_more": {
            "type": "boolean"
          },
          "total": {
            "type": "integer"
          }
        },
        "required": [
          "data",
          "total",
          "has_more"
        ],
        "type": "object"
      },
      "ChatRollSummary": {
        "description": "Compressed roll summary embedded in a chat message. Only present when the request set `include_rolls=true`.",
        "properties": {
          "formula": {
            "type": "string"
          },
          "total": {
            "format": "double",
            "type": "number"
          }
        },
        "required": [
          "formula",
          "total"
        ],
        "type": "object"
      },
      "ChatSpeaker": {
        "description": "In-game speaker. `actor_id` and `token_id` are nullable — only present for IC messages tied to an actor.",
        "properties": {
          "actor_id": {
            "nullable": true,
            "type": "string"
          },
          "alias": {
            "type": "string"
          },
          "token_id": {
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "alias"
        ],
        "type": "object"
      },
      "ClearChatResponse": {
        "description": "Confirmation envelope for `DELETE /v1/chat/messages` (collection nuke). `deleted_count` is the module-reported number of messages wiped.",
        "properties": {
          "data": {
            "properties": {
              "deleted_count": {
                "type": "integer"
              }
            },
            "required": [
              "deleted_count"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "ClearTargetsResponse": {
        "description": "Bulk-untarget confirmation envelope. Idempotent; `count` is 0 when there were no targets.",
        "properties": {
          "data": {
            "properties": {
              "cleared": {
                "type": "boolean"
              },
              "count": {
                "description": "Number of tokens that were untargeted by this call.",
                "type": "integer"
              }
            },
            "required": [
              "cleared",
              "count"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "CloneSceneRequest": {
        "description": "Override name / folder for the cloned scene. Both optional — omit for Foundry's default `\u003csource\u003e (Copy)` name and the source's folder.",
        "properties": {
          "folder": {
            "description": "Folder ID (NOT name).",
            "minLength": 1,
            "type": "string"
          },
          "name": {
            "minLength": 1,
            "type": "string"
          }
        },
        "type": "object"
      },
      "CombatIDRequest": {
        "description": "Shared body for lifecycle / turn-nav ops (start / end / delete / next-turn / previous-turn). All fields optional — combat_id targets a specific combat, omit for active.",
        "properties": {
          "combat_id": {
            "minLength": 1,
            "type": "string"
          }
        },
        "type": "object"
      },
      "CombatState": {
        "description": "Combat encounter snapshot. `current` is null before start-combat or when no current turn.",
        "properties": {
          "combatants": {
            "items": {
              "$ref": "#/components/schemas/Combatant"
            },
            "type": "array"
          },
          "current": {
            "$ref": "#/components/schemas/Combatant",
            "nullable": true
          },
          "id": {
            "type": "string"
          },
          "round": {
            "type": "integer"
          },
          "started": {
            "type": "boolean"
          },
          "turn": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "round",
          "turn",
          "started",
          "combatants"
        ],
        "type": "object"
      },
      "CombatStateResponse": {
        "description": "Single-state envelope. Returned by GET /combat plus every state-mutating op (create / start / next-turn / previous-turn / set-turn).",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/CombatState"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "CombatTurnContext": {
        "description": "Tactical view for the current turn. `ascii_map` is **opt-in** via `?include_ascii_map=true` — bridge always renders it (~3-5KB), we drop it from the response unless explicitly requested.",
        "properties": {
          "ascii_map": {
            "description": "Bridge-rendered ASCII tactical map (legend included). Present only when `?include_ascii_map=true`.",
            "type": "string"
          },
          "current_combatant": {
            "$ref": "#/components/schemas/TurnCombatantInfo"
          },
          "nearby_tokens": {
            "items": {
              "$ref": "#/components/schemas/NearbyTokenInfo"
            },
            "type": "array"
          },
          "round": {
            "type": "integer"
          },
          "turn": {
            "type": "integer"
          }
        },
        "required": [
          "round",
          "turn",
          "current_combatant",
          "nearby_tokens"
        ],
        "type": "object"
      },
      "CombatTurnContextResponse": {
        "properties": {
          "data": {
            "$ref": "#/components/schemas/CombatTurnContext"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "Combatant": {
        "description": "One combat participant. `initiative` is `null` when not yet rolled. `actor_id`/`token_id` may be null in degenerate cases (orphaned combatants).",
        "properties": {
          "actor_id": {
            "nullable": true,
            "type": "string"
          },
          "defeated": {
            "description": "Skull icon + auto-skip turn.",
            "type": "boolean"
          },
          "hidden": {
            "description": "Invisible to players.",
            "type": "boolean"
          },
          "id": {
            "type": "string"
          },
          "img": {
            "description": "Token / actor portrait URL.",
            "type": "string"
          },
          "initiative": {
            "description": "null = not yet rolled.",
            "format": "double",
            "nullable": true,
            "type": "number"
          },
          "name": {
            "type": "string"
          },
          "token_id": {
            "nullable": true,
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "img",
          "defeated",
          "hidden"
        ],
        "type": "object"
      },
      "CombatantResponse": {
        "description": "Single-combatant envelope. Returned by add-combatant / update-combatant / set-defeated / toggle-visibility.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Combatant"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "CompendiumDocument": {
        "description": "Full single compendium document — identity plus the raw `data` blob (Foundry's stored `system` shape for the document type).",
        "properties": {
          "data": {
            "additionalProperties": true,
            "description": "Raw document data (Foundry's `doc.toObject()` output). Includes `system` plus all top-level fields the document carries.",
            "type": "object"
          },
          "doc_id": {
            "type": "string"
          },
          "document_type": {
            "enum": [
              "Actor",
              "Item",
              "JournalEntry",
              "Scene",
              "RollTable",
              "Macro",
              "Cards",
              "Playlist",
              "Adventure"
            ],
            "type": "string"
          },
          "image": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "pack_id": {
            "type": "string"
          },
          "type": {
            "description": "System sub-type. Empty when the document has no sub-type.",
            "type": "string"
          },
          "uuid": {
            "type": "string"
          }
        },
        "required": [
          "pack_id",
          "doc_id",
          "uuid",
          "name",
          "image",
          "document_type",
          "type",
          "data"
        ],
        "type": "object"
      },
      "CompendiumDocumentResponse": {
        "description": "Response for `GET /v1/compendiums/{packId}/documents/{docId}`.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/CompendiumDocument"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "CompendiumFilter": {
        "description": "One clause of a search query. AND-combined with sibling clauses.",
        "properties": {
          "field": {
            "description": "Dot-path into the document's system data — e.g. `system.level`, `system.school`, `system.attributes.hp.value`.",
            "minLength": 1,
            "type": "string"
          },
          "negate": {
            "default": false,
            "description": "Invert the clause's outcome. Useful for NOT-EQUALS / NOT-IS-EMPTY.",
            "type": "boolean"
          },
          "operator": {
            "default": "EQUALS",
            "description": "Comparison operator. Defaults to `EQUALS` when omitted. `BETWEEN` requires `value` to be `[min, max]`. `IS_EMPTY` ignores `value`.",
            "enum": [
              "EQUALS",
              "CONTAINS",
              "STARTS_WITH",
              "ENDS_WITH",
              "LESS_THAN",
              "LESS_THAN_EQUAL",
              "GREATER_THAN",
              "GREATER_THAN_EQUAL",
              "BETWEEN",
              "IS_EMPTY"
            ],
            "type": "string"
          },
          "value": {
            "description": "Operand. Type depends on operator: primitive for most, two-element array for `BETWEEN`, ignored for `IS_EMPTY`."
          }
        },
        "required": [
          "field"
        ],
        "type": "object"
      },
      "CompendiumImportRequest": {
        "description": "Request body for `POST /v1/compendiums/{packId}/documents/{docId}/import`. Both fields optional — empty body imports the document at world root with the source name.",
        "properties": {
          "folder": {
            "description": "Folder id to place the imported document in. Foundry validates folder.type ↔ document type at engine level — pass a folder of the matching type (lookup via `GET /v1/folders?type=\u003cPackType\u003e`). Mismatched-type folders may be silently ignored or rejected depending on Foundry version.",
            "minLength": 1,
            "type": "string"
          },
          "name": {
            "description": "Override the imported document's display name. Useful for distinguishing copies (e.g. `Sneaky Goblin` for a renamed Goblin import).",
            "minLength": 1,
            "type": "string"
          }
        },
        "type": "object"
      },
      "CompendiumImportedDocument": {
        "description": "The freshly-created world-side document after import. `world_id` is the new id within its target collection (game.actors / game.items / etc.); `document_type` echoes the Foundry pack type so callers know which collection to follow up against.",
        "properties": {
          "document_type": {
            "description": "Foundry pack type of the source pack — also identifies the world collection the document landed in.",
            "enum": [
              "Actor",
              "Item",
              "JournalEntry",
              "Scene",
              "RollTable",
              "Macro",
              "Cards",
              "Playlist"
            ],
            "type": "string"
          },
          "name": {
            "description": "Final display name (after optional override).",
            "type": "string"
          },
          "uuid": {
            "description": "Foundry-canonical uuid (`Compendium.\u003cpack\u003e.\u003cdoc\u003e` for compendium docs; `\u003cDocumentType\u003e.\u003cid\u003e` for world-imported).",
            "type": "string"
          },
          "world_id": {
            "description": "New document id in its target world collection.",
            "type": "string"
          }
        },
        "required": [
          "world_id",
          "uuid",
          "name",
          "document_type"
        ],
        "type": "object"
      },
      "CompendiumImportedDocumentResponse": {
        "description": "Response envelope for `POST /v1/compendiums/{packId}/documents/{docId}/import`.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/CompendiumImportedDocument"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "CompendiumIndexEntry": {
        "description": "One entry in a compendium pack's index. Identity + display fields plus optional `fields` map populated when the request asked for specific dot-paths.",
        "properties": {
          "doc_id": {
            "description": "Document id within the pack.",
            "type": "string"
          },
          "document_type": {
            "description": "Foundry pack type.",
            "enum": [
              "Actor",
              "Item",
              "JournalEntry",
              "Scene",
              "RollTable",
              "Macro",
              "Cards",
              "Playlist",
              "Adventure"
            ],
            "type": "string"
          },
          "fields": {
            "additionalProperties": true,
            "description": "Map of dot-path → value, populated only when the request supplied `fields`. Each key is a dot-path into the document's system data; the value is whatever Foundry stored there (number, string, object, null).",
            "type": "object"
          },
          "image": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "pack_id": {
            "type": "string"
          },
          "type": {
            "description": "System sub-type. Examples (D\u0026D 5e): `npc`, `weapon`, `spell`, `class`, `feat`, `background`. Empty when the document has no sub-type.",
            "type": "string"
          },
          "uuid": {
            "type": "string"
          }
        },
        "required": [
          "pack_id",
          "doc_id",
          "uuid",
          "name",
          "image",
          "document_type",
          "type"
        ],
        "type": "object"
      },
      "CompendiumIndexResponse": {
        "description": "Response for `GET /v1/compendiums/{packId}/index`. `pack` carries the metadata; `entries` is the (possibly empty) per-document index list.",
        "properties": {
          "data": {
            "properties": {
              "entries": {
                "items": {
                  "$ref": "#/components/schemas/CompendiumIndexEntry"
                },
                "type": "array"
              },
              "pack": {
                "$ref": "#/components/schemas/CompendiumPack"
              }
            },
            "required": [
              "pack",
              "entries"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "CompendiumPack": {
        "description": "One compendium pack's metadata. `id` equals Foundry `pack.collection` (e.g. `dnd5e.monsters`); `type` is the Foundry pack type (closed enum).",
        "properties": {
          "document_count": {
            "minimum": 0,
            "type": "integer"
          },
          "id": {
            "description": "Pack id (`pack.collection`).",
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "package_name": {
            "description": "Owning module / system / `world`.",
            "type": "string"
          },
          "system": {
            "description": "Game system slug (`dnd5e`, `pf2e`, ...) or empty for system-agnostic packs.",
            "type": "string"
          },
          "type": {
            "enum": [
              "Actor",
              "Item",
              "JournalEntry",
              "Scene",
              "RollTable",
              "Macro",
              "Cards",
              "Playlist",
              "Adventure"
            ],
            "type": "string"
          }
        },
        "required": [
          "id",
          "label",
          "type",
          "system",
          "package_name",
          "document_count"
        ],
        "type": "object"
      },
      "CompendiumPackListResponse": {
        "description": "List response for `GET /v1/compendiums`. `data` is the (possibly empty) array of pack metadata.",
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/CompendiumPack"
            },
            "type": "array"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "CompendiumSearchRequest": {
        "description": "Request body for `POST /v1/compendiums/{packId}/search`. All top-level fields optional; an empty body returns the default page over the whole pack.",
        "properties": {
          "exclude": {
            "description": "List of document ids to drop from the result.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "fields": {
            "description": "Dot-paths to copy from each matched document into the entry's `fields` map (same semantics as `GET /index`).",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "filters": {
            "description": "Structured filter clauses (AND between, see CompendiumFilter for clause shape).",
            "items": {
              "$ref": "#/components/schemas/CompendiumFilter"
            },
            "type": "array"
          },
          "limit": {
            "default": 50,
            "description": "Page size. Clamped to [1, 200].",
            "maximum": 200,
            "minimum": 1,
            "type": "integer"
          },
          "offset": {
            "default": 0,
            "description": "Skip first N results.",
            "minimum": 0,
            "type": "integer"
          },
          "query": {
            "description": "Free-text query matched against document `name` (Foundry's native CompendiumCollection.search). Combined with `filters` via AND.",
            "type": "string"
          }
        },
        "type": "object"
      },
      "CompendiumSearchResponse": {
        "description": "Response for `POST /v1/compendiums/{packId}/search`. `data` is the (possibly empty) entry array; `pagination` echoes the page the server applied.",
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/CompendiumIndexEntry"
            },
            "type": "array"
          },
          "pack_id": {
            "type": "string"
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        },
        "required": [
          "data",
          "pagination",
          "pack_id"
        ],
        "type": "object"
      },
      "CreateCombatRequest": {
        "description": "Create a new combat encounter. Both fields optional — defaults to active scene + activate=true.",
        "properties": {
          "activate": {
            "description": "Make this the world's active combat. Bridge default: true.",
            "type": "boolean"
          },
          "scene_id": {
            "description": "Scene to anchor combat to. Omit for active scene.",
            "minLength": 1,
            "type": "string"
          }
        },
        "type": "object"
      },
      "CreateFolderRequest": {
        "description": "Create a folder of the given document type. Foundry keeps separate folder trees per type.",
        "properties": {
          "color": {
            "description": "Hex color string (e.g. `#abcdef`).",
            "minLength": 1,
            "type": "string"
          },
          "description": {
            "minLength": 1,
            "type": "string"
          },
          "name": {
            "minLength": 1,
            "type": "string"
          },
          "parent_id": {
            "description": "Parent folder id. Omit for root-level.",
            "minLength": 1,
            "type": "string"
          },
          "sort": {
            "description": "Sort position within parent. Module assigns one if omitted.",
            "type": "integer"
          },
          "type": {
            "$ref": "#/components/schemas/FolderDocumentType"
          }
        },
        "required": [
          "name",
          "type"
        ],
        "type": "object"
      },
      "CreateItemRequest": {
        "description": "Create a new world-level item. `name` and `type` required. `folder` here is the folder ID (write-side); responses return folder NAME.\n\nValid `type` values for D\u0026D 5e: `weapon`, `equipment`, `consumable`, `tool`, `container`, `loot`, `spell`, `feat`, `background`, `race`, `class`, `subclass`, `feature`. Module rejects unknown types as 502 with the system's reason.\n\n`system` is the D\u0026D 5e system-data blob — see GET /v1/items/{id} response for the canonical shape (rarity, weight, price, identified, attunement, etc).",
        "properties": {
          "folder": {
            "description": "Folder ID to place the item in. Omit for root.",
            "minLength": 1,
            "type": "string"
          },
          "image": {
            "description": "Image path or URL.",
            "minLength": 1,
            "type": "string"
          },
          "name": {
            "description": "Display name shown in the Items Directory.",
            "minLength": 1,
            "type": "string"
          },
          "system": {
            "additionalProperties": true,
            "description": "System-data partial. Foundry deep-merges into defaults.",
            "type": "object"
          },
          "type": {
            "description": "Game-system-specific item type. Module validates; unknown → 502.",
            "minLength": 1,
            "type": "string"
          }
        },
        "required": [
          "name",
          "type"
        ],
        "type": "object"
      },
      "CreateJournalPageRequest": {
        "description": "Create a new page in an existing journal. `name` required. `type` defaults to text on Foundry side when omitted. `content` is the body for text pages (HTML supported); `src` is the asset URL for image/video/pdf pages.",
        "properties": {
          "content": {
            "type": "string"
          },
          "name": {
            "minLength": 1,
            "type": "string"
          },
          "src": {
            "description": "Asset URL for image/video/pdf pages.",
            "type": "string"
          },
          "type": {
            "$ref": "#/components/schemas/JournalPageType"
          }
        },
        "required": [
          "name"
        ],
        "type": "object"
      },
      "CreateJournalRequest": {
        "description": "Create a new journal. Only `name` is required. `folder` places it under a folder by NAME (Foundry creates the folder concept implicitly via grouping). `content` + optional `page_type` create an initial page.",
        "properties": {
          "content": {
            "description": "Initial page body. When set, an initial page is created with this content.",
            "type": "string"
          },
          "folder": {
            "description": "Folder NAME (not id). When omitted, journal goes to root.",
            "minLength": 1,
            "type": "string"
          },
          "name": {
            "minLength": 1,
            "type": "string"
          },
          "page_type": {
            "$ref": "#/components/schemas/JournalPageType"
          }
        },
        "required": [
          "name"
        ],
        "type": "object"
      },
      "CreateMacroRequest": {
        "description": "Create a macro. `type: script` is accepted only while the module setting **Allow Script Macros** is on (409 otherwise). `folder_id` is a folder ID (NOT the name the read side returns).",
        "properties": {
          "command": {
            "description": "Chat text for `chat` macros (e.g. `/roll 1d20`), JavaScript source for `script` macros.",
            "minLength": 1,
            "type": "string"
          },
          "folder_id": {
            "description": "Folder ID (NOT name) from `GET /v1/folders?type=Macro`. Omit for root-level.",
            "minLength": 1,
            "type": "string"
          },
          "img": {
            "description": "Icon path. Foundry assigns its default macro icon when omitted.",
            "minLength": 1,
            "type": "string"
          },
          "name": {
            "minLength": 1,
            "type": "string"
          },
          "scope": {
            "description": "Macro scope — see MacroScope. Foundry defaults to `global` when omitted.",
            "enum": [
              "global",
              "actors",
              "actor"
            ],
            "type": "string"
          },
          "type": {
            "description": "Macro type — see MacroType.",
            "enum": [
              "chat",
              "script"
            ],
            "type": "string"
          }
        },
        "required": [
          "name",
          "type",
          "command"
        ],
        "type": "object"
      },
      "CreateNoteRequest": {
        "description": "Pin a note at pixel coordinates `(x, y)`. Notes work standalone (no journal binding required) — set `text` for free-text labels, or `entry_id` (+ optional `page_id`) to bind to a journal.",
        "properties": {
          "entry_id": {
            "minLength": 1,
            "type": "string"
          },
          "font_size": {
            "minimum": 1,
            "type": "integer"
          },
          "global": {
            "type": "boolean"
          },
          "icon_size": {
            "minimum": 1,
            "type": "integer"
          },
          "icon_src": {
            "minLength": 1,
            "type": "string"
          },
          "icon_tint": {
            "minLength": 1,
            "type": "string"
          },
          "page_id": {
            "minLength": 1,
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "text_anchor": {
            "$ref": "#/components/schemas/NoteTextAnchor"
          },
          "text_color": {
            "minLength": 1,
            "type": "string"
          },
          "x": {
            "type": "integer"
          },
          "y": {
            "type": "integer"
          }
        },
        "required": [
          "x",
          "y"
        ],
        "type": "object"
      },
      "CreateRollTableEntry": {
        "description": "Initial entry for a brand-new table. `text` and `range: [min, max]` are required. `weight` defaults to 1. `type` defaults to 0 (text); use 1/2 for document/compendium references and pair with `document_collection` + `document_id`.",
        "properties": {
          "document_collection": {
            "minLength": 1,
            "type": "string"
          },
          "document_id": {
            "minLength": 1,
            "type": "string"
          },
          "img": {
            "minLength": 1,
            "type": "string"
          },
          "range": {
            "description": "[min, max] inclusive roll bracket.",
            "items": {
              "type": "integer"
            },
            "maxItems": 2,
            "minItems": 2,
            "type": "array"
          },
          "text": {
            "minLength": 1,
            "type": "string"
          },
          "type": {
            "description": "0 = text (default), 1 = document, 2 = compendium.",
            "enum": [
              0,
              1,
              2
            ],
            "type": "integer"
          },
          "weight": {
            "minimum": 1,
            "type": "integer"
          }
        },
        "required": [
          "text",
          "range"
        ],
        "type": "object"
      },
      "CreateRollTableRequest": {
        "description": "Create a new roll table. Only `name` is required; everything else uses Foundry defaults if omitted. `folder` is a folder ID (mirrors items, NOT journals which use folder NAME). Pass an initial `results` array to seed entries on creation.",
        "properties": {
          "description": {
            "type": "string"
          },
          "display_roll": {
            "description": "Foundry default: true.",
            "type": "boolean"
          },
          "folder": {
            "description": "Folder ID (NOT name).",
            "minLength": 1,
            "type": "string"
          },
          "formula": {
            "description": "Dice formula. Foundry default: `1d20`.",
            "minLength": 1,
            "type": "string"
          },
          "img": {
            "minLength": 1,
            "type": "string"
          },
          "name": {
            "minLength": 1,
            "type": "string"
          },
          "replacement": {
            "description": "Foundry default: true.",
            "type": "boolean"
          },
          "results": {
            "items": {
              "$ref": "#/components/schemas/CreateRollTableEntry"
            },
            "type": "array"
          }
        },
        "required": [
          "name"
        ],
        "type": "object"
      },
      "CreateSceneRequest": {
        "description": "Create a new scene. Only `name` is required; everything else uses Foundry defaults. `folder` is a folder ID (NOT a name).",
        "properties": {
          "background": {
            "description": "Background image URL.",
            "minLength": 1,
            "type": "string"
          },
          "darkness": {
            "format": "double",
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "fog_exploration": {
            "description": "Track per-player fog of war.",
            "type": "boolean"
          },
          "folder": {
            "description": "Folder ID (NOT name).",
            "minLength": 1,
            "type": "string"
          },
          "foreground": {
            "description": "Foreground image URL.",
            "minLength": 1,
            "type": "string"
          },
          "grid": {
            "$ref": "#/components/schemas/SceneGridInput"
          },
          "height": {
            "minimum": 1,
            "type": "integer"
          },
          "name": {
            "minLength": 1,
            "type": "string"
          },
          "nav_name": {
            "minLength": 1,
            "type": "string"
          },
          "nav_order": {
            "type": "integer"
          },
          "navigation": {
            "description": "Show in Foundry's sidebar navigation strip.",
            "type": "boolean"
          },
          "padding": {
            "format": "double",
            "minimum": 0,
            "type": "number"
          },
          "width": {
            "minimum": 1,
            "type": "integer"
          }
        },
        "required": [
          "name"
        ],
        "type": "object"
      },
      "CreateTokenRequest": {
        "description": "Place a token by instancing an actor at pixel coordinates. `actor_id`, `x`, `y` required; `scene_id` defaults to the active scene; the rest follow Foundry's defaults.",
        "properties": {
          "actor_id": {
            "minLength": 1,
            "type": "string"
          },
          "elevation": {
            "type": "integer"
          },
          "hidden": {
            "type": "boolean"
          },
          "rotation": {
            "description": "Rotation in degrees.",
            "type": "integer"
          },
          "scale": {
            "format": "double",
            "minimum": 0,
            "type": "number"
          },
          "scene_id": {
            "minLength": 1,
            "type": "string"
          },
          "x": {
            "type": "integer"
          },
          "y": {
            "type": "integer"
          }
        },
        "required": [
          "actor_id",
          "x",
          "y"
        ],
        "type": "object"
      },
      "CreateWallRequest": {
        "description": "Create a wall. Only `c` (the endpoint tuple) is required; enum fields use the bridge's defaults when omitted.",
        "properties": {
          "c": {
            "$ref": "#/components/schemas/WallCoordinates"
          },
          "dir": {
            "$ref": "#/components/schemas/WallDirection"
          },
          "door": {
            "$ref": "#/components/schemas/WallDoorType"
          },
          "door_state": {
            "$ref": "#/components/schemas/WallDoorState"
          },
          "light": {
            "$ref": "#/components/schemas/WallRestriction"
          },
          "move": {
            "$ref": "#/components/schemas/WallRestriction"
          },
          "sense": {
            "$ref": "#/components/schemas/WallRestriction"
          },
          "sound": {
            "$ref": "#/components/schemas/WallRestriction"
          }
        },
        "required": [
          "c"
        ],
        "type": "object"
      },
      "DeleteActorResponse": {
        "description": "Delete confirmation envelope. Echoes the deleted actor id for client convenience.",
        "properties": {
          "data": {
            "properties": {
              "actor_id": {
                "type": "string"
              },
              "deleted": {
                "type": "boolean"
              }
            },
            "required": [
              "actor_id",
              "deleted"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "DeleteChatMessageResponse": {
        "description": "Delete confirmation envelope. Echoes the deleted message id for client convenience.",
        "properties": {
          "data": {
            "properties": {
              "deleted": {
                "type": "boolean"
              },
              "message_id": {
                "type": "string"
              }
            },
            "required": [
              "message_id",
              "deleted"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "DeleteCombatResponse": {
        "description": "Delete confirmation envelope. Returned by DELETE /combat / POST /combat/end / DELETE /combat/combatants/{id}.",
        "properties": {
          "data": {
            "properties": {
              "deleted": {
                "type": "boolean"
              }
            },
            "required": [
              "deleted"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "DeleteFolderRequest": {
        "description": "Optional cascade flags for `DELETE /v1/folders/{id}`. Body is allowed on DELETE here per project convention. Both default false → sub-folders/documents are orphaned to the parent folder (or root) on delete.",
        "properties": {
          "delete_contents": {
            "default": false,
            "description": "Also delete documents inside the folder. Default false — documents are orphaned.",
            "type": "boolean"
          },
          "delete_subfolders": {
            "default": false,
            "description": "Recursively delete child folders. Default false — children are orphaned.",
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "DeleteFolderResponse": {
        "description": "Delete confirmation envelope. Echoes the deleted folder id for client convenience.",
        "properties": {
          "data": {
            "properties": {
              "deleted": {
                "type": "boolean"
              },
              "folder_id": {
                "type": "string"
              }
            },
            "required": [
              "folder_id",
              "deleted"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "DeleteItemResponse": {
        "description": "Delete confirmation envelope. Echoes the deleted item id for client convenience.",
        "properties": {
          "data": {
            "properties": {
              "deleted": {
                "type": "boolean"
              },
              "item_id": {
                "type": "string"
              }
            },
            "required": [
              "item_id",
              "deleted"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "DeleteJournalPageResponse": {
        "description": "Delete confirmation envelope for a single page.",
        "properties": {
          "data": {
            "properties": {
              "deleted": {
                "type": "boolean"
              },
              "journal_id": {
                "type": "string"
              },
              "page_id": {
                "type": "string"
              }
            },
            "required": [
              "journal_id",
              "page_id",
              "deleted"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "DeleteJournalResponse": {
        "description": "Delete confirmation envelope.",
        "properties": {
          "data": {
            "properties": {
              "deleted": {
                "type": "boolean"
              },
              "journal_id": {
                "type": "string"
              }
            },
            "required": [
              "journal_id",
              "deleted"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "DeleteMacroResponse": {
        "description": "Delete confirmation envelope. Echoes the deleted macro id for client convenience.",
        "properties": {
          "data": {
            "properties": {
              "deleted": {
                "type": "boolean"
              },
              "macro_id": {
                "type": "string"
              }
            },
            "required": [
              "macro_id",
              "deleted"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "DeleteNoteResponse": {
        "description": "Delete confirmation envelope.",
        "properties": {
          "data": {
            "properties": {
              "deleted": {
                "type": "boolean"
              },
              "note_id": {
                "type": "string"
              },
              "scene_id": {
                "type": "string"
              }
            },
            "required": [
              "scene_id",
              "note_id",
              "deleted"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "DeleteRollTableResponse": {
        "description": "Delete confirmation envelope.",
        "properties": {
          "data": {
            "properties": {
              "deleted": {
                "type": "boolean"
              },
              "roll_table_id": {
                "type": "string"
              }
            },
            "required": [
              "roll_table_id",
              "deleted"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "DeleteSceneResponse": {
        "description": "Delete confirmation envelope.",
        "properties": {
          "data": {
            "properties": {
              "deleted": {
                "type": "boolean"
              },
              "scene_id": {
                "type": "string"
              }
            },
            "required": [
              "scene_id",
              "deleted"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "DeleteTokenResponse": {
        "description": "Delete confirmation envelope.",
        "properties": {
          "data": {
            "properties": {
              "deleted": {
                "type": "boolean"
              },
              "scene_id": {
                "description": "Echo of the request's scene_id (empty when omitted = active scene).",
                "type": "string"
              },
              "token_id": {
                "type": "string"
              }
            },
            "required": [
              "scene_id",
              "token_id",
              "deleted"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "DeleteWallResponse": {
        "description": "Delete confirmation envelope.",
        "properties": {
          "data": {
            "properties": {
              "deleted": {
                "type": "boolean"
              },
              "scene_id": {
                "type": "string"
              },
              "wall_id": {
                "type": "string"
              }
            },
            "required": [
              "scene_id",
              "wall_id",
              "deleted"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "DiceGroup": {
        "description": "One homogeneous group of dice from the formula. For `2d6+1d4` the result has two groups; for `1d20+5` one group (the +5 modifier shows up only in `total`).",
        "properties": {
          "count": {
            "description": "How many dice of this type were rolled.",
            "type": "integer"
          },
          "results": {
            "description": "Individual rolled values, before any keep-highest/drop-lowest modifiers are applied at the formula level.",
            "items": {
              "type": "integer"
            },
            "type": "array"
          },
          "type": {
            "description": "Die type — `d20`, `d6`, `d4`, etc.",
            "type": "string"
          }
        },
        "required": [
          "type",
          "count",
          "results"
        ],
        "type": "object"
      },
      "DiceRoll": {
        "description": "Dice roll result.",
        "properties": {
          "dice": {
            "description": "Per-group breakdown. Empty array for purely arithmetic formulas like `10+5`.",
            "items": {
              "$ref": "#/components/schemas/DiceGroup"
            },
            "type": "array"
          },
          "formula": {
            "description": "Echo of the requested formula, normalised by the parser.",
            "type": "string"
          },
          "is_critical": {
            "description": "True when a kept d20 in the formula natural-rolled a 20. Works correctly for plain `1d20`, advantage `2d20kh1` (only the kept die counts), and disadvantage `2d20kl1` (a 20 on the discarded die does NOT count). False for non-d20 formulas.",
            "type": "boolean"
          },
          "is_fumble": {
            "description": "True when a kept d20 in the formula natural-rolled a 1. Same advantage/disadvantage semantics as is_critical — only the kept die counts.",
            "type": "boolean"
          },
          "total": {
            "description": "Final value including modifiers and arithmetic. `number` (not `integer`) because the DSL allows division and other non-integer math.",
            "format": "double",
            "type": "number"
          }
        },
        "required": [
          "total",
          "formula",
          "dice",
          "is_critical",
          "is_fumble"
        ],
        "type": "object"
      },
      "DiceRollRequest": {
        "description": "Generic dice roll request. Only `formula` is required.",
        "properties": {
          "flavor": {
            "description": "Chat-message header text (e.g. `Stealth check`). Meaningful only when `show_in_chat` is true.",
            "maxLength": 200,
            "minLength": 1,
            "type": "string"
          },
          "formula": {
            "description": "Foundry Roll DSL expression — `1d20+5`, `2d6+1d4`, `4d6kh3`, `10+5`. Validated by the module's Roll parser; invalid expressions return 502.",
            "maxLength": 200,
            "minLength": 1,
            "type": "string"
          },
          "show_in_chat": {
            "default": false,
            "description": "When true, post the roll to the world's Foundry chat (visible to all players). Default false — silent roll, result returned only to the caller.",
            "type": "boolean"
          }
        },
        "required": [
          "formula"
        ],
        "type": "object"
      },
      "DiceRollResponse": {
        "description": "Envelope wrapping a single DiceRoll under `data`, matching the convention used by every other endpoint.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/DiceRoll"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "Dnd5eActivation": {
        "description": "Action-economy slot the spell consumes.",
        "properties": {
          "cost": {
            "description": "How many slots / minutes / hours the activation consumes. 1 for a standard action; 10 for a 10-minute ritual.",
            "type": "integer"
          },
          "type": {
            "description": "Slot: action / bonus / reaction / minute / hour / day / special / none / legendary / lair / crew.",
            "type": "string"
          }
        },
        "required": [
          "type",
          "cost"
        ],
        "type": "object"
      },
      "Dnd5eDamage": {
        "description": "One damage-roll spec. `type` is the primary damage type; `types` (optional) carries additional alternative types for multi-type damage in 2024 / 5.5e schemas.",
        "properties": {
          "formula": {
            "description": "Roll formula, e.g. `8d6` or `2d6 + @mod`.",
            "type": "string"
          },
          "type": {
            "description": "Primary damage type (acid / bludgeoning / cold / fire / force / lightning / necrotic / piercing / poison / psychic / radiant / slashing / thunder / healing / temp_hp / none).",
            "type": "string"
          },
          "types": {
            "description": "Additional alternative damage types (rare; usually empty).",
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "formula",
          "type"
        ],
        "type": "object"
      },
      "Dnd5eDescription": {
        "description": "Dual-form description: raw HTML (Foundry-stored markup) plus the regex-stripped plain-text projection for list views and search snippets.",
        "properties": {
          "html": {
            "type": "string"
          },
          "plain_text": {
            "type": "string"
          }
        },
        "required": [
          "html",
          "plain_text"
        ],
        "type": "object"
      },
      "Dnd5eDocRef": {
        "description": "Compendium document reference embedded in every typed dnd5e aggregate. Carries enough identity + display fields to render a list row plus the lookup contract for follow-up reads.",
        "properties": {
          "doc_id": {
            "type": "string"
          },
          "image": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "pack_id": {
            "type": "string"
          },
          "uuid": {
            "type": "string"
          }
        },
        "required": [
          "pack_id",
          "doc_id",
          "uuid",
          "name",
          "image"
        ],
        "type": "object"
      },
      "Dnd5eDuration": {
        "description": "Effect duration. `unit` is inst / round / turn / minute / hour / day / month / year / perm / dispel / spec / empty.",
        "properties": {
          "concentration": {
            "type": "boolean"
          },
          "unit": {
            "type": "string"
          },
          "value": {
            "minimum": 0,
            "nullable": true,
            "type": "integer"
          }
        },
        "required": [
          "unit",
          "concentration"
        ],
        "type": "object"
      },
      "Dnd5eMaterialDescription": {
        "description": "Material-component detail. Present only when `components.material` is true and the source carries a non-empty material descriptor.",
        "properties": {
          "consumed": {
            "type": "boolean"
          },
          "cost_gp": {
            "description": "Material cost in gp (normalised); null when no cost.",
            "format": "double",
            "nullable": true,
            "type": "number"
          },
          "value": {
            "description": "Prose: `a tiny ball of bat guano and sulfur`.",
            "type": "string"
          }
        },
        "required": [
          "value",
          "consumed"
        ],
        "type": "object"
      },
      "Dnd5eRange": {
        "description": "Range spec. `value` is the primary number (e.g. 60 for `60 ft`); `long` is the optional thrown / ranged-weapon long range (e.g. 20/60 → value=20, long=60). `unit` is `self` / `touch` / `ft` / `mi` / `sight` / `unlimited` / `spec` / empty.",
        "properties": {
          "long": {
            "minimum": 0,
            "nullable": true,
            "type": "integer"
          },
          "unit": {
            "type": "string"
          },
          "value": {
            "minimum": 0,
            "nullable": true,
            "type": "integer"
          }
        },
        "required": [
          "unit"
        ],
        "type": "object"
      },
      "Dnd5eSpell": {
        "description": "Fully-typed spell aggregate.",
        "properties": {
          "casting_time": {
            "$ref": "#/components/schemas/Dnd5eActivation"
          },
          "classes": {
            "description": "Class identifiers that natively can learn this spell (`wizard`, `cleric`). Empty for spells with no class affinity in source data.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "components": {
            "$ref": "#/components/schemas/Dnd5eSpellComponents"
          },
          "concentration": {
            "type": "boolean"
          },
          "damage": {
            "items": {
              "$ref": "#/components/schemas/Dnd5eDamage"
            },
            "type": "array"
          },
          "description": {
            "$ref": "#/components/schemas/Dnd5eDescription"
          },
          "doc": {
            "$ref": "#/components/schemas/Dnd5eDocRef"
          },
          "duration": {
            "$ref": "#/components/schemas/Dnd5eDuration"
          },
          "level": {
            "maximum": 9,
            "minimum": 0,
            "type": "integer"
          },
          "material": {
            "$ref": "#/components/schemas/Dnd5eMaterialDescription",
            "nullable": true
          },
          "range": {
            "$ref": "#/components/schemas/Dnd5eRange"
          },
          "ritual": {
            "type": "boolean"
          },
          "save": {
            "$ref": "#/components/schemas/Dnd5eSpellSave",
            "nullable": true
          },
          "school": {
            "type": "string"
          },
          "target": {
            "$ref": "#/components/schemas/Dnd5eTarget",
            "nullable": true
          }
        },
        "required": [
          "doc",
          "description",
          "level",
          "school",
          "components",
          "casting_time",
          "range",
          "duration",
          "ritual",
          "concentration"
        ],
        "type": "object"
      },
      "Dnd5eSpellComponents": {
        "description": "Verbal / Somatic / Material flags every spell carries.",
        "properties": {
          "material": {
            "type": "boolean"
          },
          "somatic": {
            "type": "boolean"
          },
          "verbal": {
            "type": "boolean"
          }
        },
        "required": [
          "verbal",
          "somatic",
          "material"
        ],
        "type": "object"
      },
      "Dnd5eSpellResponse": {
        "description": "Single spell envelope.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Dnd5eSpell"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "Dnd5eSpellSave": {
        "description": "Saving-throw spec for spells that force a save. `dc` is rare at the compendium level — actual DC depends on caster's spellcasting ability + prof bonus, computed at the actor sheet.",
        "properties": {
          "ability": {
            "description": "Ability code (str / dex / con / int / wis / cha).",
            "type": "string"
          },
          "dc": {
            "minimum": 0,
            "nullable": true,
            "type": "integer"
          }
        },
        "required": [
          "ability"
        ],
        "type": "object"
      },
      "Dnd5eSpellSearchResponse": {
        "description": "Paginated search response.",
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/Dnd5eSpellSummary"
            },
            "type": "array"
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        },
        "required": [
          "data",
          "pagination"
        ],
        "type": "object"
      },
      "Dnd5eSpellSummary": {
        "description": "Lightweight projection returned in spell-search results. Carries identity + the most-filtered fields. For the full spell, follow up with `GET /v1/dnd5e/compendiums/spells/{packId}/{docId}`.",
        "properties": {
          "concentration": {
            "type": "boolean"
          },
          "doc": {
            "$ref": "#/components/schemas/Dnd5eDocRef"
          },
          "level": {
            "description": "Spell slot tier (0 = cantrip).",
            "maximum": 9,
            "minimum": 0,
            "type": "integer"
          },
          "ritual": {
            "type": "boolean"
          },
          "school": {
            "description": "Spell school. One of abjuration / conjuration / divination / enchantment / evocation / illusion / necromancy / transmutation; empty when the source pack uses an unknown / homebrew school.",
            "type": "string"
          }
        },
        "required": [
          "doc",
          "level",
          "school",
          "ritual",
          "concentration"
        ],
        "type": "object"
      },
      "Dnd5eTarget": {
        "description": "What the spell targets. For single-target spells `value` is the count (`up to 3 creatures` → 3); for spatial spells it's the size in `units` (`60-foot cone` → 60). `width` is the second dimension for shapes that need it.",
        "properties": {
          "type": {
            "description": "self / creature / ally / enemy / object / space / radius / sphere / cylinder / cone / cube / line / wall / any / empty.",
            "type": "string"
          },
          "units": {
            "description": "feet / miles when applicable to a spatial target; empty otherwise.",
            "type": "string"
          },
          "value": {
            "minimum": 0,
            "nullable": true,
            "type": "integer"
          },
          "width": {
            "minimum": 0,
            "nullable": true,
            "type": "integer"
          }
        },
        "required": [
          "type",
          "units"
        ],
        "type": "object"
      },
      "ErrorResponse": {
        "description": "Standard error response. Used for every non-2xx outcome where the failure isn't tied to per-field request validation. The body carries a single human-readable `error` string; nothing else.",
        "properties": {
          "error": {
            "description": "Short human-readable reason for the failure.",
            "type": "string"
          }
        },
        "required": [
          "error"
        ],
        "type": "object"
      },
      "ExecuteMacroRequest": {
        "description": "Optional execution context for `POST /v1/macros/{id}/execute`. Both optional — omit the body to run the macro without an actor / token.",
        "properties": {
          "actor_id": {
            "description": "Actor id (from `GET /v1/actors`); the macro sees it as `actor`.",
            "minLength": 1,
            "type": "string"
          },
          "token_id": {
            "description": "Token id on the active scene (from `GET /v1/tokens`); the macro sees it as `token`.",
            "minLength": 1,
            "type": "string"
          }
        },
        "type": "object"
      },
      "ExecuteMacroResponse": {
        "description": "Execution confirmation envelope. The macro's effect is not returned — it lands in chat (chat macros) or in the world (script macros).",
        "properties": {
          "data": {
            "properties": {
              "executed": {
                "description": "Always true on 200.",
                "type": "boolean"
              },
              "macro_id": {
                "type": "string"
              },
              "macro_name": {
                "type": "string"
              },
              "macro_type": {
                "$ref": "#/components/schemas/MacroType"
              }
            },
            "required": [
              "executed",
              "macro_id",
              "macro_name",
              "macro_type"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "Folder": {
        "description": "Lean folder summary returned by list/get/create/update. `parent_id` is null for root-level folders.",
        "properties": {
          "color": {
            "description": "Hex color (e.g. `#abcdef`). Null when not set.",
            "nullable": true,
            "type": "string"
          },
          "description": {
            "nullable": true,
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "parent_id": {
            "description": "Parent folder id; null for root-level folders.",
            "nullable": true,
            "type": "string"
          },
          "sort": {
            "description": "Sort order within the parent (Foundry-assigned).",
            "type": "integer"
          },
          "type": {
            "$ref": "#/components/schemas/FolderDocumentType"
          }
        },
        "required": [
          "id",
          "name",
          "type",
          "sort"
        ],
        "type": "object"
      },
      "FolderDocumentType": {
        "description": "Foundry document type a folder organises. Folders are typed — an Actor folder can only hold Actor documents, etc.",
        "enum": [
          "Actor",
          "Item",
          "Scene",
          "JournalEntry",
          "RollTable",
          "Macro",
          "Cards",
          "Playlist",
          "Compendium"
        ],
        "type": "string"
      },
      "FolderListResponse": {
        "description": "List of folder summaries — `GET /v1/folders` (optionally filtered by type).",
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/Folder"
            },
            "type": "array"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "FolderResponse": {
        "description": "Single-folder response wrapper.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Folder"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "FolderTree": {
        "description": "Recursive folder shape returned by GET when `include_subfolders` is true. Subfolders is always present (possibly empty); `content_ids` is present only when the request set `include_contents=true`.",
        "properties": {
          "color": {
            "nullable": true,
            "type": "string"
          },
          "content_ids": {
            "description": "Document ids directly inside this folder (NOT nested into subfolders). Present only when `include_contents=true` was passed.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "description": {
            "nullable": true,
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "parent_id": {
            "nullable": true,
            "type": "string"
          },
          "sort": {
            "type": "integer"
          },
          "subfolders": {
            "items": {
              "$ref": "#/components/schemas/FolderTree"
            },
            "type": "array"
          },
          "type": {
            "$ref": "#/components/schemas/FolderDocumentType"
          }
        },
        "required": [
          "id",
          "name",
          "type",
          "sort",
          "subfolders"
        ],
        "type": "object"
      },
      "FolderTreeResponse": {
        "description": "Folder tree response wrapper for GET /v1/folders/{id} with include_subfolders/contents flags.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/FolderTree"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "HPInfo": {
        "description": "value/max HP pair attached to combatants and nearby tokens.",
        "properties": {
          "max": {
            "type": "integer"
          },
          "value": {
            "type": "integer"
          }
        },
        "required": [
          "value",
          "max"
        ],
        "type": "object"
      },
      "InitiativeResult": {
        "description": "One combatant's initiative roll outcome.",
        "properties": {
          "combatant_id": {
            "type": "string"
          },
          "initiative": {
            "description": "Rolled initiative value. Float because Foundry's Roll DSL allows non-integer totals.",
            "format": "double",
            "type": "number"
          },
          "name": {
            "description": "Display name of the combatant (usually the actor or token name).",
            "type": "string"
          }
        },
        "required": [
          "combatant_id",
          "name",
          "initiative"
        ],
        "type": "object"
      },
      "InitiativeRollResponse": {
        "description": "Envelope wrapping initiative roll results under `data`. Returned by both roll-initiative and roll-all-initiative.",
        "properties": {
          "data": {
            "properties": {
              "results": {
                "description": "One entry per rolled combatant. Empty array when no combatants matched (e.g. `npcs_only=true` in a PC-only combat).",
                "items": {
                  "$ref": "#/components/schemas/InitiativeResult"
                },
                "type": "array"
              }
            },
            "required": [
              "results"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "Item": {
        "description": "Foundry item — weapons, armor, consumables, spells, abilities, magic items, etc. Same shape regardless of whether the item is embedded inside an actor's inventory, returned standalone from a future /v1/items endpoint, or pulled from a compendium.",
        "properties": {
          "id": {
            "description": "Foundry item id. Unique within its containing collection (an actor's inventory or a world-level items collection).",
            "type": "string"
          },
          "image": {
            "description": "Path or URL to the item portrait. Whatever the Foundry module emits — typically a path relative to the asset store.",
            "type": "string"
          },
          "name": {
            "description": "Human-readable item name.",
            "type": "string"
          },
          "system": {
            "additionalProperties": true,
            "description": "Game-system-specific data blob. Schema depends on the world's game system (dnd5e items carry damage/range/uses, pf2e carry traits, etc.). Opaque to this API; clients that know the system traverse keys directly.",
            "type": "object"
          },
          "type": {
            "description": "Game-system-specific item type. Common dnd5e values: `weapon`, `equipment`, `consumable`, `tool`, `loot`, `background`, `class`, `subclass`, `spell`, `feat`, `race`. Other systems define their own — free-form, no enum.",
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "type",
          "image",
          "system"
        ],
        "type": "object"
      },
      "ItemSearchResponse": {
        "description": "Paginated list of item refs matching the filter query. Empty results return `data: []`.",
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/WorldItemRef"
            },
            "type": "array"
          },
          "pagination": {
            "$ref": "#/components/schemas/Pagination"
          }
        },
        "required": [
          "data",
          "pagination"
        ],
        "type": "object"
      },
      "Journal": {
        "description": "Journal entry — named bucket of pages. **`folder` is the folder NAME string**, NOT a folder id (mirrors Foundry's journal-sidebar grouping). `null` means the journal lives at root (rendered as 'Uncategorized').",
        "properties": {
          "folder": {
            "description": "Folder NAME (not id); null = root.",
            "nullable": true,
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "pages": {
            "items": {
              "$ref": "#/components/schemas/JournalPage"
            },
            "type": "array"
          }
        },
        "required": [
          "id",
          "name",
          "pages"
        ],
        "type": "object"
      },
      "JournalListResponse": {
        "description": "List envelope. `data` is always a non-nil array — empty when filter matches nothing.",
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/Journal"
            },
            "type": "array"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "JournalPage": {
        "description": "Single page inside a journal entry. `type` is open string on read — Foundry returns `text`/`image`/`video`/`pdf`/`map` and possibly more.\n\nText pages populate `text` (raw HTML), `markdown` (auto-converted), and `enriched_text` (Foundry-resolved with @UUID links). Media pages populate `src` with the asset URL.",
        "properties": {
          "enriched_text": {
            "description": "HTML with Foundry's @UUID references resolved to anchor tags.",
            "nullable": true,
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "markdown": {
            "description": "Markdown rendering of HTML `text`. Convenient for LLMs.",
            "nullable": true,
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "sort": {
            "description": "Foundry's display ordering integer.",
            "type": "integer"
          },
          "src": {
            "description": "Asset URL for image/video/pdf pages.",
            "nullable": true,
            "type": "string"
          },
          "text": {
            "nullable": true,
            "type": "string"
          },
          "type": {
            "description": "Page type. See JournalPageType for the create-side enum; reads may surface other values.",
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "type"
        ],
        "type": "object"
      },
      "JournalPageResponse": {
        "description": "Single-page envelope, used by GET /pages/{pageId} / POST /pages / PATCH /pages/{pageId}.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/JournalPage"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "JournalPageType": {
        "description": "Page type for CREATE only. Foundry's read side may return additional system-generated types (e.g. `map`) — `JournalPage.type` on responses is plain string and may be any of these or others.",
        "enum": [
          "text",
          "image",
          "video",
          "pdf"
        ],
        "type": "string"
      },
      "JournalResponse": {
        "description": "Single-journal envelope, used by GET / POST / PATCH.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Journal"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "Macro": {
        "description": "Macro summary returned by `GET /v1/macros`. **Folder asymmetry** (same as world items): on reads `folder` is the folder NAME (null = root); on writes (`folder_id` in create / update) you pass the folder ID from `GET /v1/folders?type=Macro`.",
        "properties": {
          "author_id": {
            "description": "Id of the user who authored the macro; null when that user no longer exists.",
            "nullable": true,
            "type": "string"
          },
          "folder": {
            "description": "Folder NAME (not id); null when the macro sits at the root.",
            "nullable": true,
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "img": {
            "description": "Icon path.",
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "scope": {
            "$ref": "#/components/schemas/MacroScope"
          },
          "type": {
            "$ref": "#/components/schemas/MacroType"
          },
          "uuid": {
            "description": "Foundry UUID, e.g. `Macro.\u003cid\u003e`.",
            "type": "string"
          }
        },
        "required": [
          "id",
          "uuid",
          "name",
          "type",
          "img",
          "scope",
          "folder",
          "author_id"
        ],
        "type": "object"
      },
      "MacroDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Macro"
          },
          {
            "properties": {
              "command": {
                "description": "The macro body: chat text for `chat` macros, JavaScript source for `script` macros.",
                "type": "string"
              }
            },
            "required": [
              "command"
            ],
            "type": "object"
          }
        ],
        "description": "Macro with its command text — returned by get / create / update."
      },
      "MacroListResponse": {
        "description": "List of macro summaries — `GET /v1/macros` (optionally filtered by type). No `command` text; fetch one macro for its source.",
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/Macro"
            },
            "type": "array"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "MacroResponse": {
        "description": "Single-macro response wrapper (get / create / update) — carries the MacroDetail including `command`.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/MacroDetail"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "MacroScope": {
        "description": "Foundry macro scope: `global` (default for new macros), `actors`, or `actor`.",
        "enum": [
          "global",
          "actors",
          "actor"
        ],
        "type": "string"
      },
      "MacroType": {
        "description": "Kind of macro. `chat` — the command is chat text posted as if typed (e.g. `/roll 1d20`). `script` — the command is JavaScript that runs with the GM's rights; the bridge module accepts script macros (create / update / execute) only while its world setting **Allow Script Macros** is on (off by default — the user enables it in Foundry's module settings; the API cannot). Otherwise the call returns 409.",
        "enum": [
          "chat",
          "script"
        ],
        "type": "string"
      },
      "MoveTokenRequest": {
        "description": "Reposition a token. `x`, `y` required (pixel coordinates). Optional `animate` (default true), `can_open_doors` (default false) for A* pathfinding behaviour. Optional `elevation` / `rotation` apply at the final waypoint.",
        "properties": {
          "animate": {
            "description": "Animate the final move. Default true. Intermediate A* waypoints jump regardless.",
            "type": "boolean"
          },
          "can_open_doors": {
            "description": "Allow opening closed doors encountered along the A* path. Default false. Opened doors stay open after the move.",
            "type": "boolean"
          },
          "elevation": {
            "type": "integer"
          },
          "rotation": {
            "type": "integer"
          },
          "scene_id": {
            "minLength": 1,
            "type": "string"
          },
          "x": {
            "type": "integer"
          },
          "y": {
            "type": "integer"
          }
        },
        "required": [
          "x",
          "y"
        ],
        "type": "object"
      },
      "NearbyTokenInfo": {
        "description": "Token close to the current combatant. Sorted by distance ascending. `line_of_sight` is computed via Foundry's polygon raycast (true = walls don't block sight).",
        "properties": {
          "ac": {
            "nullable": true,
            "type": "integer"
          },
          "actor_id": {
            "nullable": true,
            "type": "string"
          },
          "conditions": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "disposition": {
            "enum": [
              "hostile",
              "neutral",
              "friendly"
            ],
            "type": "string"
          },
          "distance_ft": {
            "description": "Chebyshev distance × scene grid distance (5ft per cell in standard DnD).",
            "format": "double",
            "type": "number"
          },
          "grid_x": {
            "type": "integer"
          },
          "grid_y": {
            "type": "integer"
          },
          "hp": {
            "$ref": "#/components/schemas/HPInfo",
            "nullable": true
          },
          "line_of_sight": {
            "type": "boolean"
          },
          "name": {
            "type": "string"
          },
          "token_id": {
            "type": "string"
          }
        },
        "required": [
          "token_id",
          "name",
          "grid_x",
          "grid_y",
          "distance_ft",
          "disposition",
          "line_of_sight",
          "conditions"
        ],
        "type": "object"
      },
      "Note": {
        "description": "Map-pinned note on a scene's canvas. May be anchored to a journal entry (`entry_id` + optional `page_id`), carry free-text (`text`), or both. Visual fields (`icon_src`, `icon_tint`, `icon_size`, `font_size`, `text_anchor`, `text_color`) control rendering; `global: true` shows the note to all users (vs GM-only).",
        "properties": {
          "entry_id": {
            "description": "Bound journal entry id. null when this is a free-text note.",
            "nullable": true,
            "type": "string"
          },
          "font_size": {
            "type": "integer"
          },
          "global": {
            "description": "True = visible to all users; false = GM-only.",
            "type": "boolean"
          },
          "icon_size": {
            "type": "integer"
          },
          "icon_src": {
            "description": "Icon image URL.",
            "type": "string"
          },
          "icon_tint": {
            "description": "Hex RGB tint applied to the icon, or null for no tint.",
            "nullable": true,
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "page_id": {
            "description": "Bound journal page id (within entry_id). null when not page-specific.",
            "nullable": true,
            "type": "string"
          },
          "text": {
            "description": "Free-text label / annotation. null when sourced from the bound journal entry's name.",
            "nullable": true,
            "type": "string"
          },
          "text_anchor": {
            "$ref": "#/components/schemas/NoteTextAnchor"
          },
          "text_color": {
            "description": "Hex RGB color for the label text, or null for default.",
            "nullable": true,
            "type": "string"
          },
          "x": {
            "type": "integer"
          },
          "y": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "x",
          "y",
          "icon_src",
          "icon_size",
          "font_size",
          "text_anchor",
          "global"
        ],
        "type": "object"
      },
      "NoteListResponse": {
        "description": "Scene-scoped note list envelope.",
        "properties": {
          "data": {
            "properties": {
              "notes": {
                "items": {
                  "$ref": "#/components/schemas/Note"
                },
                "type": "array"
              },
              "scene_id": {
                "type": "string"
              }
            },
            "required": [
              "scene_id",
              "notes"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "NoteResponse": {
        "description": "Single-note envelope. Used by POST / PATCH.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Note"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "NoteTextAnchor": {
        "description": "Foundry text anchor position relative to the note icon.",
        "enum": [
          "center",
          "bottom",
          "top",
          "left",
          "right"
        ],
        "type": "string"
      },
      "Pagination": {
        "description": "Pagination metadata accompanying a paginated array response. Always sibling of `data` on list endpoints; never appears on single-object responses.",
        "properties": {
          "has_more": {
            "description": "True iff `offset + len(data) \u003c total`. Convenience for clients that just need to know whether to issue another request; equivalent to that arithmetic but pre-computed.",
            "type": "boolean"
          },
          "limit": {
            "description": "Page size used for this response. Echoes back what the server applied — may be smaller than the client's requested limit if it exceeded the per-endpoint cap.",
            "minimum": 0,
            "type": "integer"
          },
          "offset": {
            "description": "Zero-based index of the first item in `data`.",
            "minimum": 0,
            "type": "integer"
          },
          "total": {
            "description": "Total number of items matching the query, before pagination.",
            "minimum": 0,
            "type": "integer"
          }
        },
        "required": [
          "limit",
          "offset",
          "total",
          "has_more"
        ],
        "type": "object"
      },
      "Pf2eCastSpell": {
        "description": "Cast result.",
        "properties": {
          "cast": {
            "description": "Always true on success.",
            "type": "boolean"
          },
          "rank": {
            "description": "Effective rank the spell was cast at.",
            "type": "integer"
          },
          "spell_id": {
            "type": "string"
          },
          "spell_name": {
            "type": "string"
          }
        },
        "required": [
          "spell_id",
          "spell_name",
          "rank",
          "cast"
        ],
        "type": "object"
      },
      "Pf2eCastSpellRequest": {
        "description": "Cast a Pathfinder 2e spell the actor owns.",
        "properties": {
          "actor_id": {
            "description": "Foundry actor id within the calling tenant's world.",
            "minLength": 1,
            "type": "string"
          },
          "rank": {
            "description": "Heightened rank (1–10). Omit to cast at the spell's own rank.",
            "maximum": 10,
            "minimum": 1,
            "type": "integer"
          },
          "show_in_chat": {
            "default": true,
            "description": "Post the spell card to Foundry chat. Default true for this command.",
            "type": "boolean"
          },
          "spell_id": {
            "description": "Item id of a spell on the actor.",
            "minLength": 1,
            "type": "string"
          }
        },
        "required": [
          "actor_id",
          "spell_id"
        ],
        "type": "object"
      },
      "Pf2eCastSpellResponse": {
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Pf2eCastSpell"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "Pf2eConditionList": {
        "description": "An actor's active conditions.",
        "properties": {
          "actor_id": {
            "type": "string"
          },
          "actor_name": {
            "type": "string"
          },
          "conditions": {
            "items": {
              "$ref": "#/components/schemas/Pf2eConditionState"
            },
            "type": "array"
          }
        },
        "required": [
          "actor_id",
          "actor_name",
          "conditions"
        ],
        "type": "object"
      },
      "Pf2eConditionListResponse": {
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Pf2eConditionList"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "Pf2eConditionMutation": {
        "description": "Condition state after a set / increase / decrease. `condition` is null when the condition is no longer on the actor.",
        "properties": {
          "actor_id": {
            "type": "string"
          },
          "condition": {
            "allOf": [
              {
                "$ref": "#/components/schemas/Pf2eConditionState"
              }
            ],
            "nullable": true
          }
        },
        "required": [
          "actor_id",
          "condition"
        ],
        "type": "object"
      },
      "Pf2eConditionMutationResponse": {
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Pf2eConditionMutation"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "Pf2eConditionRemoval": {
        "description": "Remove-condition result.",
        "properties": {
          "actor_id": {
            "type": "string"
          },
          "removed": {
            "description": "True when the condition is no longer on the actor.",
            "type": "boolean"
          },
          "slug": {
            "type": "string"
          }
        },
        "required": [
          "actor_id",
          "slug",
          "removed"
        ],
        "type": "object"
      },
      "Pf2eConditionRemovalResponse": {
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Pf2eConditionRemoval"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "Pf2eConditionSlugRequest": {
        "description": "Name a single Pathfinder 2e condition to mutate (increase, decrease, remove).",
        "properties": {
          "actor_id": {
            "description": "Foundry actor id within the calling tenant's world.",
            "minLength": 1,
            "type": "string"
          },
          "slug": {
            "description": "PF2E condition slug (e.g. `frightened`, `off-guard`, `prone`).",
            "type": "string"
          }
        },
        "required": [
          "actor_id",
          "slug"
        ],
        "type": "object"
      },
      "Pf2eConditionState": {
        "description": "One PF2E condition on an actor.",
        "properties": {
          "active": {
            "description": "Whether the condition is currently in effect.",
            "type": "boolean"
          },
          "name": {
            "description": "Localized condition name.",
            "type": "string"
          },
          "slug": {
            "description": "Condition slug.",
            "type": "string"
          },
          "value": {
            "description": "Numeric value for valued conditions; null for binary conditions.",
            "nullable": true,
            "type": "integer"
          }
        },
        "required": [
          "slug",
          "name",
          "value",
          "active"
        ],
        "type": "object"
      },
      "Pf2eDiceGroup": {
        "description": "One homogeneous group of dice from a PF2E roll (e.g. {d20,1,[17]}).",
        "properties": {
          "count": {
            "description": "How many dice of this type were rolled.",
            "type": "integer"
          },
          "results": {
            "description": "Individual rolled values.",
            "items": {
              "type": "integer"
            },
            "type": "array"
          },
          "type": {
            "description": "Die type — d20, d8, etc.",
            "type": "string"
          }
        },
        "required": [
          "type",
          "count",
          "results"
        ],
        "type": "object"
      },
      "Pf2eGetConditionsRequest": {
        "description": "List a Pathfinder 2e actor's active conditions.",
        "properties": {
          "actor_id": {
            "description": "Foundry actor id within the calling tenant's world.",
            "minLength": 1,
            "type": "string"
          }
        },
        "required": [
          "actor_id"
        ],
        "type": "object"
      },
      "Pf2eListStrikesRequest": {
        "description": "Request the Strikes available to a Pathfinder 2e actor.",
        "properties": {
          "actor_id": {
            "description": "Foundry actor id within the calling tenant's world.",
            "minLength": 1,
            "type": "string"
          }
        },
        "required": [
          "actor_id"
        ],
        "type": "object"
      },
      "Pf2ePostItem": {
        "description": "Post result.",
        "properties": {
          "chat_message_id": {
            "description": "Id of the created chat message, or null when nothing was posted.",
            "nullable": true,
            "type": "string"
          },
          "item_id": {
            "type": "string"
          },
          "item_name": {
            "type": "string"
          },
          "item_type": {
            "type": "string"
          },
          "posted": {
            "description": "Always true on success.",
            "type": "boolean"
          }
        },
        "required": [
          "item_id",
          "item_name",
          "item_type",
          "posted",
          "chat_message_id"
        ],
        "type": "object"
      },
      "Pf2ePostItemRequest": {
        "description": "Post any item the actor owns to chat.",
        "properties": {
          "actor_id": {
            "description": "Foundry actor id within the calling tenant's world.",
            "minLength": 1,
            "type": "string"
          },
          "item_id": {
            "description": "Item id on the actor.",
            "minLength": 1,
            "type": "string"
          },
          "show_in_chat": {
            "default": true,
            "description": "Post the item card to Foundry chat. Default true for this command.",
            "type": "boolean"
          }
        },
        "required": [
          "actor_id",
          "item_id"
        ],
        "type": "object"
      },
      "Pf2ePostItemResponse": {
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Pf2ePostItem"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "Pf2eRoll": {
        "description": "Pathfinder 2e roll result. Shared by skill, save, perception, and strike attack/damage rolls.",
        "properties": {
          "dice": {
            "description": "Per-group dice breakdown. Empty array for purely arithmetic results.",
            "items": {
              "$ref": "#/components/schemas/Pf2eDiceGroup"
            },
            "type": "array"
          },
          "formula": {
            "description": "Echo of the rolled formula, normalised by the module.",
            "type": "string"
          },
          "is_critical": {
            "description": "PF2E critical success (degree 3). Omitted when false — absence means not a critical success (and is always absent on damage rolls).",
            "type": "boolean"
          },
          "is_fumble": {
            "description": "PF2E critical failure (degree 0). Omitted when false; always absent on damage rolls.",
            "type": "boolean"
          },
          "total": {
            "description": "Final roll total.",
            "format": "double",
            "type": "number"
          }
        },
        "required": [
          "total",
          "formula",
          "dice"
        ],
        "type": "object"
      },
      "Pf2eRollPerceptionRequest": {
        "description": "Pathfinder 2e perception check. Perception is a single actor-wide statistic, so only the actor is required.",
        "properties": {
          "actor_id": {
            "description": "Foundry actor id within the calling tenant's world.",
            "minLength": 1,
            "type": "string"
          },
          "show_in_chat": {
            "default": false,
            "description": "Post the roll to the world's Foundry chat. Default false (silent).",
            "type": "boolean"
          }
        },
        "required": [
          "actor_id"
        ],
        "type": "object"
      },
      "Pf2eRollResponse": {
        "description": "Envelope wrapping a single Pf2eRoll under `data`, matching the convention used by every other endpoint.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Pf2eRoll"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "Pf2eRollSaveRequest": {
        "description": "Pathfinder 2e saving throw. The save slug is one of `fortitude`, `reflex`, `will` — validated up front (422 on unknown).",
        "properties": {
          "actor_id": {
            "description": "Foundry actor id within the calling tenant's world.",
            "minLength": 1,
            "type": "string"
          },
          "save": {
            "description": "PF2E save slug.",
            "type": "string"
          },
          "show_in_chat": {
            "default": false,
            "description": "Post the roll to the world's Foundry chat. Default false (silent).",
            "type": "boolean"
          }
        },
        "required": [
          "actor_id",
          "save"
        ],
        "type": "object"
      },
      "Pf2eRollSkillRequest": {
        "description": "Pathfinder 2e skill check. The module computes the check from the actor's skill proficiency and returns a degree-of-success-aware roll.\n\n**Skill is a PF2E slug (full lowercase name)** — one of the 16 core skills: `acrobatics`, `arcana`, `athletics`, `crafting`, `deception`, `diplomacy`, `intimidation`, `medicine`, `nature`, `occultism`, `performance`, `religion`, `society`, `stealth`, `survival`, `thievery`. Unlike the dnd5e endpoint (opaque 3-letter code) the slug is validated up front — unknown values yield 422.",
        "properties": {
          "actor_id": {
            "description": "Foundry actor id within the calling tenant's world.",
            "minLength": 1,
            "type": "string"
          },
          "show_in_chat": {
            "default": false,
            "description": "Post the roll to the world's Foundry chat. Default false (silent).",
            "type": "boolean"
          },
          "skill": {
            "description": "PF2E skill slug (full lowercase name).",
            "type": "string"
          }
        },
        "required": [
          "actor_id",
          "skill"
        ],
        "type": "object"
      },
      "Pf2eRollStrikeDamageRequest": {
        "description": "Pathfinder 2e Strike damage roll. `slug` is a weapon/unarmed strike slug from `/pf2e/strikes/list`.",
        "properties": {
          "actor_id": {
            "description": "Foundry actor id within the calling tenant's world.",
            "minLength": 1,
            "type": "string"
          },
          "critical": {
            "default": false,
            "description": "Roll critical damage (PF2E doubling rules). Default false.",
            "type": "boolean"
          },
          "show_in_chat": {
            "default": false,
            "description": "Post the roll to the world's Foundry chat. Default false (silent).",
            "type": "boolean"
          },
          "slug": {
            "description": "Strike slug from list-strikes (e.g. `longsword`, `fist`).",
            "minLength": 1,
            "type": "string"
          }
        },
        "required": [
          "actor_id",
          "slug"
        ],
        "type": "object"
      },
      "Pf2eRollStrikeRequest": {
        "description": "Pathfinder 2e Strike attack roll. `slug` is a weapon/unarmed strike slug from `/pf2e/strikes/list` (not an index).",
        "properties": {
          "actor_id": {
            "description": "Foundry actor id within the calling tenant's world.",
            "minLength": 1,
            "type": "string"
          },
          "map_increase": {
            "description": "Multiple-attack-penalty step: 0 (none), 1 (−5 / −4 agile), 2 (−10 / −8 agile). Default 0.",
            "maximum": 2,
            "minimum": 0,
            "type": "integer"
          },
          "show_in_chat": {
            "default": false,
            "description": "Post the roll to the world's Foundry chat. Default false (silent).",
            "type": "boolean"
          },
          "slug": {
            "description": "Strike slug from list-strikes (e.g. `longsword`, `fist`).",
            "minLength": 1,
            "type": "string"
          }
        },
        "required": [
          "actor_id",
          "slug"
        ],
        "type": "object"
      },
      "Pf2eSetConditionRequest": {
        "description": "Set a Pathfinder 2e condition to an exact state. `value` applies only to valued conditions.",
        "properties": {
          "actor_id": {
            "description": "Foundry actor id within the calling tenant's world.",
            "minLength": 1,
            "type": "string"
          },
          "slug": {
            "description": "PF2E condition slug (e.g. `frightened`, `off-guard`, `prone`).",
            "type": "string"
          },
          "value": {
            "description": "Exact value for a valued condition (e.g. frightened 2). Omit for the module default (1). Ignored for binary conditions.",
            "minimum": 1,
            "type": "integer"
          }
        },
        "required": [
          "actor_id",
          "slug"
        ],
        "type": "object"
      },
      "Pf2eStrikeList": {
        "description": "The Strikes available to an actor.",
        "properties": {
          "actor_id": {
            "type": "string"
          },
          "actor_name": {
            "type": "string"
          },
          "strikes": {
            "items": {
              "$ref": "#/components/schemas/Pf2eStrikeSummary"
            },
            "type": "array"
          }
        },
        "required": [
          "actor_id",
          "actor_name",
          "strikes"
        ],
        "type": "object"
      },
      "Pf2eStrikeListResponse": {
        "description": "Envelope wrapping a Pf2eStrikeList under `data`.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Pf2eStrikeList"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "Pf2eStrikeSummary": {
        "description": "One Strike available to an actor.",
        "properties": {
          "label": {
            "description": "Human-readable strike name.",
            "type": "string"
          },
          "ready": {
            "description": "Whether the strike is currently usable (e.g. weapon equipped).",
            "type": "boolean"
          },
          "slug": {
            "description": "Strike slug — pass to /pf2e/strikes/roll (e.g. `longsword`, `fist`).",
            "type": "string"
          },
          "variants": {
            "description": "MAP-step labels by index (e.g. [\"+14\", \"+9\", \"+4\"]).",
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "slug",
          "label",
          "ready",
          "variants"
        ],
        "type": "object"
      },
      "Pf2eUseConsumable": {
        "description": "Use result.",
        "properties": {
          "consumed": {
            "description": "Always true on success.",
            "type": "boolean"
          },
          "item_id": {
            "type": "string"
          },
          "item_name": {
            "type": "string"
          },
          "remaining_quantity": {
            "description": "Stack quantity left, or null when the item has no stack quantity.",
            "nullable": true,
            "type": "integer"
          },
          "remaining_uses": {
            "description": "Charges left, or null when the item has no charge track.",
            "nullable": true,
            "type": "integer"
          }
        },
        "required": [
          "item_id",
          "item_name",
          "consumed",
          "remaining_uses",
          "remaining_quantity"
        ],
        "type": "object"
      },
      "Pf2eUseConsumableRequest": {
        "description": "Consume a charge of a consumable the actor owns.",
        "properties": {
          "actor_id": {
            "description": "Foundry actor id within the calling tenant's world.",
            "minLength": 1,
            "type": "string"
          },
          "item_id": {
            "description": "Item id of a consumable on the actor.",
            "minLength": 1,
            "type": "string"
          },
          "quantity": {
            "description": "How many to consume. Default 1.",
            "minimum": 1,
            "type": "integer"
          }
        },
        "required": [
          "actor_id",
          "item_id"
        ],
        "type": "object"
      },
      "Pf2eUseConsumableResponse": {
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Pf2eUseConsumable"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "ResetRollTableResponse": {
        "description": "Reset confirmation envelope. `reset_count` is the number of entries whose `drawn` flag was cleared.",
        "properties": {
          "data": {
            "properties": {
              "reset_count": {
                "type": "integer"
              },
              "table_id": {
                "type": "string"
              },
              "table_name": {
                "type": "string"
              }
            },
            "required": [
              "table_id",
              "table_name",
              "reset_count"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "RollAllInitiativeRequest": {
        "description": "Roll initiative for every combatant in a combat (or NPCs only when `npcs_only=true`).\n\n`combat_id` is optional — when omitted the module uses the world's active combat. `npcs_only=true` restricts the roll to combatants whose actor is NOT a player character (system-specific).",
        "properties": {
          "combat_id": {
            "description": "Combat (encounter) id. When omitted, the module uses the world's active combat.",
            "minLength": 1,
            "type": "string"
          },
          "formula": {
            "description": "Optional override formula. When omitted, Foundry uses each actor's standard initiative formula.",
            "minLength": 1,
            "type": "string"
          },
          "npcs_only": {
            "default": false,
            "description": "When true, only roll initiative for combatants whose actor is not a player character. Useful so DM can roll NPCs while players roll their own.",
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "RollAttackRequest": {
        "description": "Weapon-bound attack roll. Module looks up the item's `attack` activity and rolls 1d20 + actor's attack bonus (proficiency + ability + magic).\n\n**`item_id` must reference a weapon (or attack-spell) the actor owns** — get it from `GET /v1/actors/{id}` (look at `items[].id` for entries whose `system.activities` contains an attack activity). Items without an attack activity (consumables, features, non-attack spells) yield 502 with a module reason.\n\n**`advantage` and `disadvantage` are independent booleans.** Both true is allowed — Foundry cancels them to a normal roll (dnd5e standard).",
        "properties": {
          "actor_id": {
            "description": "Foundry actor id within the calling tenant's world.",
            "minLength": 1,
            "type": "string"
          },
          "advantage": {
            "default": false,
            "description": "Roll with advantage (2d20kh1). Combined with `disadvantage: true` cancels to a normal roll.",
            "type": "boolean"
          },
          "disadvantage": {
            "default": false,
            "description": "Roll with disadvantage (2d20kl1). Combined with `advantage: true` cancels to a normal roll.",
            "type": "boolean"
          },
          "item_id": {
            "description": "Item id (weapon or attack-spell) owned by the actor.",
            "minLength": 1,
            "type": "string"
          },
          "show_in_chat": {
            "default": false,
            "description": "Post the roll to the world's Foundry chat. Default false (silent).",
            "type": "boolean"
          }
        },
        "required": [
          "actor_id",
          "item_id"
        ],
        "type": "object"
      },
      "RollAttributeRequest": {
        "description": "Actor-bound ability check (called \"attribute\" in our API for product naming; equivalent to dnd5e's ability check).\n\n**Ability is dnd5e-specific.** Valid values: `str`, `dex`, `con`, `int`, `wis`, `cha`. Unknown codes are rejected at the API layer with 422.",
        "properties": {
          "ability": {
            "description": "dnd5e ability short code: `str`, `dex`, `con`, `int`, `wis`, or `cha`.",
            "type": "string"
          },
          "actor_id": {
            "description": "Foundry actor id within the calling tenant's world.",
            "minLength": 1,
            "type": "string"
          },
          "advantage": {
            "default": false,
            "description": "Roll with advantage (2d20kh1). Honoured by bridge module 8.13+; older modules ignore it. Rejected with 422 together with `disadvantage: true`.",
            "type": "boolean"
          },
          "disadvantage": {
            "default": false,
            "description": "Roll with disadvantage (2d20kl1). Honoured by bridge module 8.13+; older modules ignore it. Rejected with 422 together with `advantage: true`.",
            "type": "boolean"
          },
          "show_in_chat": {
            "default": false,
            "description": "Post the roll to the world's Foundry chat. Default false (silent).",
            "type": "boolean"
          }
        },
        "required": [
          "actor_id",
          "ability"
        ],
        "type": "object"
      },
      "RollDamageRequest": {
        "description": "Weapon-bound damage roll. Module looks up the item's `attack` activity and rolls the damage formula — base dice + ability modifier + magic bonuses.\n\n**`critical: true` doubles the dice** (Foundry standard crit damage handling) and the response's `is_critical` flag is set to true regardless of any natural d20 in the dice. Use this when the preceding `roll-attack` came back as crit (or when you simply want to force a crit damage roll).\n\n**`is_fumble` is never set on damage rolls** — the concept doesn't apply.",
        "properties": {
          "actor_id": {
            "description": "Foundry actor id within the calling tenant's world.",
            "minLength": 1,
            "type": "string"
          },
          "critical": {
            "default": false,
            "description": "Force critical-hit damage (double dice). Sets `is_critical: true` on the response. Use after a successful crit attack roll.",
            "type": "boolean"
          },
          "item_id": {
            "description": "Item id (weapon or attack-spell) owned by the actor.",
            "minLength": 1,
            "type": "string"
          },
          "show_in_chat": {
            "default": false,
            "description": "Post the roll to the world's Foundry chat. Default false (silent).",
            "type": "boolean"
          }
        },
        "required": [
          "actor_id",
          "item_id"
        ],
        "type": "object"
      },
      "RollInitiativeRequest": {
        "description": "Roll initiative for the listed combatants in a combat. Combatant ids are NOT actor ids — get them from `GET /v1/combats/{id}` → `combatants[]`.\n\n`combat_id` is optional — when omitted the module uses the world's active combat. `formula` is optional — when omitted Foundry uses the actor's standard initiative formula (system-specific).",
        "properties": {
          "combat_id": {
            "description": "Combat (encounter) id. When omitted, the module uses the world's active combat.",
            "minLength": 1,
            "type": "string"
          },
          "combatant_ids": {
            "description": "Combatant ids (NOT actor ids). At least one required.",
            "items": {
              "minLength": 1,
              "type": "string"
            },
            "minItems": 1,
            "type": "array"
          },
          "formula": {
            "description": "Optional override formula (e.g. `1d20 + 5`). When omitted, Foundry uses the actor's standard initiative formula.",
            "minLength": 1,
            "type": "string"
          }
        },
        "required": [
          "combatant_ids"
        ],
        "type": "object"
      },
      "RollOnTableRequest": {
        "description": "Roll on a table. `display_chat` MUST be sent — clients must explicitly choose whether the roll is broadcast to Foundry chat. No default. Absent / null → 422. Modeled as an optional+validated nullable pointer because OpenAPI's `required` interacts with `bool` zero-value via go-playground/validator: marking required would reject `false` as 'missing'.",
        "properties": {
          "display_chat": {
            "description": "True = broadcast roll and result to Foundry chat. False = silent (server-side only). Required — must be sent explicitly.",
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "RollOnTableResponse": {
        "description": "Roll outcome. `data.results` is an ARRAY — overlapping ranges can yield multiple matched entries from one roll.",
        "properties": {
          "data": {
            "properties": {
              "results": {
                "items": {
                  "$ref": "#/components/schemas/RollTableEntry"
                },
                "type": "array"
              },
              "roll": {
                "properties": {
                  "formula": {
                    "type": "string"
                  },
                  "total": {
                    "type": "integer"
                  }
                },
                "required": [
                  "formula",
                  "total"
                ],
                "type": "object"
              },
              "table_id": {
                "type": "string"
              },
              "table_name": {
                "type": "string"
              }
            },
            "required": [
              "table_id",
              "table_name",
              "roll",
              "results"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "RollSavingRequest": {
        "description": "Actor-bound saving throw. Module applies the actor's proficiency + ability modifier per class/race/feat configuration.\n\n**Ability is dnd5e-specific.** Valid values: `str`, `dex`, `con`, `int`, `wis`, `cha`. Unknown codes are rejected at the API layer with 422.",
        "properties": {
          "ability": {
            "description": "dnd5e ability short code for the save: `str`, `dex`, `con`, `int`, `wis`, or `cha`.",
            "type": "string"
          },
          "actor_id": {
            "description": "Foundry actor id within the calling tenant's world.",
            "minLength": 1,
            "type": "string"
          },
          "advantage": {
            "default": false,
            "description": "Roll with advantage (2d20kh1). Honoured by bridge module 8.13+; older modules ignore it. Rejected with 422 together with `disadvantage: true`.",
            "type": "boolean"
          },
          "disadvantage": {
            "default": false,
            "description": "Roll with disadvantage (2d20kl1). Honoured by bridge module 8.13+; older modules ignore it. Rejected with 422 together with `advantage: true`.",
            "type": "boolean"
          },
          "show_in_chat": {
            "default": false,
            "description": "Post the roll to the world's Foundry chat. Default false (silent).",
            "type": "boolean"
          }
        },
        "required": [
          "actor_id",
          "ability"
        ],
        "type": "object"
      },
      "RollSkillRequest": {
        "description": "Actor-bound skill check. Module computes the formula from the actor's skill modifier + proficiency.\n\n**Skill is dnd5e-specific.** Valid values: `acr` (Acrobatics), `ani` (Animal Handling), `arc` (Arcana), `ath` (Athletics), `dec` (Deception), `his` (History), `ins` (Insight), `itm` (Intimidation), `inv` (Investigation), `med` (Medicine), `nat` (Nature), `prc` (Perception), `prf` (Performance), `per` (Persuasion), `rel` (Religion), `slt` (Sleight of Hand), `ste` (Stealth), `sur` (Survival). Unknown codes are rejected at the API layer with 422.",
        "properties": {
          "actor_id": {
            "description": "Foundry actor id within the calling tenant's world.",
            "minLength": 1,
            "type": "string"
          },
          "advantage": {
            "default": false,
            "description": "Roll with advantage (2d20kh1). Honoured by bridge module 8.13+; older modules ignore it. Rejected with 422 together with `disadvantage: true`.",
            "type": "boolean"
          },
          "disadvantage": {
            "default": false,
            "description": "Roll with disadvantage (2d20kl1). Honoured by bridge module 8.13+; older modules ignore it. Rejected with 422 together with `advantage: true`.",
            "type": "boolean"
          },
          "show_in_chat": {
            "default": false,
            "description": "Post the roll to the world's Foundry chat. Default false (silent).",
            "type": "boolean"
          },
          "skill": {
            "description": "dnd5e skill short code (see schema description for the full list).",
            "type": "string"
          }
        },
        "required": [
          "actor_id",
          "skill"
        ],
        "type": "object"
      },
      "RollTable": {
        "description": "Full roll table with all entries. `formula` is the dice expression rolled by `POST /roll-tables/{id}/roll`. `replacement: false` makes entries draw-once until reset.",
        "properties": {
          "description": {
            "type": "string"
          },
          "display_roll": {
            "description": "Whether Foundry shows the roll formula and total in chat by default.",
            "type": "boolean"
          },
          "formula": {
            "description": "Dice formula rolled when the table is rolled (e.g. `1d20`, `1d100`).",
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "img": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "replacement": {
            "description": "True = entries can repeat. False = each entry draws at most once until reset.",
            "type": "boolean"
          },
          "results": {
            "items": {
              "$ref": "#/components/schemas/RollTableEntry"
            },
            "type": "array"
          }
        },
        "required": [
          "id",
          "name",
          "img",
          "description",
          "formula",
          "replacement",
          "display_roll",
          "results"
        ],
        "type": "object"
      },
      "RollTableEntry": {
        "description": "One row of a roll table. `type`: 0 = text (text field is the body), 1 = document (in-world reference via `document_collection` + `document_id`), 2 = compendium (compendium reference via `document_collection` + `document_id`). `range` is the `[min, max]` inclusive roll bracket. `drawn` is meaningful only when the parent table has `replacement: false`.",
        "properties": {
          "document_collection": {
            "description": "For document/compendium entries — collection name (e.g. `dnd5e.items`).",
            "nullable": true,
            "type": "string"
          },
          "document_id": {
            "description": "For document/compendium entries — referenced document id.",
            "nullable": true,
            "type": "string"
          },
          "drawn": {
            "description": "True when this entry has been drawn from a no-replacement table. Always false on replacement tables.",
            "type": "boolean"
          },
          "id": {
            "type": "string"
          },
          "img": {
            "type": "string"
          },
          "range": {
            "description": "[min, max] inclusive roll bracket.",
            "items": {
              "type": "integer"
            },
            "maxItems": 2,
            "minItems": 2,
            "type": "array"
          },
          "text": {
            "type": "string"
          },
          "type": {
            "description": "0 = text, 1 = document, 2 = compendium.",
            "enum": [
              0,
              1,
              2
            ],
            "type": "integer"
          },
          "weight": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "type",
          "text",
          "img",
          "range",
          "weight",
          "drawn"
        ],
        "type": "object"
      },
      "RollTableListResponse": {
        "description": "List envelope. `data` is always a non-nil array — empty when the world has no tables.",
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/RollTableSummary"
            },
            "type": "array"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "RollTableResponse": {
        "description": "Single-table envelope. Used by GET, POST, PATCH.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/RollTable"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "RollTableSummary": {
        "description": "Lightweight roll table descriptor used by the list endpoint. `total_results` / `drawn_results` come straight from Foundry; `drawn_results` is only useful when `replacement: false`.",
        "properties": {
          "description": {
            "type": "string"
          },
          "drawn_results": {
            "type": "integer"
          },
          "formula": {
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "img": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "replacement": {
            "type": "boolean"
          },
          "total_results": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "name",
          "img",
          "description",
          "formula",
          "replacement",
          "total_results",
          "drawn_results"
        ],
        "type": "object"
      },
      "Scene": {
        "description": "Full scene with placed content (live canvas state). Returned by `GET /v1/scenes/{id}`. Differs from `SceneSummary` (CRUD shape returned by POST/PATCH/clone): this view has tokens/walls/lights/tiles/drawings/regions but NOT navigation/folder metadata. Use both shapes for their respective needs.",
        "properties": {
          "active": {
            "description": "True for the world's currently active scene.",
            "type": "boolean"
          },
          "darkness": {
            "description": "Ambient darkness (0 = lit, 1 = pitch-black).",
            "format": "double",
            "type": "number"
          },
          "drawings": {
            "items": {
              "$ref": "#/components/schemas/SceneDrawing"
            },
            "type": "array"
          },
          "grid": {
            "$ref": "#/components/schemas/SceneGrid"
          },
          "height": {
            "type": "integer"
          },
          "id": {
            "type": "string"
          },
          "img": {
            "type": "string"
          },
          "lights": {
            "items": {
              "$ref": "#/components/schemas/SceneLight"
            },
            "type": "array"
          },
          "name": {
            "type": "string"
          },
          "notes": {
            "items": {
              "$ref": "#/components/schemas/SceneNote"
            },
            "type": "array"
          },
          "regions": {
            "items": {
              "$ref": "#/components/schemas/SceneRegion"
            },
            "type": "array"
          },
          "tiles": {
            "items": {
              "$ref": "#/components/schemas/SceneTile"
            },
            "type": "array"
          },
          "tokens": {
            "items": {
              "$ref": "#/components/schemas/SceneToken"
            },
            "type": "array"
          },
          "walls": {
            "items": {
              "$ref": "#/components/schemas/SceneWall"
            },
            "type": "array"
          },
          "width": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "name",
          "active",
          "img",
          "width",
          "height",
          "grid",
          "darkness",
          "notes",
          "walls",
          "lights",
          "tiles",
          "drawings",
          "regions",
          "tokens"
        ],
        "type": "object"
      },
      "SceneDrawing": {
        "description": "GM annotation drawn on the canvas (rect / ellipse / polygon / freehand).",
        "properties": {
          "fill_color": {
            "nullable": true,
            "type": "string"
          },
          "hidden": {
            "type": "boolean"
          },
          "shape": {
            "$ref": "#/components/schemas/SceneDrawingShape"
          },
          "stroke_color": {
            "nullable": true,
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "x": {
            "type": "integer"
          },
          "y": {
            "type": "integer"
          }
        },
        "required": [
          "x",
          "y",
          "shape",
          "text",
          "hidden"
        ],
        "type": "object"
      },
      "SceneDrawingShape": {
        "description": "Geometry component of a SceneDrawing. `type` is Foundry's shape primitive (`r` rect, `e` ellipse, `p` polygon, `f` freehand).",
        "properties": {
          "height": {
            "type": "integer"
          },
          "points": {
            "items": {
              "type": "integer"
            },
            "type": "array"
          },
          "type": {
            "type": "string"
          },
          "width": {
            "type": "integer"
          }
        },
        "required": [
          "type",
          "width",
          "height",
          "points"
        ],
        "type": "object"
      },
      "SceneGrid": {
        "description": "Grid configuration on a scene. `type` is the layout enum; `size` is the grid cell size in pixels; `distance` is the in-game distance one cell represents (in `units`).",
        "properties": {
          "distance": {
            "type": "integer"
          },
          "size": {
            "type": "integer"
          },
          "type": {
            "$ref": "#/components/schemas/SceneGridType"
          },
          "units": {
            "type": "string"
          }
        },
        "required": [
          "size",
          "type",
          "units",
          "distance"
        ],
        "type": "object"
      },
      "SceneGridInput": {
        "description": "Grid configuration input for create/update. All fields optional; omitted fields keep Foundry's defaults (create) or current value (update).",
        "properties": {
          "distance": {
            "minimum": 1,
            "type": "integer"
          },
          "size": {
            "minimum": 1,
            "type": "integer"
          },
          "type": {
            "$ref": "#/components/schemas/SceneGridType"
          },
          "units": {
            "minLength": 1,
            "type": "string"
          }
        },
        "type": "object"
      },
      "SceneGridType": {
        "description": "Foundry grid layout. `gridless` = freeform canvas; `square` = standard grid; the four `hex*` variants pair an orientation (pointy / flat) with row-offset parity (odd / even).",
        "enum": [
          "gridless",
          "square",
          "hexPointyOdd",
          "hexPointyEven",
          "hexFlatOdd",
          "hexFlatEven"
        ],
        "type": "string"
      },
      "SceneLight": {
        "description": "Light source placed on the scene. `bright`/`dim` are radii in scene units; `angle` is the emission arc in degrees (360 = omni); `walls` flags whether the light respects walls; `color` is a hex RGB string or null.",
        "properties": {
          "angle": {
            "type": "integer"
          },
          "bright": {
            "type": "integer"
          },
          "color": {
            "nullable": true,
            "type": "string"
          },
          "dim": {
            "type": "integer"
          },
          "hidden": {
            "type": "boolean"
          },
          "walls": {
            "type": "boolean"
          },
          "x": {
            "type": "integer"
          },
          "y": {
            "type": "integer"
          }
        },
        "required": [
          "x",
          "y",
          "bright",
          "dim",
          "angle",
          "walls",
          "hidden"
        ],
        "type": "object"
      },
      "SceneListEntry": {
        "description": "Lightweight scene descriptor used by `GET /v1/scenes`. Just enough to render a picker / nav list.",
        "properties": {
          "active": {
            "type": "boolean"
          },
          "id": {
            "type": "string"
          },
          "img": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "active",
          "img"
        ],
        "type": "object"
      },
      "SceneListResponse": {
        "description": "List envelope. `data` is always a non-nil array — empty when the world has no scenes.",
        "properties": {
          "data": {
            "items": {
              "$ref": "#/components/schemas/SceneListEntry"
            },
            "type": "array"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "SceneNote": {
        "description": "Map-pinned journal annotation. `entry_id` references a journal when the note is anchored to one; null for free-text notes.",
        "properties": {
          "entry_id": {
            "nullable": true,
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "x": {
            "type": "integer"
          },
          "y": {
            "type": "integer"
          }
        },
        "required": [
          "x",
          "y",
          "text",
          "label"
        ],
        "type": "object"
      },
      "SceneRegion": {
        "description": "Tagged map region (Foundry v12+). Used for triggers, weather, lighting overrides.",
        "properties": {
          "color": {
            "nullable": true,
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "shapes": {
            "items": {
              "$ref": "#/components/schemas/SceneRegionShape"
            },
            "type": "array"
          }
        },
        "required": [
          "id",
          "name",
          "shapes"
        ],
        "type": "object"
      },
      "SceneRegionShape": {
        "description": "Sub-shape of a SceneRegion (Foundry v12+ regions support multi-shape composition).",
        "properties": {
          "type": {
            "type": "string"
          }
        },
        "required": [
          "type"
        ],
        "type": "object"
      },
      "SceneResponse": {
        "description": "Single-scene envelope returned by GET /v1/scenes/{id} (full detail including placed content).",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Scene"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "SceneSummary": {
        "description": "CRUD-side scene shape returned by POST / PATCH / clone. Carries navigation, folder, and grid metadata but NOT placed content (call GET for tokens/walls/etc). `nav_name` / `folder` are nullable to express Foundry's clear-on-null semantics.",
        "properties": {
          "active": {
            "type": "boolean"
          },
          "background": {
            "nullable": true,
            "type": "string"
          },
          "folder": {
            "description": "Folder NAME (null = root). Distinct from create/update which take a folder ID.",
            "nullable": true,
            "type": "string"
          },
          "grid": {
            "$ref": "#/components/schemas/SceneGrid"
          },
          "height": {
            "type": "integer"
          },
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "nav_name": {
            "description": "Optional short label for the sidebar navigation entry.",
            "nullable": true,
            "type": "string"
          },
          "nav_order": {
            "type": "integer"
          },
          "navigation": {
            "description": "Whether the scene appears in Foundry's sidebar navigation strip.",
            "type": "boolean"
          },
          "uuid": {
            "type": "string"
          },
          "width": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "uuid",
          "name",
          "active",
          "width",
          "height",
          "navigation",
          "nav_order",
          "grid"
        ],
        "type": "object"
      },
      "SceneSummaryResponse": {
        "description": "Single-scene envelope returned by create / update / clone (CRUD summary).",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/SceneSummary"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "SceneTile": {
        "description": "Image tile placed on the scene canvas. `elevation` controls layering (negative for under-token decor).",
        "properties": {
          "elevation": {
            "type": "integer"
          },
          "height": {
            "type": "integer"
          },
          "hidden": {
            "type": "boolean"
          },
          "img": {
            "type": "string"
          },
          "rotation": {
            "type": "integer"
          },
          "width": {
            "type": "integer"
          },
          "x": {
            "type": "integer"
          },
          "y": {
            "type": "integer"
          }
        },
        "required": [
          "x",
          "y",
          "width",
          "height",
          "img",
          "hidden",
          "elevation",
          "rotation"
        ],
        "type": "object"
      },
      "SceneToken": {
        "description": "Token placed on the scene. `grid_x` / `grid_y` are the cell coordinates (derived from pixel `x`/`y` divided by the scene's grid size); `disposition` is Foundry's int code (-1 hostile, 0 neutral, 1 friendly); `conditions` lists active status-effect ids. `hp` / `ac` are populated only when the actor exposes those system attributes (D\u0026D 5e style).",
        "properties": {
          "ac": {
            "type": "integer"
          },
          "actor_id": {
            "nullable": true,
            "type": "string"
          },
          "conditions": {
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "disposition": {
            "type": "integer"
          },
          "elevation": {
            "type": "integer"
          },
          "grid_x": {
            "type": "integer"
          },
          "grid_y": {
            "type": "integer"
          },
          "hidden": {
            "type": "boolean"
          },
          "hp": {
            "$ref": "#/components/schemas/SceneTokenHP"
          },
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "x": {
            "type": "integer"
          },
          "y": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "name",
          "grid_x",
          "grid_y",
          "x",
          "y",
          "elevation",
          "hidden",
          "disposition",
          "conditions"
        ],
        "type": "object"
      },
      "SceneTokenHP": {
        "description": "Hit-point snapshot for a token's bound actor (when applicable).",
        "properties": {
          "max": {
            "type": "integer"
          },
          "value": {
            "type": "integer"
          }
        },
        "required": [
          "value",
          "max"
        ],
        "type": "object"
      },
      "SceneTokensResponse": {
        "description": "Scene-scoped token list envelope. `data.tokens` is always non-nil (empty array when no tokens).",
        "properties": {
          "data": {
            "properties": {
              "scene_id": {
                "type": "string"
              },
              "scene_name": {
                "type": "string"
              },
              "tokens": {
                "items": {
                  "$ref": "#/components/schemas/Token"
                },
                "type": "array"
              }
            },
            "required": [
              "scene_id",
              "scene_name",
              "tokens"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "SceneWall": {
        "description": "Wall / door segment. `c` is the four-element coordinate array `[x1, y1, x2, y2]`. `move`/`sense` are Foundry's blocking codes; `door` is the door type (0 = none, 1 = door, 2 = secret); `ds` is the door state (0 closed, 1 open, 2 locked).",
        "properties": {
          "c": {
            "items": {
              "type": "integer"
            },
            "type": "array"
          },
          "door": {
            "type": "integer"
          },
          "ds": {
            "type": "integer"
          },
          "id": {
            "type": "string"
          },
          "move": {
            "type": "integer"
          },
          "sense": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "c",
          "move",
          "sense",
          "door",
          "ds"
        ],
        "type": "object"
      },
      "SendChatMessageRequest": {
        "description": "Send a new chat message to the world. Only `content` is required. `actor_id` binds the message to a specific actor (sets the speaker alias to the actor's name); `whisper_to` makes the message private to the listed user IDs. `content` may include HTML, but the read side strips it — round-trip fidelity is not preserved.",
        "properties": {
          "actor_id": {
            "description": "Foundry actor id whose name becomes the speaker. Yields 404 if the actor doesn't exist.",
            "minLength": 1,
            "type": "string"
          },
          "content": {
            "description": "Message body. HTML supported on send, returned as plain text from list/get.",
            "minLength": 1,
            "type": "string"
          },
          "flavor": {
            "description": "Italic header text shown above the message body (e.g. `Stealth Check`).",
            "minLength": 1,
            "type": "string"
          },
          "speaker": {
            "description": "Override the speaker alias (free text). Use `actor_id` for actor-bound speaker.",
            "minLength": 1,
            "type": "string"
          },
          "type": {
            "description": "Message style. Default lets Foundry pick `other`. `ic` for in-character, `ooc` for out-of-character, `emote` for italicised actions.",
            "enum": [
              "ic",
              "ooc",
              "emote"
            ],
            "type": "string"
          },
          "whisper_to": {
            "description": "User IDs to whisper this message to. Empty / omitted = public broadcast.",
            "items": {
              "type": "string"
            },
            "type": "array"
          }
        },
        "required": [
          "content"
        ],
        "type": "object"
      },
      "SendChatMessageResponse": {
        "description": "Lean confirmation envelope returned from `POST /v1/chat/messages` and `PATCH /v1/chat/messages/{id}`.",
        "properties": {
          "data": {
            "properties": {
              "message_id": {
                "type": "string"
              },
              "sent": {
                "type": "boolean"
              }
            },
            "required": [
              "message_id",
              "sent"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "SetCombatantDefeatedRequest": {
        "description": "Mark / unmark a combatant defeated.",
        "properties": {
          "combat_id": {
            "minLength": 1,
            "type": "string"
          },
          "defeated": {
            "type": "boolean"
          }
        },
        "required": [
          "defeated"
        ],
        "type": "object"
      },
      "SetDoorStateRequest": {
        "description": "Change a door's state. `state` is required (use the same string enum as `door_state` elsewhere).",
        "properties": {
          "state": {
            "$ref": "#/components/schemas/WallDoorState"
          }
        },
        "required": [
          "state"
        ],
        "type": "object"
      },
      "SetDoorStateResponse": {
        "description": "Door state change confirmation. `door` echoes the wall's door type (it is NOT changed by this call); `previous_state` and `new_state` show the transition.",
        "properties": {
          "data": {
            "properties": {
              "door": {
                "$ref": "#/components/schemas/WallDoorType"
              },
              "new_state": {
                "$ref": "#/components/schemas/WallDoorState"
              },
              "previous_state": {
                "$ref": "#/components/schemas/WallDoorState"
              },
              "wall_id": {
                "type": "string"
              }
            },
            "required": [
              "wall_id",
              "door",
              "previous_state",
              "new_state"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "SetTokenTargetRequest": {
        "description": "Set or unset target on a token. **`targeted` is required** — explicit choice. Modelled as nullable so the validator can require it (a bare `bool` would reject the literal `false` as the zero value).",
        "properties": {
          "release_others": {
            "description": "When true (bridge default), the GM's other targets are released first. When false, the change is additive (multi-target). Default: true (bridge-side).",
            "type": "boolean"
          },
          "targeted": {
            "description": "True = target, false = untarget. Required.",
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "SetTokenTargetResponse": {
        "description": "Target-state confirmation envelope.",
        "properties": {
          "data": {
            "properties": {
              "targeted": {
                "description": "Echo of the request — true if now targeted, false if untargeted.",
                "type": "boolean"
              },
              "token_id": {
                "type": "string"
              }
            },
            "required": [
              "token_id",
              "targeted"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "SetTurnRequest": {
        "description": "Jump to a specific combatant's turn — does NOT trigger round-based effects (concentration checks, condition decrements, etc).",
        "properties": {
          "combat_id": {
            "minLength": 1,
            "type": "string"
          },
          "combatant_id": {
            "minLength": 1,
            "type": "string"
          }
        },
        "required": [
          "combatant_id"
        ],
        "type": "object"
      },
      "ShowJournalRequest": {
        "description": "Broadcast a journal (or one specific page) to players. `force=true` re-opens it even for users who already dismissed. `users` restricts the broadcast to specific user ids — omit to broadcast to everyone.",
        "properties": {
          "force": {
            "description": "Re-open even for users who already dismissed.",
            "type": "boolean"
          },
          "page_id": {
            "description": "Optional specific page to open. When omitted, the journal opens at its first page.",
            "minLength": 1,
            "type": "string"
          },
          "users": {
            "description": "User ids to broadcast to. Omit / empty = everyone.",
            "items": {
              "minLength": 1,
              "type": "string"
            },
            "type": "array"
          }
        },
        "type": "object"
      },
      "ShowJournalResponse": {
        "description": "Confirmation envelope. `shown:true` indicates Foundry accepted the broadcast (does NOT confirm individual users actually received it).",
        "properties": {
          "data": {
            "properties": {
              "journal_id": {
                "type": "string"
              },
              "journal_name": {
                "type": "string"
              },
              "page_id": {
                "description": "Echo of the page_id from the request, if any.",
                "nullable": true,
                "type": "string"
              },
              "shown": {
                "type": "boolean"
              }
            },
            "required": [
              "shown",
              "journal_id",
              "journal_name"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "TierForbiddenResponse": {
        "description": "Tier-gated 403 response. Distinct from the standard ErrorResponse because clients use the structured fields (current_tier, required_tier, patreon_url, reauth_url) to render an upgrade UI without parsing the message string.",
        "properties": {
          "code": {
            "description": "Machine-readable identifier so clients can match without parsing the message.",
            "enum": [
              "tier_required"
            ],
            "type": "string"
          },
          "current_tier": {
            "description": "Tier resolved from the caller's API key at the time of the request.",
            "enum": [
              "guest",
              "free",
              "adventurer",
              "dm"
            ],
            "type": "string"
          },
          "error": {
            "description": "Short human-readable reason.",
            "type": "string"
          },
          "patreon_url": {
            "description": "Where to upgrade. From server-side configuration.",
            "format": "uri",
            "type": "string"
          },
          "reauth_url": {
            "description": "Where to refresh the API key after upgrading. From server-side configuration.",
            "format": "uri",
            "type": "string"
          },
          "required_tier": {
            "description": "Minimum tier required to access this endpoint.",
            "enum": [
              "guest",
              "free",
              "adventurer",
              "dm"
            ],
            "type": "string"
          }
        },
        "required": [
          "error",
          "code",
          "required_tier",
          "current_tier",
          "patreon_url",
          "reauth_url"
        ],
        "type": "object"
      },
      "Token": {
        "description": "Canonical token shape — unifies the bridge's two divergent wire shapes (`TokenResult` from create/update/move/list and `TokenDetail` from get-token / get-token-by-actor). The Go projection normalises `img`/`textureSrc` → `img`, int/string `disposition` → int, and `hp.{value,max}`/`hp.{current,max}` → `hp.{value,max}`.\n\n`scene_id` is always present in the REST response (stamped from the request when the bridge's `TokenResult` shape doesn't carry it).\n\n`path_cost` / `doors_opened` are populated only by `POST /v1/tokens/{id}/move` when A* pathfinding actually ran.",
        "properties": {
          "ac": {
            "type": "integer"
          },
          "actor_id": {
            "description": "Bound actor id; null for unlinked tokens.",
            "nullable": true,
            "type": "string"
          },
          "conditions": {
            "description": "Active status-effect ids. Always an array; empty when no conditions (or when the wire shape didn't carry them — `get-token` doesn't).",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "disposition": {
            "description": "-2: secret, -1: hostile, 0: neutral, 1: friendly.",
            "enum": [
              -2,
              -1,
              0,
              1
            ],
            "type": "integer"
          },
          "doors_opened": {
            "description": "Wall IDs of doors that were opened en route. Present only when `POST /move` with `can_open_doors: true` actually opened doors.",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "elevation": {
            "type": "integer"
          },
          "height": {
            "description": "Height in grid cells.",
            "type": "integer"
          },
          "hidden": {
            "type": "boolean"
          },
          "hp": {
            "$ref": "#/components/schemas/TokenHP"
          },
          "id": {
            "type": "string"
          },
          "img": {
            "description": "Token texture URL.",
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "path_cost": {
            "description": "Grid cells traversed by A* pathfinding. Present only when `POST /move` ran pathfinding (i.e. direct path was blocked or doors were opened).",
            "type": "integer"
          },
          "rotation": {
            "description": "Rotation in degrees.",
            "type": "integer"
          },
          "scene_id": {
            "description": "Scene this token lives on. Empty string only when the bridge couldn't resolve the active scene at response time (degenerate).",
            "type": "string"
          },
          "width": {
            "description": "Width in grid cells.",
            "type": "integer"
          },
          "x": {
            "description": "Pixel X coordinate on the scene canvas.",
            "type": "integer"
          },
          "y": {
            "description": "Pixel Y coordinate on the scene canvas.",
            "type": "integer"
          }
        },
        "required": [
          "id",
          "scene_id",
          "name",
          "x",
          "y",
          "width",
          "height",
          "elevation",
          "rotation",
          "hidden",
          "img",
          "disposition",
          "conditions"
        ],
        "type": "object"
      },
      "TokenHP": {
        "description": "Hit-point snapshot for a token's bound actor. Canonical shape (`{value, max}`) — wire's `get-token` returns `{current, max}` which is projected to `{value, max}` on our side.",
        "properties": {
          "max": {
            "type": "integer"
          },
          "value": {
            "type": "integer"
          }
        },
        "required": [
          "value",
          "max"
        ],
        "type": "object"
      },
      "TokenRangeEntry": {
        "description": "Lightweight token descriptor for `GET /v1/tokens/in-range`. Carries only the fields useful for tactical queries; call `GET /v1/tokens/{id}` for full detail.",
        "properties": {
          "actor_id": {
            "nullable": true,
            "type": "string"
          },
          "disposition": {
            "enum": [
              -2,
              -1,
              0,
              1
            ],
            "type": "integer"
          },
          "distance": {
            "description": "Distance from origin IN SCENE UNITS (ft / m / etc — see envelope `units`).",
            "format": "double",
            "type": "number"
          },
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "x": {
            "type": "integer"
          },
          "y": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "name",
          "x",
          "y",
          "distance",
          "disposition"
        ],
        "type": "object"
      },
      "TokenResponse": {
        "description": "Single-token envelope. Returned by GET / POST / PATCH and the action endpoints (move).",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Token"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "TokensInRangeResponse": {
        "description": "Range-query envelope. `units` reflects the scene's grid distance unit (typically `ft` / `m`). `tokens` are sorted by distance ascending.",
        "properties": {
          "data": {
            "properties": {
              "origin": {
                "properties": {
                  "x": {
                    "description": "Origin X in pixels (echo of request).",
                    "type": "integer"
                  },
                  "y": {
                    "description": "Origin Y in pixels (echo of request).",
                    "type": "integer"
                  }
                },
                "required": [
                  "x",
                  "y"
                ],
                "type": "object"
              },
              "range": {
                "description": "Range in scene units (echo of request).",
                "format": "double",
                "type": "number"
              },
              "scene_id": {
                "type": "string"
              },
              "tokens": {
                "items": {
                  "$ref": "#/components/schemas/TokenRangeEntry"
                },
                "type": "array"
              },
              "units": {
                "description": "Scene grid distance unit (ft / m / etc).",
                "type": "string"
              }
            },
            "required": [
              "scene_id",
              "origin",
              "range",
              "units",
              "tokens"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "TurnCombatantInfo": {
        "description": "Enriched view of the current combatant — adds grid coords, HP/AC, and active status conditions.",
        "properties": {
          "ac": {
            "nullable": true,
            "type": "integer"
          },
          "actor_id": {
            "type": "string"
          },
          "conditions": {
            "description": "Active status effect ids (e.g. `prone`, `poisoned`, `unconscious`).",
            "items": {
              "type": "string"
            },
            "type": "array"
          },
          "grid_x": {
            "description": "Grid coord (cells), NOT pixels.",
            "type": "integer"
          },
          "grid_y": {
            "type": "integer"
          },
          "hp": {
            "$ref": "#/components/schemas/HPInfo",
            "nullable": true
          },
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "token_id": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "actor_id",
          "token_id",
          "name",
          "grid_x",
          "grid_y",
          "conditions"
        ],
        "type": "object"
      },
      "UpdateChatMessageRequest": {
        "description": "Edit an existing chat message — content and/or flavor only. Speaker / whisper / type are immutable post-create.",
        "properties": {
          "content": {
            "description": "New message body. Omit to leave unchanged.",
            "minLength": 1,
            "type": "string"
          },
          "flavor": {
            "description": "New flavor header. Omit to leave unchanged.",
            "minLength": 1,
            "type": "string"
          }
        },
        "type": "object"
      },
      "UpdateCombatantRequest": {
        "description": "Generic patch — `updates` is passthrough to Foundry's `Combatant.update()`. Common keys: `initiative` (number), `defeated` (bool), `hidden` (bool), plus game-system flags.",
        "properties": {
          "updates": {
            "additionalProperties": true,
            "description": "Free-form passthrough — Foundry validates.",
            "type": "object"
          }
        },
        "required": [
          "updates"
        ],
        "type": "object"
      },
      "UpdateFolderRequest": {
        "description": "Patch a folder — name, parent, color, description, or sort. **`parent_id` is tri-state** (same pattern as items.folder):\n\n- omit both `parent_id` and `clear_parent` → don't touch parent\n- `parent_id: \"abc\"` → set parent to that folder id\n- `clear_parent: true` → move to root (Foundry: parentId=null); takes precedence over `parent_id` if both set\n\nCross-field rule: setting BOTH `parent_id` and `clear_parent: true` returns 422.",
        "properties": {
          "clear_parent": {
            "default": false,
            "description": "Move folder to root. When true, `parent_id` is ignored. Default false.",
            "type": "boolean"
          },
          "color": {
            "minLength": 1,
            "type": "string"
          },
          "description": {
            "minLength": 1,
            "type": "string"
          },
          "name": {
            "minLength": 1,
            "type": "string"
          },
          "parent_id": {
            "description": "New parent folder id. Mutually exclusive with `clear_parent: true`.",
            "minLength": 1,
            "type": "string"
          },
          "sort": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "UpdateItemRequest": {
        "description": "Patch an existing world item. **Folder semantics is tri-state**:\n\n- `folder: \"abc\"` (string) → set folder to id `abc`\n- `clear_folder: true` → move item to root (Foundry: `folder: null`); takes precedence over `folder` if both set\n- both omitted → don't touch folder\n\n`name`, `image`, `system` use standard PATCH semantics — present = set, absent = leave alone. `system` is deep-merged by Foundry.",
        "properties": {
          "clear_folder": {
            "default": false,
            "description": "Move item to root (no folder). When true, `folder` is ignored. Default false.",
            "type": "boolean"
          },
          "folder": {
            "description": "New folder id. Omit + omit clear_folder = leave folder alone. Mutually exclusive with clear_folder=true.",
            "minLength": 1,
            "type": "string"
          },
          "image": {
            "minLength": 1,
            "type": "string"
          },
          "name": {
            "minLength": 1,
            "type": "string"
          },
          "system": {
            "additionalProperties": true,
            "description": "System-data partial. Deep-merged by Foundry — pass only the keys you want to change.",
            "type": "object"
          }
        },
        "type": "object"
      },
      "UpdateJournalPageRequest": {
        "description": "Patch an existing page. All fields optional — empty body is a no-op.",
        "properties": {
          "content": {
            "type": "string"
          },
          "name": {
            "minLength": 1,
            "type": "string"
          },
          "src": {
            "type": "string"
          }
        },
        "type": "object"
      },
      "UpdateJournalRequest": {
        "description": "Patch an existing journal. **`folder` semantics is tri-state**:\n\n- `folder: \"Locations\"` → move journal under folder NAME 'Locations'\n- `clear_folder: true` → move to root (Foundry: `folder: null`); takes precedence over `folder` if both set, but the handler rejects the combination as 422\n- both omitted → leave folder alone\n\n`name` uses standard PATCH semantics (present = set, absent = leave).",
        "properties": {
          "clear_folder": {
            "description": "When true, move journal to root. 422 if combined with `folder`.",
            "type": "boolean"
          },
          "folder": {
            "description": "Folder NAME (not id) to move journal under. See description for tri-state semantics.",
            "minLength": 1,
            "type": "string"
          },
          "name": {
            "minLength": 1,
            "type": "string"
          }
        },
        "type": "object"
      },
      "UpdateMacroRequest": {
        "description": "Patch a macro — name, type, command, scope, image or folder; omitted fields stay unchanged. **`folder_id` is tri-state** (same pattern as folders.parent_id):\n\n- omit both `folder_id` and `clear_folder` → don't touch the folder\n- `folder_id: \"abc\"` → move into that folder (folder ID, NOT name)\n- `clear_folder: true` → move to root (Foundry: folder=null)\n\nCross-field rule: setting BOTH `folder_id` and `clear_folder: true` returns 422. Script macros (the macro is, or becomes, `type: script`) need the module setting **Allow Script Macros** (409 otherwise).",
        "properties": {
          "clear_folder": {
            "default": false,
            "description": "Move the macro to the root. Mutually exclusive with `folder_id`. Default false.",
            "type": "boolean"
          },
          "command": {
            "minLength": 1,
            "type": "string"
          },
          "folder_id": {
            "description": "New folder ID (NOT name). Mutually exclusive with `clear_folder: true`.",
            "minLength": 1,
            "type": "string"
          },
          "img": {
            "minLength": 1,
            "type": "string"
          },
          "name": {
            "minLength": 1,
            "type": "string"
          },
          "scope": {
            "description": "Macro scope — see MacroScope.",
            "enum": [
              "global",
              "actors",
              "actor"
            ],
            "type": "string"
          },
          "type": {
            "description": "Macro type — see MacroType.",
            "enum": [
              "chat",
              "script"
            ],
            "type": "string"
          }
        },
        "type": "object"
      },
      "UpdateNoteRequest": {
        "description": "Patch note properties. Five fields are **tri-state nullable** (`entry_id`, `page_id`, `text`, `icon_tint`, `text_color`):\n\n- omit → leave alone\n- send a value → set\n- send the matching `clear_*: true` boolean → clear (Foundry null)\n\nEach `(field, clear_field)` pair is mutually exclusive — sending both → 422.",
        "properties": {
          "clear_entry_id": {
            "type": "boolean"
          },
          "clear_icon_tint": {
            "type": "boolean"
          },
          "clear_page_id": {
            "type": "boolean"
          },
          "clear_text": {
            "type": "boolean"
          },
          "clear_text_color": {
            "type": "boolean"
          },
          "entry_id": {
            "minLength": 1,
            "type": "string"
          },
          "font_size": {
            "minimum": 1,
            "type": "integer"
          },
          "global": {
            "type": "boolean"
          },
          "icon_size": {
            "minimum": 1,
            "type": "integer"
          },
          "icon_src": {
            "minLength": 1,
            "type": "string"
          },
          "icon_tint": {
            "minLength": 1,
            "type": "string"
          },
          "page_id": {
            "minLength": 1,
            "type": "string"
          },
          "text": {
            "type": "string"
          },
          "text_anchor": {
            "$ref": "#/components/schemas/NoteTextAnchor"
          },
          "text_color": {
            "minLength": 1,
            "type": "string"
          },
          "x": {
            "type": "integer"
          },
          "y": {
            "type": "integer"
          }
        },
        "type": "object"
      },
      "UpdateRollTableRequest": {
        "description": "Patch top-level table fields. **Does NOT touch the `results` array** — that is a wire-protocol limitation; to change entries, delete and re-create the table. Empty body is a no-op.",
        "properties": {
          "description": {
            "type": "string"
          },
          "display_roll": {
            "type": "boolean"
          },
          "formula": {
            "minLength": 1,
            "type": "string"
          },
          "img": {
            "minLength": 1,
            "type": "string"
          },
          "name": {
            "minLength": 1,
            "type": "string"
          },
          "replacement": {
            "type": "boolean"
          }
        },
        "type": "object"
      },
      "UpdateSceneRequest": {
        "description": "Patch scene metadata. All fields optional. **Tri-state for `nav_name` and `folder`:**\n\n- `nav_name: \"Outside\"` → set sidebar nav label.\n- `clear_nav_name: true` → clear it (Foundry stores null).\n- both omitted → leave alone.\n- both set → 422.\n\nSame contract for `folder` / `clear_folder`.",
        "properties": {
          "background": {
            "minLength": 1,
            "type": "string"
          },
          "clear_folder": {
            "description": "When true, move scene to root. 422 if combined with `folder`.",
            "type": "boolean"
          },
          "clear_nav_name": {
            "description": "When true, clear nav label. 422 if combined with `nav_name`.",
            "type": "boolean"
          },
          "darkness": {
            "format": "double",
            "maximum": 1,
            "minimum": 0,
            "type": "number"
          },
          "fog_exploration": {
            "type": "boolean"
          },
          "folder": {
            "description": "Folder ID. See tri-state semantics in description.",
            "minLength": 1,
            "type": "string"
          },
          "foreground": {
            "minLength": 1,
            "type": "string"
          },
          "grid": {
            "$ref": "#/components/schemas/SceneGridInput"
          },
          "height": {
            "minimum": 1,
            "type": "integer"
          },
          "name": {
            "minLength": 1,
            "type": "string"
          },
          "nav_name": {
            "description": "Set sidebar nav label. See tri-state semantics in description.",
            "minLength": 1,
            "type": "string"
          },
          "nav_order": {
            "type": "integer"
          },
          "navigation": {
            "type": "boolean"
          },
          "padding": {
            "format": "double",
            "minimum": 0,
            "type": "number"
          },
          "width": {
            "minimum": 1,
            "type": "integer"
          }
        },
        "type": "object"
      },
      "UpdateTokenRequest": {
        "description": "Patch token metadata. All fields optional; empty body is a no-op. Does NOT reposition — use `POST /v1/tokens/{id}/move` for that. `disposition` accepts -2 (secret), -1 (hostile), 0 (neutral), 1 (friendly).",
        "properties": {
          "display_name": {
            "description": "Foundry's display-name visibility int.",
            "type": "integer"
          },
          "disposition": {
            "enum": [
              -2,
              -1,
              0,
              1
            ],
            "type": "integer"
          },
          "elevation": {
            "type": "integer"
          },
          "hidden": {
            "type": "boolean"
          },
          "lock_rotation": {
            "type": "boolean"
          },
          "name": {
            "minLength": 1,
            "type": "string"
          },
          "rotation": {
            "type": "integer"
          },
          "scale": {
            "format": "double",
            "minimum": 0,
            "type": "number"
          },
          "scene_id": {
            "minLength": 1,
            "type": "string"
          }
        },
        "type": "object"
      },
      "UpdateWallRequest": {
        "description": "Patch wall properties. All fields optional; empty body is a no-op.",
        "properties": {
          "c": {
            "$ref": "#/components/schemas/WallCoordinates"
          },
          "dir": {
            "$ref": "#/components/schemas/WallDirection"
          },
          "door": {
            "$ref": "#/components/schemas/WallDoorType"
          },
          "door_state": {
            "$ref": "#/components/schemas/WallDoorState"
          },
          "light": {
            "$ref": "#/components/schemas/WallRestriction"
          },
          "move": {
            "$ref": "#/components/schemas/WallRestriction"
          },
          "sense": {
            "$ref": "#/components/schemas/WallRestriction"
          },
          "sound": {
            "$ref": "#/components/schemas/WallRestriction"
          }
        },
        "type": "object"
      },
      "ValidationErrorResponse": {
        "description": "Request-validation error response. Used only on 400 / 422 when the failure cause is one or more invalid request fields. The `error` is a short summary; `validation` enumerates each failing field with a stable per-field message.",
        "properties": {
          "error": {
            "description": "Short summary of the validation failure (e.g. `request validation failed`).",
            "type": "string"
          },
          "validation": {
            "description": "Per-field validation errors. Order is stable so clients can match against indices.",
            "items": {
              "properties": {
                "field": {
                  "description": "Dot-separated path to the offending field. Examples: `email`, `address.city`, `items[2].name`.",
                  "type": "string"
                },
                "message": {
                  "description": "Human-readable reason. Stable enough for clients to surface to end users without translation.",
                  "type": "string"
                }
              },
              "required": [
                "field",
                "message"
              ],
              "type": "object"
            },
            "minItems": 1,
            "type": "array"
          }
        },
        "required": [
          "error",
          "validation"
        ],
        "type": "object"
      },
      "ViewSceneResponse": {
        "description": "View confirmation envelope. `viewed: true` means Foundry opened the scene in the GM's canvas; players are not affected.",
        "properties": {
          "data": {
            "properties": {
              "scene_id": {
                "type": "string"
              },
              "viewed": {
                "type": "boolean"
              }
            },
            "required": [
              "scene_id",
              "viewed"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "Wall": {
        "description": "Wall segment placed on a scene's canvas. Enum fields use the bridge's string codes — same as the values exposed in `GET /v1/scenes/{id}` (read-only walls collection there). `c` is the wall endpoint tuple.",
        "properties": {
          "c": {
            "$ref": "#/components/schemas/WallCoordinates"
          },
          "dir": {
            "$ref": "#/components/schemas/WallDirection"
          },
          "door": {
            "$ref": "#/components/schemas/WallDoorType"
          },
          "door_state": {
            "$ref": "#/components/schemas/WallDoorState"
          },
          "id": {
            "type": "string"
          },
          "light": {
            "$ref": "#/components/schemas/WallRestriction"
          },
          "move": {
            "$ref": "#/components/schemas/WallRestriction"
          },
          "sense": {
            "$ref": "#/components/schemas/WallRestriction"
          },
          "sound": {
            "$ref": "#/components/schemas/WallRestriction"
          }
        },
        "required": [
          "id",
          "c",
          "door",
          "door_state",
          "move",
          "sense",
          "sound",
          "light",
          "dir"
        ],
        "type": "object"
      },
      "WallCoordinates": {
        "description": "Wall endpoint coordinates `[x1, y1, x2, y2]` in pixels.",
        "items": {
          "type": "integer"
        },
        "maxItems": 4,
        "minItems": 4,
        "type": "array"
      },
      "WallDirection": {
        "description": "Direction in which the wall blocks: `both` (default), `left` (only from left side), `right` (only from right side).",
        "enum": [
          "both",
          "left",
          "right"
        ],
        "type": "string"
      },
      "WallDoorState": {
        "description": "Foundry door state. Only meaningful when `door` is `door` or `secret`.",
        "enum": [
          "closed",
          "open",
          "locked"
        ],
        "type": "string"
      },
      "WallDoorType": {
        "description": "Foundry wall door type: `none` (regular wall, not a door), `door` (regular door — can be opened/closed/locked), `secret` (hidden door, still functional).",
        "enum": [
          "none",
          "door",
          "secret"
        ],
        "type": "string"
      },
      "WallListResponse": {
        "description": "Scene-scoped wall list envelope. `data.walls` is always non-nil (empty array when no walls).",
        "properties": {
          "data": {
            "properties": {
              "scene_id": {
                "type": "string"
              },
              "walls": {
                "items": {
                  "$ref": "#/components/schemas/Wall"
                },
                "type": "array"
              }
            },
            "required": [
              "scene_id",
              "walls"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "WallResponse": {
        "description": "Single-wall envelope. Used by POST / PATCH.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/Wall"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "WallRestriction": {
        "description": "Foundry wall blocking restriction: `none` (no effect), `normal` (fully blocks), `limited` (single-token threshold).",
        "enum": [
          "none",
          "normal",
          "limited"
        ],
        "type": "string"
      },
      "WorldInfoResponse": {
        "description": "Metadata snapshot of the tenant's currently-loaded Foundry world. Wraps the world fields under `data` to match the envelope convention every public endpoint follows.",
        "properties": {
          "data": {
            "properties": {
              "foundry_version": {
                "description": "Version string of the Foundry VTT engine.",
                "type": "string"
              },
              "id": {
                "description": "Foundry world id. Stable across the lifetime of the world; changes only when the user switches to a different world.",
                "type": "string"
              },
              "system": {
                "description": "Game system identifier. Examples: `dnd5e`, `pf2e`, `swade`.",
                "type": "string"
              },
              "system_version": {
                "description": "Version string of the installed game system module.",
                "type": "string"
              },
              "title": {
                "description": "Human-readable title shown in the Foundry UI.",
                "type": "string"
              }
            },
            "required": [
              "id",
              "title",
              "system",
              "system_version",
              "foundry_version"
            ],
            "type": "object"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "WorldItem": {
        "description": "World-level item (game.items / Items Directory) — distinct from actor-embedded items. **Folder asymmetry**: on this read-side shape `folder` is the folder NAME; on write-side requests (create-item / update-item) `folder` is the folder ID. Wire convention faithfully relayed.",
        "properties": {
          "folder": {
            "description": "Folder NAME (not id) when in a folder; null at root.",
            "nullable": true,
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "image": {
            "description": "Image path or URL; empty string if not set.",
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "type": {
            "description": "D\u0026D 5e: weapon/equipment/consumable/tool/container/loot/spell/feat/background/race/class/subclass/feature.",
            "type": "string"
          },
          "uuid": {
            "description": "Full Foundry UUID, e.g. `Item.abc123`.",
            "type": "string"
          }
        },
        "required": [
          "id",
          "uuid",
          "name",
          "type",
          "image"
        ],
        "type": "object"
      },
      "WorldItemFull": {
        "description": "World item with full system data blob — what `GET /v1/items/{id}` returns. Same scalar fields as WorldItem plus the system-specific data.",
        "properties": {
          "folder": {
            "description": "Folder NAME (not id); null at root.",
            "nullable": true,
            "type": "string"
          },
          "id": {
            "type": "string"
          },
          "image": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "system": {
            "additionalProperties": true,
            "description": "Game-system data blob — D\u0026D 5e: rarity, weight, price, identified, attunement, school (for spells), etc. Schema depends on the world's game system.",
            "type": "object"
          },
          "type": {
            "type": "string"
          },
          "uuid": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "uuid",
          "name",
          "type",
          "image",
          "system"
        ],
        "type": "object"
      },
      "WorldItemFullResponse": {
        "description": "Single-item response wrapper for `GET /v1/items/{id}` — full ItemData with system blob.",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/WorldItemFull"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      },
      "WorldItemRef": {
        "description": "Minimal item reference returned by `GET /v1/items` filter — id + name only. Follow up with `GET /v1/items/{id}` for full details.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name"
        ],
        "type": "object"
      },
      "WorldItemResponse": {
        "description": "Single-item response wrapper — used by `POST /v1/items` (lean WorldItem after create) and `PATCH /v1/items/{id}` (lean WorldItem after update).",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/WorldItem"
          }
        },
        "required": [
          "data"
        ],
        "type": "object"
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "description": "API key issued via foundry-mcp Patreon registration. Include as `Authorization: Bearer \u003ckey\u003e` on every authenticated endpoint.",
        "scheme": "bearer",
        "type": "http"
      }
    }
  },
  "info": {
    "description": "Public API for Foundry VTT instances connected via the bridge module.",
    "title": "Foundry API",
    "version": "1.0.0"
  },
  "openapi": "3.0.3",
  "paths": {
    "/actors": {
      "get": {
        "description": "Returns a paginated list of actor refs (id + name) matching **system-agnostic** filters: name, type, disposition, player-ownership, and folder. Backed by the bridge module's `filter-actors` command — requires the user's Foundry instance to be online and connected.\n\n**D\u0026D 5e stat-block filters** (creature type, size, challenge rating, level, armor class, ability scores, HP) live on `GET /v1/dnd5e/actors`.\n\n**Filter semantics:** array fields (type, disposition) are OR within (matches if actor matches any value); fields AND with each other. Sort is by name ASC, id as tiebreaker.\n\n**Pagination:** `limit` defaults to 50, capped at 200. `offset` is zero-based. Response includes `total` and `has_more`.\n\n**Result shape:** each entry is `{id, name}` only. Follow up with `GET /v1/actors/{id}` for full game-system data + inventory.",
        "operationId": "listActors",
        "parameters": [
          {
            "$ref": "#/components/parameters/ActorSearchName"
          },
          {
            "$ref": "#/components/parameters/ActorSearchType"
          },
          {
            "$ref": "#/components/parameters/ActorSearchDisposition"
          },
          {
            "$ref": "#/components/parameters/ActorSearchHasPlayerOwner"
          },
          {
            "$ref": "#/components/parameters/ActorSearchFolderID"
          },
          {
            "$ref": "#/components/parameters/ActorSearchFolderName"
          },
          {
            "$ref": "#/components/parameters/ActorSearchFolderRecursive"
          },
          {
            "$ref": "#/components/parameters/PaginationLimit"
          },
          {
            "$ref": "#/components/parameters/PaginationOffset"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActorSearchResponse"
                }
              }
            },
            "description": "Paginated list of actor refs"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "List / search actors",
        "tags": [
          "tier-free"
        ]
      },
      "post": {
        "description": "Creates an actor in the calling tenant's world from scratch. `name` and `type` are required; `folder`, `image`, and `system` are optional.\n\n**Type is game-system-aware** — valid values depend on the world's game system (dnd5e: `character`, `npc`, `vehicle`, `group`; pf2e: `character`, `npc`, `loot`, `familiar`, `hazard`, `vehicle`; etc.). The module rejects unknown types with a 502 error carrying the module's reason.\n\n**System is shallow-merged** with the world-system defaults — pass partial paths to seed specific stats (e.g. `{\"system\": {\"attributes\": {\"hp\": {\"value\": 7, \"max\": 7}}}}`). Omit the field entirely to accept all defaults.\n\nResponse is `201 Created` with a `Location` header pointing at the new actor and the body carrying the freshly re-fetched full actor (system + items, same envelope as `GET /v1/actors/{id}`). If the re-fetch fails after a successful create, the actor IS persisted module-side; the error reflects only the re-fetch. Caller may locate it via `GET /v1/actors?name=\u003cname\u003e`.",
        "operationId": "createActor",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ActorCreateRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActorResponse"
                }
              }
            },
            "description": "Actor created. Body carries the full re-fetched actor (system + items).",
            "headers": {
              "Location": {
                "description": "Path to the newly-created actor — `/v1/actors/{id}`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Create a new actor",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/actors/{id}": {
      "delete": {
        "description": "Permanently removes an actor from the world. Foundry's `Document.delete()` cascades — embedded items and active effects on the actor are removed automatically. Tokens already placed on scenes that referenced this actor are NOT auto-removed and become orphaned references — that's a Foundry-side limitation, not an API one.\n\nThis action cannot be undone. There is no soft-delete or recovery path; double-check the id before calling.\n\nIdempotent in the REST sense: a second DELETE for the same id returns 404 (the resource no longer exists), but the side effect of the first call is the same as if the second had been the only one.",
        "operationId": "deleteActor",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteActorResponse"
                }
              }
            },
            "description": "Actor deleted. Confirmation envelope with the deleted actor id."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Delete an actor",
        "tags": [
          "tier-free"
        ]
      },
      "get": {
        "description": "Returns the full detail view of a single actor — name, type, portrait, game-system data, and inventory. Backed by a `get-actor` command sent over the bridge module's WebSocket connection — requires the user's Foundry instance to be online and connected.",
        "operationId": "getActor",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActorResponse"
                }
              }
            },
            "description": "Actor detail with full game-system data and inventory"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Get actor by id",
        "tags": [
          "tier-free"
        ]
      },
      "parameters": [
        {
          "description": "Foundry actor id, unique within the tenant's world. Returned by `GET /v1/actors` (when that endpoint lands) or read from a Foundry export.",
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "minLength": 1,
            "type": "string"
          }
        }
      ],
      "patch": {
        "description": "Applies partial updates to the actor — name, image, folder, or any portion of the game-system data. The `system` field is shallow-merged into existing actor data: passing `{\"system\": {\"attributes\": {\"hp\": {\"value\": 25}}}}` updates HP without disturbing AC or other fields.\n\nActor `type` is immutable — there's no field to change it.\n\nReturns the freshly re-fetched full actor (with system + items) after the update is applied. If the re-fetch fails after a successful update, the change IS still applied module-side; the error reflects only the re-fetch failure. Caller may retry `GET /v1/actors/{id}` to see the updated state.",
        "operationId": "updateActor",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ActorUpdateRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActorResponse"
                }
              }
            },
            "description": "Actor after the update — full re-fetched state"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Partially update an actor",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/alive": {
      "get": {
        "description": "Readiness probe: 204 when the API and its database answer, 503 when Postgres does not answer within 2 seconds.",
        "responses": {
          "204": {
            "content": {},
            "description": "API and database are alive"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          }
        },
        "summary": "Health check",
        "tags": [
          "public"
        ]
      }
    },
    "/chat/export": {
      "get": {
        "description": "Returns a serialised dump of the entire chat log. `format=text` (default) gives a human-readable transcript; `format=json` gives a structured JSON string for programmatic processing.\n\nResponse can be large for long-running worlds — payload size scales with chat history.",
        "operationId": "exportChat",
        "parameters": [
          {
            "description": "Export format. `text` is the human transcript; `json` is a structured dump.",
            "in": "query",
            "name": "format",
            "schema": {
              "default": "text",
              "enum": [
                "text",
                "json"
              ],
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatExportResponse"
                }
              }
            },
            "description": "Export payload."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Export the chat log",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/chat/messages": {
      "delete": {
        "description": "**Permanent and irreversible.** Wipes the entire chat log for the calling tenant's world. Use with extreme care — there is no undo at the Foundry layer. Returns the count of messages deleted.\n\nFor surgical removal of a single message, use `DELETE /v1/chat/messages/{id}` instead.",
        "operationId": "clearChat",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClearChatResponse"
                }
              }
            },
            "description": "Chat log wiped. `deleted_count` reflects the number of messages removed."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Clear ALL chat messages (destructive)",
        "tags": [
          "tier-free"
        ]
      },
      "get": {
        "description": "Paginated chat-log listing with optional filters. **Cursor pagination via `since` and `before`** — pass a message id to get messages strictly after / before that anchor. `limit` defaults to module's 20, max 100. `include_rolls` populates the per-message `rolls[]` summary; default false to keep responses lean.",
        "operationId": "listChatMessages",
        "parameters": [
          {
            "description": "Max messages to return. Default 20, max 100.",
            "in": "query",
            "name": "limit",
            "schema": {
              "maximum": 100,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "Cursor: return messages AFTER this id.",
            "in": "query",
            "name": "since",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Cursor: return messages BEFORE this id.",
            "in": "query",
            "name": "before",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "When true, populate per-message `rolls[]` with formula+total summaries.",
            "in": "query",
            "name": "include_rolls",
            "schema": {
              "default": false,
              "type": "boolean"
            }
          },
          {
            "description": "Filter by author user id.",
            "in": "query",
            "name": "author_id",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Filter by speaker actor id.",
            "in": "query",
            "name": "actor_id",
            "schema": {
              "type": "string"
            }
          },
          {
            "description": "Filter by message style.",
            "in": "query",
            "name": "type",
            "schema": {
              "enum": [
                "ic",
                "ooc",
                "emote",
                "roll"
              ],
              "type": "string"
            }
          },
          {
            "description": "Full-text search in message content.",
            "in": "query",
            "name": "search",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatMessageListResponse"
                }
              }
            },
            "description": "Paginated message list."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "List chat messages",
        "tags": [
          "tier-free"
        ]
      },
      "post": {
        "description": "Broadcasts a new chat message to the world. `content` is required; `actor_id` binds the message to a specific actor (404 if missing); `whisper_to` makes it private to the listed user IDs. Returns the new message id.",
        "operationId": "sendChatMessage",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendChatMessageRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SendChatMessageResponse"
                }
              }
            },
            "description": "Message persisted. Body carries the new message id."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Send a chat message",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/chat/messages/{id}": {
      "delete": {
        "description": "Removes a single message from the chat log. Permanent. For wiping the entire log use `DELETE /v1/chat/messages` (no id).",
        "operationId": "deleteChatMessage",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteChatMessageResponse"
                }
              }
            },
            "description": "Message deleted. Confirmation envelope with the deleted message id."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Delete a chat message",
        "tags": [
          "tier-free"
        ]
      },
      "parameters": [
        {
          "description": "Chat message id, unique within the world's chat log.",
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "minLength": 1,
            "type": "string"
          }
        }
      ],
      "patch": {
        "description": "Update content and/or flavor of an existing message. Speaker, type, and whisper recipients are immutable post-create — re-create the message if you need to change them.",
        "operationId": "updateChatMessage",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateChatMessageRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SendChatMessageResponse"
                }
              }
            },
            "description": "Message updated. Body confirms the message id."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Edit a chat message",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/combat": {
      "delete": {
        "description": "Removes the combat completely. Foundry cascades to combatants. **Irreversible.** Different from `POST /combat/end` which preserves the combat document.",
        "operationId": "deleteCombat",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CombatIDRequest"
              }
            }
          },
          "required": false
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteCombatResponse"
                }
              }
            },
            "description": "Deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Delete a combat (irreversible)",
        "tags": [
          "tier-dm"
        ]
      },
      "get": {
        "description": "Reads the current combat snapshot. `combat_id` query param targets a specific combat; omit for the world's active combat.\n\n**404** when there is no active combat (or `combat_id` doesn't exist).",
        "operationId": "getCombat",
        "parameters": [
          {
            "description": "Specific combat id. Omit for active combat.",
            "in": "query",
            "name": "combat_id",
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CombatStateResponse"
                }
              }
            },
            "description": "Combat state."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Get current combat state",
        "tags": [
          "tier-dm"
        ]
      },
      "post": {
        "description": "Creates an encounter on the given scene (or active scene if omitted). Activate=true (bridge default) makes it the world's active combat.",
        "operationId": "createCombat",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateCombatRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CombatStateResponse"
                }
              }
            },
            "description": "Combat created.",
            "headers": {
              "Location": {
                "description": "Path to the new combat — `/v1/combat?combat_id={id}`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Create a new combat encounter",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/combat/combatants": {
      "post": {
        "description": "Adds a combatant from an actor reference. Use `token_id` when the actor has multiple tokens on the scene. Pre-set `initiative` is rare — usually call `POST /v1/dnd5e/dice/roll-initiative` after.",
        "operationId": "addCombatant",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AddCombatantRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CombatantResponse"
                }
              }
            },
            "description": "Combatant added.",
            "headers": {
              "Location": {
                "description": "Path to the new combatant — `/v1/combat/combatants/{id}`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Add an actor to combat",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/combat/combatants/{id}": {
      "delete": {
        "description": "Pulls the combatant out — creature fled, removed mid-fight, etc. Different from `set-defeated` (which keeps them in the order, just skipped).",
        "operationId": "removeCombatant",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteCombatResponse"
                }
              }
            },
            "description": "Removed."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Remove a combatant from the encounter",
        "tags": [
          "tier-dm"
        ]
      },
      "patch": {
        "description": "Generic combatant patch — `updates` is a passthrough map sent directly to Foundry's `Combatant.update()`. Common keys: `initiative` (number), `defeated` (bool), `hidden` (bool), plus any game-system flags. Foundry validates.",
        "operationId": "updateCombatant",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateCombatantRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CombatantResponse"
                }
              }
            },
            "description": "Updated combatant."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Patch a combatant",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/combat/combatants/{id}/defeated": {
      "post": {
        "description": "Sets the defeated flag (skull icon, turn auto-skipped) or undoes it. Combatant remains in the initiative order — use `DELETE /combat/combatants/{id}` to remove entirely.",
        "operationId": "setCombatantDefeated",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetCombatantDefeatedRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CombatantResponse"
                }
              }
            },
            "description": "Defeat state updated."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Mark / unmark a combatant defeated",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/combat/combatants/{id}/toggle-visibility": {
      "post": {
        "description": "Flips the hidden flag — TOGGLE, not set. Useful for surprise / reveal moments. Returns the updated combatant.",
        "operationId": "toggleCombatantVisibility",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CombatantResponse"
                }
              }
            },
            "description": "Visibility toggled."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Toggle combatant visibility (hidden flag)",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/combat/end": {
      "post": {
        "description": "Marks the combat ended — turn-based effects stop. Different from `DELETE /combat`: end preserves the combat document for review/replay; delete wipes it.",
        "operationId": "endCombat",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CombatIDRequest"
              }
            }
          },
          "required": false
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteCombatResponse"
                }
              }
            },
            "description": "Combat ended."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "End a combat (preserve document)",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/combat/next-turn": {
      "post": {
        "description": "Moves to the next combatant in initiative order. Auto-advances round when all have acted. Triggers Foundry's turn-based effects (concentration checks, condition durations, rage timer, etc).",
        "operationId": "nextTurn",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CombatIDRequest"
              }
            }
          },
          "required": false
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CombatStateResponse"
                }
              }
            },
            "description": "Turn advanced."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Advance to the next combatant's turn",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/combat/previous-turn": {
      "post": {
        "description": "Undoes a `next-turn`. Use for accidental advances. Note: this does NOT unwind side-effects already triggered (HP changes, conditions applied).",
        "operationId": "previousTurn",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CombatIDRequest"
              }
            }
          },
          "required": false
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CombatStateResponse"
                }
              }
            },
            "description": "Turn rolled back."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Step back to the previous combatant's turn",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/combat/set-turn": {
      "post": {
        "description": "Sets the current turn to a specific combatant — does NOT cycle through intermediate turns or trigger their round-based effects. Use to skip past defeated NPCs cleanly or fix accidental skips.",
        "operationId": "setTurn",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetTurnRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CombatStateResponse"
                }
              }
            },
            "description": "Turn set."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Jump to a specific combatant's turn",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/combat/start": {
      "post": {
        "description": "Marks combat started — round 1, current combatant set, turn-based effects begin ticking. Returns the updated state.",
        "operationId": "startCombat",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CombatIDRequest"
              }
            }
          },
          "required": false
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CombatStateResponse"
                }
              }
            },
            "description": "Combat started."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Start a combat (begin round 1)",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/combat/turn-context": {
      "get": {
        "description": "Rich tactical view: current combatant + nearby combatants (with grid coords, HP, AC, conditions, line-of-sight) + optional ASCII tactical map.\n\n**`include_ascii_map`** is OPT-IN. Default false drops the bridge-rendered map (~3-5KB) from the response. When true, `data.ascii_map` carries the rendered scene with walls/doors/legend.\n\nThe bridge always computes the map (sunk cost ~1250 raycasts on the Foundry side) — the flag only controls wire bandwidth.",
        "operationId": "getCombatTurnContext",
        "parameters": [
          {
            "description": "Specific combat id. Omit for active.",
            "in": "query",
            "name": "combat_id",
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Include the bridge-rendered ASCII tactical map. Default false.",
            "in": "query",
            "name": "include_ascii_map",
            "schema": {
              "default": false,
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CombatTurnContextResponse"
                }
              }
            },
            "description": "Tactical turn context."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Tactical context for the current combatant's turn",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/compendiums": {
      "get": {
        "description": "Returns metadata for every compendium pack registered in the calling tenant's Foundry world — system, official module, world-level, third-party. Lightweight: no document inflation, only pack-level catalog (id, label, type, document count).\n\nUse `GET /v1/compendiums/{packId}/index` to browse a specific pack's contents lightly, or `GET /v1/compendiums/{packId}/documents/{docId}` for one full document.\n\n**Pack id** equals Foundry's `pack.collection` — pass it verbatim to downstream endpoints. Example: `dnd5e.monsters`.\n\n**Pack types**: `Actor`, `Item`, `JournalEntry`, `Scene`, `RollTable`, `Macro`, `Cards`, `Playlist`, `Adventure`. Adventure packs are listed but cannot be imported via this API (Foundry routes those through its own UI).",
        "operationId": "listCompendiums",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompendiumPackListResponse"
                }
              }
            },
            "description": "List of compendium packs."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "List all compendium packs",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/compendiums/{packId}/documents/{docId}": {
      "get": {
        "description": "Returns one full document by id without inflating the rest of the pack. The response carries the document's identity plus the raw `system` blob — for D\u0026D 5e, this is the same shape Foundry stores internally (`{abilities, attributes, details, ...}`).\n\nFor system-agnostic callers, traverse `data` directly. For D\u0026D 5e clients wanting typed access (Spell, Class, Monster, ...), use `/v1/dnd5e/compendiums/...` (when available) instead.\n\n**`type` vs `document_type`**:\n- `type` is the system sub-type (`npc`, `weapon`, `spell`, ...).\n- `document_type` is the Foundry pack type (`Actor`, `Item`, ...).\n\n**Errors**: 404 when packId or docId doesn't resolve.",
        "operationId": "getCompendiumDocument",
        "parameters": [
          {
            "description": "Compendium pack id.",
            "in": "path",
            "name": "packId",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Document id within the pack (Foundry's `_id` field on the source document).",
            "in": "path",
            "name": "docId",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompendiumDocumentResponse"
                }
              }
            },
            "description": "Full compendium document."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Get a single full document from a compendium",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/compendiums/{packId}/documents/{docId}/import": {
      "post": {
        "description": "Generic import of any compendium document into its natural world collection — Actor → `game.actors`, Item → `game.items`, JournalEntry → `game.journal`, Scene → `game.scenes`, RollTable → `game.tables`, Macro → `game.macros`, Cards → `game.cards`, Playlist → `game.playlists`. The target collection is determined by `pack.metadata.type` on the module side, NOT by the caller — there's no `target` parameter.\n\n**Adventure packs are rejected** with 501 (Foundry routes those through its own UI flow, not the WS bridge).\n\n**Cannot place items into actor inventories.** Use a future actor-bound import endpoint for that — generic only ever lands documents at world level (top of the natural collection), never embedded inside an actor.\n\n**Folder semantics:** `folder` is passed through to Foundry verbatim. Foundry-side validation enforces `folder.type === document.type` (Actor folders hold only Actor documents, etc.); we do NOT pre-validate. Mismatched-type folders may be silently ignored or rejected depending on Foundry version — for predictable results, always pass a folder of the matching type (lookup via `GET /v1/folders?type=\u003cPackType\u003e`).\n\n**Name override:** `name` overrides the imported document's display name in the world. Useful for distinguishing copies (\"Sneaky Goblin\" vs \"Goblin\").",
        "operationId": "importCompendiumDocument",
        "parameters": [
          {
            "description": "Compendium pack id.",
            "in": "path",
            "name": "packId",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Source document id within the pack.",
            "in": "path",
            "name": "docId",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompendiumImportRequest"
              }
            }
          },
          "required": false
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompendiumImportedDocumentResponse"
                }
              }
            },
            "description": "Document imported. Body carries the new world-side id, uuid, name, and the resolved document type.",
            "headers": {
              "Location": {
                "description": "Path to the new document — depends on `document_type`. Actor → `/v1/actors/{world_id}`; Item → `/v1/items/{world_id}`; JournalEntry → `/v1/journals/{world_id}`. Other types are not yet exposed via REST API and have no Location pointer.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "501": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            },
            "description": "The pack's `documentType` is `Adventure`; bridge module deliberately rejects Adventure imports — use Foundry UI for adventures."
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Import a compendium document into the world",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/compendiums/{packId}/index": {
      "get": {
        "description": "Returns the per-document index for a single pack — `id`, `name`, `image`, `type` for every document in the pack, with optional dot-path field copying via the `fields` query parameter.\n\nDistinct from `GET /v1/compendiums/{packId}/documents/{docId}` (full single document, system data inlined) and `POST /v1/compendiums/{packId}/search` (filtered + paginated). The index is the cheapest way to enumerate a pack — no system data, no inflation, just identity + selectable extracted fields.\n\n**Fields**: pass dot-paths into the document's system data (e.g. `system.level`, `system.school`). The module copies the resolved values into each entry's `fields` map. Useful for browsing a spell pack and showing level/school inline without a per-doc fetch.\n\n**Empty fields**: omit the parameter entirely → entries carry only the always-present id/name/img/type.\n\n**Errors**: `404 ErrCompendiumPackNotFound` when packId doesn't resolve.",
        "operationId": "getCompendiumIndex",
        "parameters": [
          {
            "description": "Compendium pack id (= Foundry `pack.collection`). Example: `dnd5e.monsters`.",
            "in": "path",
            "name": "packId",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Dot-paths to copy from each document's system data into the entry's `fields` map. Repeat: `?fields=system.level\u0026fields=system.school`. Omit for bare entries.",
            "explode": true,
            "in": "query",
            "name": "fields",
            "schema": {
              "items": {
                "minLength": 1,
                "type": "string"
              },
              "type": "array"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompendiumIndexResponse"
                }
              }
            },
            "description": "Pack index."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Get a compendium pack's lightweight index",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/compendiums/{packId}/search": {
      "post": {
        "description": "Filtered, paginated search inside a single compendium pack. Combines a free-text query against `name` with a structured `filters` array of `(field, operator, value)` clauses (AND between clauses).\n\n**POST not GET** because the filter clauses don't fit cleanly into query strings — `filters[].value` is an arbitrary JSON value (number, string, array for `BETWEEN`).\n\n**Result entries** carry only id/name/img/type plus any dot-path fields requested via `fields` — same shape as `GET /v1/compendiums/{packId}/index`. For full documents, follow up with `GET /v1/compendiums/{packId}/documents/{docId}`.\n\n**Operators** (`filters[].operator`):\n- `EQUALS` (default), `CONTAINS`, `STARTS_WITH`, `ENDS_WITH`\n- `LESS_THAN`, `LESS_THAN_EQUAL`, `GREATER_THAN`, `GREATER_THAN_EQUAL`\n- `BETWEEN` — `value` must be `[min, max]`\n- `IS_EMPTY` — `value` ignored\n\n**Negate**: invert clause result. `EQUALS + negate=true` ≡ NOT EQUALS.\n\n**Errors**: 404 when packId doesn't resolve.",
        "operationId": "searchCompendium",
        "parameters": [
          {
            "description": "Compendium pack id.",
            "in": "path",
            "name": "packId",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CompendiumSearchRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CompendiumSearchResponse"
                }
              }
            },
            "description": "Search results."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Server-side search inside a compendium pack",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/dice/roll": {
      "post": {
        "description": "Executes a Foundry Roll DSL formula and returns the result. Supports any expression Foundry's `Roll` parser accepts: `1d20+5`, `2d6+1d4`, `4d6kh3` (drop-low), `2d20kh1` (advantage), constant arithmetic like `10+5`.\n\n**Side effect:** when `show_in_chat` is true the result posts to the world's Foundry chat for all players to see. Default is `false` — silent roll, result returned only to the caller. `flavor` is the chat-message header text (e.g. \"Stealth check\") and is meaningful only when `show_in_chat` is true.\n\n**Errors:** invalid formula syntax surfaces as 502 with the module's reason. There is no per-formula validation on this side — the DSL is owned by Foundry's Roll class.",
        "operationId": "rollDice",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DiceRollRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DiceRollResponse"
                }
              }
            },
            "description": "Roll result. `total` is the final number including modifiers; `dice` lists each homogeneous group of dice the formula contained."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Roll a dice formula",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/dnd5e/actors": {
      "get": {
        "description": "Paginated actor search with the full D\u0026D 5e filter surface: everything `GET /v1/actors` accepts (name, type, disposition, player-ownership, folder) **plus** stat-block filters read from the actor's `system` data — creature type, size, challenge rating, character level, armor class, ability scores (str/dex/con/int/wis/cha), and HP ranges (max / current).\n\nBacked by the bridge module's `filter-actors` command — requires the user's Foundry instance to be online and connected.\n\n**Filter semantics:** array fields (type, creature_type, size, disposition) are OR within (matches if actor matches any value); fields AND with each other. Actors lacking a queried field (e.g. PCs with no challenge rating) are silently excluded from filters that target it. Sort is by name ASC, id as tiebreaker.\n\n**Pagination:** `limit` defaults to 50, capped at 200. `offset` is zero-based.\n\n**Result shape:** each entry is `{id, name}` only. Follow up with `GET /v1/actors/{id}` for full game-system data + inventory.",
        "operationId": "searchDnd5eActors",
        "parameters": [
          {
            "$ref": "#/components/parameters/ActorSearchName"
          },
          {
            "$ref": "#/components/parameters/ActorSearchType"
          },
          {
            "$ref": "#/components/parameters/ActorSearchCreatureType"
          },
          {
            "$ref": "#/components/parameters/ActorSearchSize"
          },
          {
            "$ref": "#/components/parameters/ActorSearchDisposition"
          },
          {
            "$ref": "#/components/parameters/ActorSearchHasPlayerOwner"
          },
          {
            "$ref": "#/components/parameters/ActorSearchCRMin"
          },
          {
            "$ref": "#/components/parameters/ActorSearchCRMax"
          },
          {
            "$ref": "#/components/parameters/ActorSearchLevelMin"
          },
          {
            "$ref": "#/components/parameters/ActorSearchLevelMax"
          },
          {
            "$ref": "#/components/parameters/ActorSearchMaxHPMin"
          },
          {
            "$ref": "#/components/parameters/ActorSearchMaxHPMax"
          },
          {
            "$ref": "#/components/parameters/ActorSearchCurrentHPMin"
          },
          {
            "$ref": "#/components/parameters/ActorSearchCurrentHPMax"
          },
          {
            "$ref": "#/components/parameters/ActorSearchACMin"
          },
          {
            "$ref": "#/components/parameters/ActorSearchACMax"
          },
          {
            "$ref": "#/components/parameters/ActorSearchStrMin"
          },
          {
            "$ref": "#/components/parameters/ActorSearchStrMax"
          },
          {
            "$ref": "#/components/parameters/ActorSearchDexMin"
          },
          {
            "$ref": "#/components/parameters/ActorSearchDexMax"
          },
          {
            "$ref": "#/components/parameters/ActorSearchConMin"
          },
          {
            "$ref": "#/components/parameters/ActorSearchConMax"
          },
          {
            "$ref": "#/components/parameters/ActorSearchIntMin"
          },
          {
            "$ref": "#/components/parameters/ActorSearchIntMax"
          },
          {
            "$ref": "#/components/parameters/ActorSearchWisMin"
          },
          {
            "$ref": "#/components/parameters/ActorSearchWisMax"
          },
          {
            "$ref": "#/components/parameters/ActorSearchChaMin"
          },
          {
            "$ref": "#/components/parameters/ActorSearchChaMax"
          },
          {
            "$ref": "#/components/parameters/ActorSearchFolderID"
          },
          {
            "$ref": "#/components/parameters/ActorSearchFolderName"
          },
          {
            "$ref": "#/components/parameters/ActorSearchFolderRecursive"
          },
          {
            "$ref": "#/components/parameters/PaginationLimit"
          },
          {
            "$ref": "#/components/parameters/PaginationOffset"
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActorSearchResponse"
                }
              }
            },
            "description": "Paginated list of actor refs"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Search actors with D\u0026D 5e filters",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/dnd5e/compendiums/spells": {
      "get": {
        "description": "Typed search over a single compendium pack's spells. Returns a paginated page of `SpellSummary` entries (lightweight: id, name, image, level, school, ritual / concentration flags). For full spell detail (description, components, casting time, range, duration, target, damage, save), follow up with `GET /v1/dnd5e/compendiums/spells/{packId}/{docId}`.\n\n**Filters** combine with AND across distinct fields; within array filters (`levels`, `schools`) the semantics is OR — a spell matches if its value equals any element. Empty / omitted filter = no constraint.\n\n**`pack_id` is required** — V1 search is scoped to a single pack. Cross-pack search is a future feature.\n\n**Implementation note:** under the hood this fetches the full pack index in one round-trip and applies all filtering server-side in Go. Spell packs cap around 500 entries (~150 KB of metadata), so the latency cost of pulling the whole index is bounded; in exchange, multi-value filters work naturally without N round-trips to Foundry.",
        "operationId": "searchDnd5eSpells",
        "parameters": [
          {
            "description": "Compendium pack id (e.g. `dnd5e.spells`). Required.",
            "in": "query",
            "name": "pack_id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Substring of spell name; case-insensitive (ASCII fold).",
            "in": "query",
            "name": "name",
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Spell-level filter (0 = cantrip). Multiple values OR'd. Repeat: `?levels=1\u0026levels=2`.",
            "explode": true,
            "in": "query",
            "name": "levels",
            "schema": {
              "items": {
                "maximum": 9,
                "minimum": 0,
                "type": "integer"
              },
              "type": "array"
            }
          },
          {
            "description": "Spell school filter. Full names (`evocation`); module's 3-letter codes are normalised on read.",
            "explode": true,
            "in": "query",
            "name": "schools",
            "schema": {
              "items": {
                "enum": [
                  "abjuration",
                  "conjuration",
                  "divination",
                  "enchantment",
                  "evocation",
                  "illusion",
                  "necromancy",
                  "transmutation"
                ],
                "type": "string"
              },
              "type": "array"
            }
          },
          {
            "description": "True to match only rituals; false to match only non-rituals.",
            "in": "query",
            "name": "ritual",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "description": "True to match only concentration spells.",
            "in": "query",
            "name": "concentration",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "description": "Page size. Default 50, max 200.",
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 50,
              "maximum": 200,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "Skip first N results.",
            "in": "query",
            "name": "offset",
            "schema": {
              "default": 0,
              "minimum": 0,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Dnd5eSpellSearchResponse"
                }
              }
            },
            "description": "Paginated list of spell summaries."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Search D\u0026D 5e spells in a compendium pack",
        "tags": [
          "tier-adventurer"
        ]
      }
    },
    "/dnd5e/compendiums/spells/{packId}/{docId}": {
      "get": {
        "description": "Returns a fully-hydrated typed `Spell` aggregate — description (HTML + plain text), components (V/S/M flags + material descriptor), casting time, range, duration, target, damage parts (with type and optional alt-types), save (ability + DC), ritual / concentration flags, and class affinity list.\n\nUnder the hood: single `get-compendium-document` round-trip + ACL projection through `SpellMapper`. Defensive against schema variants (2014 / 2024 dnd5e shapes) — each sub-field handles both formats.\n\nDC on save is rarely populated at the compendium level — it depends on the caster's spellcasting ability and prof bonus, computed at the actor sheet, not on the template.",
        "operationId": "getDnd5eSpell",
        "parameters": [
          {
            "description": "Compendium pack id.",
            "in": "path",
            "name": "packId",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Document id within the pack.",
            "in": "path",
            "name": "docId",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Dnd5eSpellResponse"
                }
              }
            },
            "description": "Full Spell detail."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Get a D\u0026D 5e spell with full typed detail",
        "tags": [
          "tier-adventurer"
        ]
      }
    },
    "/dnd5e/dice/roll-all-initiative": {
      "post": {
        "description": "Rolls initiative for every combatant in the encounter (or NPCs only when `npcs_only=true`). Returns one entry per rolled combatant.\n\n**`combat_id`** is optional — when omitted the module uses the world's active combat.\n\n**`npcs_only=true`** restricts the roll to combatants whose actor is NOT a player character (system-specific; for dnd5e this means actor type != `character`). Useful at the start of an encounter when players want to roll their own initiative.\n\n**404** when combat doesn't exist; **502** when no active combat exists and `combat_id` is omitted.",
        "operationId": "rollAllInitiative",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RollAllInitiativeRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InitiativeRollResponse"
                }
              }
            },
            "description": "Initiative roll results, one entry per rolled combatant."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Roll initiative for every combatant in a combat",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/dnd5e/dice/roll-attack": {
      "post": {
        "description": "Executes an attack roll for a weapon (or attack-spell) the actor owns. Module rolls 1d20 + attack bonus from the item's `attack` activity. Critical-hit detection works correctly here (uses Foundry's attack-roll context), unlike `/dice/roll` which has known limitations.\n\n**`item_id`** must reference an item with an `attack` activity. Get the id from `GET /v1/actors/{id}` → `items[]`.\n\n**404** when the actor or item doesn't exist; **502** when the item exists but isn't a weapon (no attack activity).",
        "operationId": "rollAttack",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RollAttackRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DiceRollResponse"
                }
              }
            },
            "description": "Attack roll result. `is_critical: true` on natural 20 (or item's expanded crit range); `is_fumble: true` on natural 1."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Roll a weapon-bound attack",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/dnd5e/dice/roll-attribute": {
      "post": {
        "description": "Executes an ability check (dnd5e: STR/DEX/CON/INT/WIS/CHA) against the actor's stat block. Module computes the formula from the actor's ability modifier (no proficiency — that's `roll-saving` for proficient saves).\n\n**Wire command** is `roll-ability`; we expose it as `/roll-attribute` in the URL for product naming. Body field is `ability` to match the wire shape.\n\n**Side effect:** when `show_in_chat` is true the result posts to the world's Foundry chat. **404** when the actor doesn't exist; **502** for invalid ability codes.",
        "operationId": "rollAttribute",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RollAttributeRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DiceRollResponse"
                }
              }
            },
            "description": "Roll result."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Roll an actor-bound ability check",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/dnd5e/dice/roll-damage": {
      "post": {
        "description": "Executes a damage roll for a weapon (or attack-spell) the actor owns. Module rolls the item's damage formula — base dice + ability modifier + magic bonuses, **doubled if `critical: true`** (Foundry standard crit damage handling).\n\nTypically called as the second WS roundtrip after `/dnd5e/dice/roll-attack`: roll attack first, check `is_critical` in the response, then call `/dnd5e/dice/roll-damage` with `critical: true` to apply double dice.\n\n**404** when the actor or item doesn't exist; **502** when the item exists but isn't a weapon.",
        "operationId": "rollDamage",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RollDamageRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DiceRollResponse"
                }
              }
            },
            "description": "Damage roll result. `is_critical: true` whenever `critical: true` was in the request (forced — not derived from any natural d20)."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Roll weapon damage",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/dnd5e/dice/roll-initiative": {
      "post": {
        "description": "Rolls initiative for the listed combatants in a combat encounter. Returns one entry per combatant with the rolled value.\n\n**`combatant_ids`** are combatant ids (NOT actor ids — get them from `GET /v1/combats/{id}` → `combatants[]`).\n\n**`combat_id`** is optional — when omitted the module uses the world's active combat. **404** when combat or any combatant doesn't exist; **502** when no active combat exists and `combat_id` is omitted.\n\nFor rolling everyone at once (or NPCs only), use `POST /v1/dnd5e/dice/roll-all-initiative`.",
        "operationId": "rollInitiative",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RollInitiativeRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InitiativeRollResponse"
                }
              }
            },
            "description": "Initiative roll results, one entry per combatant."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Roll initiative for specific combatants",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/dnd5e/dice/roll-saving": {
      "post": {
        "description": "Executes a saving throw using the actor's proficiency + ability modifier (per class/race/feat configuration). For dnd5e: STR/DEX/CON/INT/WIS/CHA save, with proficiency added when the actor is proficient in that save.\n\n**Wire command** is `roll-save`; we expose it as `/roll-saving` for product naming.\n\n**Side effect:** when `show_in_chat` is true the result posts to the world's Foundry chat. **404** when the actor doesn't exist; **502** for invalid ability codes.",
        "operationId": "rollSaving",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RollSavingRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DiceRollResponse"
                }
              }
            },
            "description": "Roll result."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Roll an actor-bound saving throw",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/dnd5e/dice/roll-skill": {
      "post": {
        "description": "Executes a skill check against the actor's stat block. Module computes the formula from the actor's skill modifier + proficiency and rolls 1d20-based.\n\n**Side effect:** when `show_in_chat` is true the result posts to the world's Foundry chat. Default false (silent — caller-only).\n\n**404** when the actor doesn't exist; **502** for invalid skill codes (game-system-aware, no enum validation up-front).",
        "operationId": "rollSkill",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RollSkillRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DiceRollResponse"
                }
              }
            },
            "description": "Roll result."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Roll an actor-bound skill check",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/dnd5e/items": {
      "get": {
        "description": "Paginated item search with the full D\u0026D 5e filter surface: everything `GET /v1/items` accepts (name, type, is_container, folder) **plus** filters read from the item's `system` data — rarity, spell school, attunement, identified state, activities, weight, price, and spell level. AND between different filters, OR within array enums.\n\n**Silent exclude semantics**: items missing a filtered field are silently excluded (e.g. `spell_level` against a weapon → false, no error). `rarity=rare` + `spell_level_min=3` matches only spell-type items that ARE rare AND level≥3.\n\n**Result shape**: each entry is `{id, name}` only — follow up with `GET /v1/items/{id}` for full details.\n\n**Sort**: name ASC, id as tiebreaker.\n\n**Wire conventions**:\n- `weight` in lb (D\u0026D 5e standard)\n- `price` in gp (gold pieces) — module normalises pp/sp/cp/ep on read\n- `spell_level`: integer 0–9 (0 = cantrip)\n- `rarity`: camelCase `veryRare` (NOT `very rare`)\n- `spell_school`: full English words (`evocation`, NOT 3-letter `evo`)",
        "operationId": "searchDnd5eItems",
        "parameters": [
          {
            "description": "Substring of item name; case-insensitive, trimmed.",
            "in": "query",
            "name": "name",
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Item type filter. Multiple values OR'd. Repeat the parameter: `?type=weapon\u0026type=consumable`.",
            "explode": true,
            "in": "query",
            "name": "type",
            "schema": {
              "items": {
                "type": "string"
              },
              "type": "array"
            }
          },
          {
            "description": "Rarity filter. Strict enum.",
            "explode": true,
            "in": "query",
            "name": "rarity",
            "schema": {
              "items": {
                "enum": [
                  "common",
                  "uncommon",
                  "rare",
                  "veryRare",
                  "legendary",
                  "artifact"
                ],
                "type": "string"
              },
              "type": "array"
            }
          },
          {
            "description": "Spell school filter (spell items only).",
            "explode": true,
            "in": "query",
            "name": "spell_school",
            "schema": {
              "items": {
                "enum": [
                  "abjuration",
                  "conjuration",
                  "divination",
                  "enchantment",
                  "evocation",
                  "illusion",
                  "necromancy",
                  "transmutation"
                ],
                "type": "string"
              },
              "type": "array"
            }
          },
          {
            "description": "True if item requires attunement. Items without the field silently excluded.",
            "in": "query",
            "name": "requires_attunement",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "description": "True if item is identified.",
            "in": "query",
            "name": "identified",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "description": "True if item has at least one activity (D\u0026D 5e v3+).",
            "in": "query",
            "name": "has_activities",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "description": "True if item.type === 'container'.",
            "in": "query",
            "name": "is_container",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "description": "Min weight (lb), inclusive.",
            "in": "query",
            "name": "weight_min",
            "schema": {
              "format": "double",
              "minimum": 0,
              "type": "number"
            }
          },
          {
            "description": "Max weight (lb), inclusive.",
            "in": "query",
            "name": "weight_max",
            "schema": {
              "format": "double",
              "minimum": 0,
              "type": "number"
            }
          },
          {
            "description": "Min price in gp, inclusive.",
            "in": "query",
            "name": "price_min",
            "schema": {
              "format": "double",
              "minimum": 0,
              "type": "number"
            }
          },
          {
            "description": "Max price in gp, inclusive.",
            "in": "query",
            "name": "price_max",
            "schema": {
              "format": "double",
              "minimum": 0,
              "type": "number"
            }
          },
          {
            "description": "Min spell level (0 = cantrip), inclusive. Spells only.",
            "in": "query",
            "name": "spell_level_min",
            "schema": {
              "maximum": 9,
              "minimum": 0,
              "type": "integer"
            }
          },
          {
            "description": "Max spell level, inclusive.",
            "in": "query",
            "name": "spell_level_max",
            "schema": {
              "maximum": 9,
              "minimum": 0,
              "type": "integer"
            }
          },
          {
            "description": "Folder ID to scope to.",
            "in": "query",
            "name": "folder_id",
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Folder name (alternative to folder_id).",
            "in": "query",
            "name": "folder_name",
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Descend into sub-folders. Default false.",
            "in": "query",
            "name": "folder_recursive",
            "schema": {
              "default": false,
              "type": "boolean"
            }
          },
          {
            "description": "Page size. Default 50, max 200.",
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 50,
              "maximum": 200,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "Skip first N.",
            "in": "query",
            "name": "offset",
            "schema": {
              "default": 0,
              "minimum": 0,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemSearchResponse"
                }
              }
            },
            "description": "Paginated list of item refs."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Search items with D\u0026D 5e filters",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/folders": {
      "get": {
        "description": "Returns all folder summaries for the calling tenant's world. Optional `type` query param filters to a single document-folder tree (Actor / Item / Scene / etc.).\n\nResult is a flat list of FolderSummary entries (id, name, type, color, description, parent_id, sort) — does NOT include subfolder relationships or contents. Use `GET /v1/folders/{id}?include_subfolders=true` to materialise the tree for a specific folder.",
        "operationId": "listFolders",
        "parameters": [
          {
            "description": "Filter folders to a single document type. Omit for all types interleaved.",
            "in": "query",
            "name": "type",
            "schema": {
              "$ref": "#/components/schemas/FolderDocumentType"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FolderListResponse"
                }
              }
            },
            "description": "Folder summaries."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "List folders (optionally filtered by type)",
        "tags": [
          "tier-free"
        ]
      },
      "post": {
        "description": "Creates a folder for documents of the given type. Foundry keeps separate folder trees per document type — Actor folders only hold Actors, Item folders only Items, etc. (see FolderDocumentType enum).\n\n201 with the lean Folder summary in the body (no subfolders / contents — fetch via `GET /v1/folders/{id}?include_subfolders=true`).",
        "operationId": "createFolder",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateFolderRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FolderResponse"
                }
              }
            },
            "description": "Folder created.",
            "headers": {
              "Location": {
                "description": "Path to the new folder — `/v1/folders/{id}`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Create a folder",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/folders/{id}": {
      "delete": {
        "description": "Deletes a folder. Cascade behaviour for sub-folders and documents inside is controlled via the request body flags (`delete_subfolders` and `delete_contents`). Both default false — sub-folders and documents are orphaned to the parent folder (or root) when this folder is deleted.\n\nBody is allowed on DELETE here per project convention; pass an empty `{}` if you want both flags false (or omit the body entirely if your client allows).",
        "operationId": "deleteFolder",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeleteFolderRequest"
              }
            }
          },
          "required": false
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteFolderResponse"
                }
              }
            },
            "description": "Folder deleted. Body confirms with the deleted folder id."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Delete a folder, optionally cascading subfolders/contents",
        "tags": [
          "tier-free"
        ]
      },
      "get": {
        "description": "Returns the folder summary. With `include_subfolders=true` recursively materialises the subfolder tree. With `include_contents=true` also populates `content_ids` with the ids of documents directly inside the folder (NOT documents in nested subfolders — those are reachable via the recursive subfolder tree).\n\nBoth flags default false → lean response with just the folder summary.",
        "operationId": "getFolder",
        "parameters": [
          {
            "description": "Recursively materialise the subfolder tree.",
            "in": "query",
            "name": "include_subfolders",
            "schema": {
              "default": false,
              "type": "boolean"
            }
          },
          {
            "description": "Populate `content_ids` for this folder (document ids directly inside it). Does NOT cascade — fetch each subfolder separately for its own content_ids.",
            "in": "query",
            "name": "include_contents",
            "schema": {
              "default": false,
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FolderTreeResponse"
                }
              }
            },
            "description": "Folder tree."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Get a folder, optionally with sub-tree and contents",
        "tags": [
          "tier-free"
        ]
      },
      "parameters": [
        {
          "description": "Folder id.",
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "minLength": 1,
            "type": "string"
          }
        }
      ],
      "patch": {
        "description": "Patches name, parent, color, description, or sort. **`parent_id` is tri-state** — see UpdateFolderRequest schema.",
        "operationId": "updateFolder",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateFolderRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FolderResponse"
                }
              }
            },
            "description": "Folder updated."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Update a folder",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/items": {
      "get": {
        "description": "Paginated structured search over `game.items` (Items Directory) with **system-agnostic** filters: name, type, is_container, and folder. AND between different filters, OR within array filters.\n\n**D\u0026D 5e filters** (rarity, spell school, attunement, identified, activities, weight, price, spell level) live on `GET /v1/dnd5e/items`.\n\n**Result shape**: each entry is `{id, name}` only — follow up with `GET /v1/items/{id}` for full details.\n\n**Sort**: name ASC, id as tiebreaker.",
        "operationId": "filterItems",
        "parameters": [
          {
            "description": "Substring of item name; case-insensitive, trimmed.",
            "in": "query",
            "name": "name",
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Item type filter. Multiple values OR'd. Repeat the parameter: `?type=weapon\u0026type=consumable`.",
            "explode": true,
            "in": "query",
            "name": "type",
            "schema": {
              "items": {
                "type": "string"
              },
              "type": "array"
            }
          },
          {
            "description": "True if item.type === 'container'.",
            "in": "query",
            "name": "is_container",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "description": "Folder ID to scope to.",
            "in": "query",
            "name": "folder_id",
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Folder name (alternative to folder_id).",
            "in": "query",
            "name": "folder_name",
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Descend into sub-folders. Default false.",
            "in": "query",
            "name": "folder_recursive",
            "schema": {
              "default": false,
              "type": "boolean"
            }
          },
          {
            "description": "Page size. Default 50, max 200.",
            "in": "query",
            "name": "limit",
            "schema": {
              "default": 50,
              "maximum": 200,
              "minimum": 1,
              "type": "integer"
            }
          },
          {
            "description": "Skip first N.",
            "in": "query",
            "name": "offset",
            "schema": {
              "default": 0,
              "minimum": 0,
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ItemSearchResponse"
                }
              }
            },
            "description": "Paginated list of item refs."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Filter / list world items",
        "tags": [
          "tier-free"
        ]
      },
      "post": {
        "description": "Creates an item in `game.items` (Items Directory). Distinct from actor-embedded items.\n\n**Folder asymmetry**: request `folder` is the folder ID; response `folder` is the folder NAME (Foundry wire convention).\n\n201 with the lean WorldItem (id + scalars + folder name); follow up with `GET /v1/items/{id}` for the full system blob.",
        "operationId": "createItem",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateItemRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorldItemResponse"
                }
              }
            },
            "description": "Item created.",
            "headers": {
              "Location": {
                "description": "Path to the new item — `/v1/items/{id}`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Create a world-level item",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/items/{id}": {
      "delete": {
        "description": "Permanently removes an item from `game.items`. Foundry's `Document.delete()` cascades through any embedded references; use with care.",
        "operationId": "deleteItem",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteItemResponse"
                }
              }
            },
            "description": "Item deleted. Confirmation envelope with the deleted item id."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Delete a world item",
        "tags": [
          "tier-free"
        ]
      },
      "get": {
        "description": "Returns the full ItemData for a single world item — id, uuid, name, type, image, folder (NAME, not id), and the complete `system` blob.",
        "operationId": "getItem",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorldItemFullResponse"
                }
              }
            },
            "description": "Full item data."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Get a world item with full system data",
        "tags": [
          "tier-free"
        ]
      },
      "parameters": [
        {
          "description": "World item id.",
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "minLength": 1,
            "type": "string"
          }
        }
      ],
      "patch": {
        "description": "Patches an existing item. **Folder is tri-state** (see UpdateItemRequest schema description). `system` is deep-merged by Foundry — pass partial paths to surgically modify nested fields.\n\nReturns the lean WorldItem post-update; follow up with `GET /v1/items/{id}` for the full re-merged system blob.",
        "operationId": "updateItem",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateItemRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorldItemResponse"
                }
              }
            },
            "description": "Item updated."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Update a world item",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/journals": {
      "get": {
        "description": "Returns every journal in the calling tenant's world, with all pages embedded. Optional `folder` query param filters to journals whose folder NAME matches exactly. The special name `Uncategorized` matches journals at root (folder=null in Foundry).\n\nResult is always a non-nil array — empty when the filter matches nothing.",
        "operationId": "listJournals",
        "parameters": [
          {
            "description": "Folder NAME to filter by. Use `Uncategorized` for root-level journals. Omit for all journals.",
            "in": "query",
            "name": "folder",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JournalListResponse"
                }
              }
            },
            "description": "Journal entries."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "List journals (optionally filtered by folder)",
        "tags": [
          "tier-adventurer"
        ]
      },
      "post": {
        "description": "Creates a new journal entry. Only `name` is required. `folder` places it under a folder by NAME (not id — Foundry's journal sidebar groups by folder name). When `content` is set, an initial page is created with that body and the optional `page_type`.\n\n201 with the created journal in the body, including any initial page.",
        "operationId": "createJournal",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateJournalRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JournalResponse"
                }
              }
            },
            "description": "Journal created.",
            "headers": {
              "Location": {
                "description": "Path to the new journal — `/v1/journals/{id}`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Create a journal entry",
        "tags": [
          "tier-adventurer"
        ]
      }
    },
    "/journals/{id}": {
      "delete": {
        "description": "Removes a journal entry and all its pages. Foundry cascades; pages aren't deletable as orphans. **Irreversible.**",
        "operationId": "deleteJournal",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteJournalResponse"
                }
              }
            },
            "description": "Deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Delete a journal (cascades to all pages)",
        "tags": [
          "tier-adventurer"
        ]
      },
      "get": {
        "description": "Reads a single journal with all pages embedded. **404** when the journal doesn't exist.",
        "operationId": "getJournal",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JournalResponse"
                }
              }
            },
            "description": "Journal with pages."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Get a journal with all its pages",
        "tags": [
          "tier-adventurer"
        ]
      },
      "patch": {
        "description": "Partially updates a journal entry. **`folder` semantics is tri-state** — see the `UpdateJournalRequest` schema description. Returns the updated journal summary (pages NOT re-fetched — call GET to materialise pages).",
        "operationId": "updateJournal",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateJournalRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JournalResponse"
                }
              }
            },
            "description": "Updated journal."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Update a journal (name and/or folder)",
        "tags": [
          "tier-adventurer"
        ]
      }
    },
    "/journals/{id}/pages": {
      "post": {
        "description": "Creates a new page in an existing journal. Returns 201 with the created page. **404** when the parent journal doesn't exist.",
        "operationId": "createJournalPage",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateJournalPageRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JournalPageResponse"
                }
              }
            },
            "description": "Page created.",
            "headers": {
              "Location": {
                "description": "Path to the new page — `/v1/journals/{id}/pages/{pageId}`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Add a page to a journal",
        "tags": [
          "tier-adventurer"
        ]
      }
    },
    "/journals/{id}/pages/{pageId}": {
      "delete": {
        "description": "Removes one page. The journal stays intact. **404** when the page doesn't exist.",
        "operationId": "deleteJournalPage",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "pageId",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteJournalPageResponse"
                }
              }
            },
            "description": "Deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Delete a single page from a journal",
        "tags": [
          "tier-adventurer"
        ]
      },
      "get": {
        "description": "Reads one page by id. **Server-side filter** — there is no get-page wire command on Foundry's side; we fetch the whole journal and filter by pageId. **404** when the journal OR the page doesn't exist.",
        "operationId": "getJournalPage",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "pageId",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JournalPageResponse"
                }
              }
            },
            "description": "Page contents."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Get a single page out of a journal",
        "tags": [
          "tier-adventurer"
        ]
      },
      "patch": {
        "description": "Partially updates a page. All fields optional — empty body is a no-op (200 with the unchanged page). **404** when the page doesn't exist.",
        "operationId": "updateJournalPage",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "pageId",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateJournalPageRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JournalPageResponse"
                }
              }
            },
            "description": "Updated page."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Update a single page",
        "tags": [
          "tier-adventurer"
        ]
      }
    },
    "/journals/{id}/show": {
      "post": {
        "description": "Side-effecting — pops the journal sheet open in target users' clients. `page_id` opens at a specific page; `force` overrides Foundry's per-user dismissal state. `users` restricts the broadcast to specific user ids; omit / empty array broadcasts to everyone.\n\n`shown:true` confirms Foundry accepted the broadcast — does NOT confirm individual users actually saw it (offline users get nothing).",
        "operationId": "showJournal",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ShowJournalRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ShowJournalResponse"
                }
              }
            },
            "description": "Broadcast accepted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Broadcast a journal (or page) to players",
        "tags": [
          "tier-adventurer"
        ]
      }
    },
    "/macros": {
      "get": {
        "description": "Returns the macro summaries of the calling tenant's world (id, uuid, name, type, img, scope, folder, author_id) — no command text; fetch one macro via `GET /v1/macros/{id}` for its source.\n\nOptional `type` query param filters to `chat` or `script` macros. `folder` on each entry is the folder NAME (null = root) — the read side of the folder asymmetry; writes take a folder ID.",
        "operationId": "listMacros",
        "parameters": [
          {
            "description": "Filter macros to a single type. Omit for both chat and script macros.",
            "in": "query",
            "name": "type",
            "schema": {
              "$ref": "#/components/schemas/MacroType"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MacroListResponse"
                }
              }
            },
            "description": "Macro summaries."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "List macros (optionally filtered by type)",
        "tags": [
          "tier-free"
        ]
      },
      "post": {
        "description": "Creates a macro without running it (use `POST /v1/macros/{id}/execute`). `type: chat` holds a chat command such as `/roll 1d20`; `type: script` holds JavaScript that runs with the GM's rights and is accepted only while the module setting **Allow Script Macros** is on (off by default; the user enables it in Foundry's module settings — the API cannot). With the setting off, a script macro returns **409** with the module's reason.\n\n`folder_id` is a folder ID from `GET /v1/folders?type=Macro` (the response's `folder` is the folder NAME). 201 with the MacroDetail (including `command`) in the body.",
        "operationId": "createMacro",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateMacroRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MacroResponse"
                }
              }
            },
            "description": "Macro created.",
            "headers": {
              "Location": {
                "description": "Path to the new macro — `/v1/macros/{id}`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Create a macro",
        "tags": [
          "tier-adventurer"
        ]
      }
    },
    "/macros/{id}": {
      "delete": {
        "description": "Permanently deletes a macro. No request body. **404** when the macro does not exist.",
        "operationId": "deleteMacro",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteMacroResponse"
                }
              }
            },
            "description": "Macro deleted. Body confirms with the deleted macro id."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Delete a macro",
        "tags": [
          "tier-adventurer"
        ]
      },
      "get": {
        "description": "Returns the macro including `command`: a chat command for `type: chat`, JavaScript source for `type: script`. `folder` is the folder NAME (null = root).\n\n**404** when the macro does not exist (bridge error: `Macro not found: \u003cid\u003e`).",
        "operationId": "getMacro",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MacroResponse"
                }
              }
            },
            "description": "Macro detail."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Get a macro with its command text",
        "tags": [
          "tier-free"
        ]
      },
      "parameters": [
        {
          "description": "Macro id.",
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "minLength": 1,
            "type": "string"
          }
        }
      ],
      "patch": {
        "description": "Patches name, type, command, scope, image or folder; omitted fields stay unchanged. **`folder_id` is tri-state** — see UpdateMacroRequest schema. Script macros (the macro is, or becomes, `type: script`) need the module setting **Allow Script Macros**; otherwise **409** with the module's reason.",
        "operationId": "updateMacro",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateMacroRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MacroResponse"
                }
              }
            },
            "description": "Macro updated."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Update a macro",
        "tags": [
          "tier-adventurer"
        ]
      }
    },
    "/macros/{id}/execute": {
      "post": {
        "description": "Runs a macro the way its hotbar button does: a chat macro posts its command, a script macro runs its JavaScript with the GM's rights (needs the module setting **Allow Script Macros**, off by default — otherwise **409** with the module's reason). Returns only that it ran; the effect is in chat (`GET /v1/chat/messages`) or in the world.\n\nBody is optional: `actor_id` / `token_id` give the macro its `actor` / `token` context (the token must be on the active scene). **404** when the macro, actor or token does not exist (bridge errors: `Macro not found: \u003cid\u003e`, `Actor not found: \u003cid\u003e`, `Token not found on active scene: \u003cid\u003e`).",
        "operationId": "executeMacro",
        "parameters": [
          {
            "description": "Macro id.",
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExecuteMacroRequest"
              }
            }
          },
          "required": false
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExecuteMacroResponse"
                }
              }
            },
            "description": "Macro executed."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Execute a macro",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/pf2e/conditions/decrease": {
      "post": {
        "description": "Lowers a valued PF2E condition by 1, removing it when it reaches zero (the response `condition` is then null).\n\nOnly works in a Pathfinder 2e world; other game systems yield **502**. **404** when the actor doesn't exist; **422** for an unknown condition slug.",
        "operationId": "pf2eDecreaseCondition",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Pf2eConditionSlugRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pf2eConditionMutationResponse"
                }
              }
            },
            "description": "Condition state after the operation."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Decrease a Pathfinder 2e condition",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/pf2e/conditions/get": {
      "post": {
        "description": "Returns the active PF2E conditions on the actor, each with its slug, localized name, value (for valued conditions), and active flag.\n\nOnly works in a Pathfinder 2e world; other game systems yield **502**. **404** when the actor doesn't exist.",
        "operationId": "pf2eGetConditions",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Pf2eGetConditionsRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pf2eConditionListResponse"
                }
              }
            },
            "description": "Active conditions."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "List a Pathfinder 2e actor's conditions",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/pf2e/conditions/increase": {
      "post": {
        "description": "Raises a valued PF2E condition by 1 (creating it at 1 if absent).\n\nOnly works in a Pathfinder 2e world; other game systems yield **502**. **404** when the actor doesn't exist; **422** for an unknown condition slug.",
        "operationId": "pf2eIncreaseCondition",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Pf2eConditionSlugRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pf2eConditionMutationResponse"
                }
              }
            },
            "description": "Condition state after the operation."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Increase a Pathfinder 2e condition",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/pf2e/conditions/remove": {
      "post": {
        "description": "Removes a PF2E condition from the actor entirely.\n\nOnly works in a Pathfinder 2e world; other game systems yield **502**. **404** when the actor doesn't exist; **422** for an unknown condition slug.",
        "operationId": "pf2eRemoveCondition",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Pf2eConditionSlugRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pf2eConditionRemovalResponse"
                }
              }
            },
            "description": "Removal result."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Remove a Pathfinder 2e condition",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/pf2e/conditions/set": {
      "post": {
        "description": "Sets a PF2E condition on the actor to an exact state (creating it if absent). For valued conditions `value` is the exact value to set (e.g. frightened 2); omit for the default (1). `value` is ignored for binary conditions.\n\nOnly works in a Pathfinder 2e world; other game systems yield **502**. **404** when the actor doesn't exist; **422** for an unknown condition slug.",
        "operationId": "pf2eSetCondition",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Pf2eSetConditionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pf2eConditionMutationResponse"
                }
              }
            },
            "description": "Condition state after the operation."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Set a Pathfinder 2e condition",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/pf2e/items/cast-spell": {
      "post": {
        "description": "Casts a spell the actor owns (item of type `spell`). `rank` heightens the spell (1–10); omit to cast at the spell's own rank.\n\n**Side effect:** posts the spell card to the world's Foundry chat unless `show_in_chat` is false. Default **true** for this command.\n\nOnly works in a Pathfinder 2e world; other game systems yield **502**. **404** when the actor or spell doesn't exist.",
        "operationId": "pf2eCastSpell",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Pf2eCastSpellRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pf2eCastSpellResponse"
                }
              }
            },
            "description": "Cast result."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Cast a Pathfinder 2e spell",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/pf2e/items/post": {
      "post": {
        "description": "Posts any item the actor owns (its description card) to the world's Foundry chat.\n\n`show_in_chat` defaults to **true** for this command.\n\nOnly works in a Pathfinder 2e world; other game systems yield **502**. **404** when the actor or item doesn't exist.",
        "operationId": "pf2ePostItem",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Pf2ePostItemRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pf2ePostItemResponse"
                }
              }
            },
            "description": "Post result."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Post a Pathfinder 2e item to chat",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/pf2e/items/use-consumable": {
      "post": {
        "description": "Consumes a charge of a consumable item the actor owns (item of type `consumable`). `quantity` is how many to consume (default 1). Always posts a card to chat.\n\nOnly works in a Pathfinder 2e world; other game systems yield **502**. **404** when the actor or item doesn't exist.",
        "operationId": "pf2eUseConsumable",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Pf2eUseConsumableRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pf2eUseConsumableResponse"
                }
              }
            },
            "description": "Use result."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Use a Pathfinder 2e consumable",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/pf2e/roll-perception": {
      "post": {
        "description": "Executes a PF2E perception check for the actor.\n\n**Side effect:** when `show_in_chat` is true the result posts to the world's Foundry chat. Default false.\n\nOnly works in a Pathfinder 2e world; other game systems yield **502**. **404** when the actor doesn't exist.",
        "operationId": "pf2eRollPerception",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Pf2eRollPerceptionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pf2eRollResponse"
                }
              }
            },
            "description": "Roll result."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Roll a Pathfinder 2e perception check",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/pf2e/roll-save": {
      "post": {
        "description": "Executes a PF2E saving throw (fortitude, reflex, or will) bound to the actor.\n\n**Side effect:** when `show_in_chat` is true the result posts to the world's Foundry chat. Default false.\n\nOnly works in a Pathfinder 2e world; other game systems yield **502**. **404** when the actor doesn't exist; **422** for an unknown save slug.",
        "operationId": "pf2eRollSave",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Pf2eRollSaveRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pf2eRollResponse"
                }
              }
            },
            "description": "Roll result."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Roll a Pathfinder 2e saving throw",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/pf2e/roll-skill": {
      "post": {
        "description": "Executes a PF2E skill check bound to the actor. The module computes the check from the actor's skill proficiency and returns a degree-of-success-aware roll.\n\n**Side effect:** when `show_in_chat` is true the result posts to the world's Foundry chat. Default false (silent — caller-only).\n\nOnly works in a Pathfinder 2e world; other game systems yield **502** with the module's reason. **404** when the actor doesn't exist; **422** for an unknown skill slug.",
        "operationId": "pf2eRollSkill",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Pf2eRollSkillRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pf2eRollResponse"
                }
              }
            },
            "description": "Roll result."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Roll a Pathfinder 2e skill check",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/pf2e/strikes/list": {
      "post": {
        "description": "Returns the Strikes (weapon and unarmed attacks) available to the actor, each with a `slug` to pass to `/pf2e/strikes/roll` and `variants` (MAP-step labels by index).\n\nOnly works in a Pathfinder 2e world; other game systems yield **502**. **404** when the actor doesn't exist.",
        "operationId": "pf2eListStrikes",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Pf2eListStrikesRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pf2eStrikeListResponse"
                }
              }
            },
            "description": "Available Strikes."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "List a Pathfinder 2e actor's Strikes",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/pf2e/strikes/roll": {
      "post": {
        "description": "Executes a PF2E Strike attack roll bound to a weapon/unarmed strike slug (from `/pf2e/strikes/list`).\n\n`map_increase` selects the multiple-attack-penalty step: 0 (none), 1 (−5 / −4 agile), 2 (−10 / −8 agile).\n\n**Side effect:** when `show_in_chat` is true the result posts to the world's Foundry chat. Default false.\n\nOnly works in a Pathfinder 2e world; other game systems yield **502**. **502** for an unknown slug; **404** when the actor doesn't exist.",
        "operationId": "pf2eRollStrike",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Pf2eRollStrikeRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pf2eRollResponse"
                }
              }
            },
            "description": "Roll result."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Roll a Pathfinder 2e Strike attack",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/pf2e/strikes/roll-damage": {
      "post": {
        "description": "Executes a PF2E Strike damage roll bound to a weapon/unarmed strike slug (from `/pf2e/strikes/list`).\n\n`critical` = true rolls critical damage (PF2E doubling rules).\n\n**Side effect:** when `show_in_chat` is true the result posts to the world's Foundry chat. Default false.\n\nOnly works in a Pathfinder 2e world; other game systems yield **502**. **502** for an unknown slug; **404** when the actor doesn't exist.",
        "operationId": "pf2eRollStrikeDamage",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/Pf2eRollStrikeDamageRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Pf2eRollResponse"
                }
              }
            },
            "description": "Roll result."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Roll Pathfinder 2e Strike damage",
        "tags": [
          "tier-free"
        ]
      }
    },
    "/roll-tables": {
      "get": {
        "description": "Returns a lightweight summary for every roll table in the calling tenant's world. `total_results` is the entry count; `drawn_results` is how many have already been drawn (only meaningful when `replacement: false`).\n\nResult is always a non-nil array — empty when the world has no tables.",
        "operationId": "listRollTables",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RollTableListResponse"
                }
              }
            },
            "description": "Roll table summaries."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "List all roll tables in the world",
        "tags": [
          "tier-adventurer"
        ]
      },
      "post": {
        "description": "Creates a new roll table. Only `name` is required. `formula` (default `1d20`), `replacement` (default `true`), and `display_roll` (default `true`) follow Foundry's defaults. `folder` is a folder ID (not a name — mirrors items, not journals).\n\n`results` is an optional initial entry list. Each entry has `text` and a `range: [min, max]` tuple; `weight` defaults to 1, `type` defaults to 0 (text). Use `type: 1` with `document_collection` + `document_id` for in-world document references, or `type: 2` for compendium references.\n\n201 with the created table (entries included).",
        "operationId": "createRollTable",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateRollTableRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RollTableResponse"
                }
              }
            },
            "description": "Roll table created.",
            "headers": {
              "Location": {
                "description": "Path to the new table — `/v1/roll-tables/{id}`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Create a roll table",
        "tags": [
          "tier-adventurer"
        ]
      }
    },
    "/roll-tables/{id}": {
      "delete": {
        "description": "Removes a roll table and all its entries. **Irreversible.**",
        "operationId": "deleteRollTable",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteRollTableResponse"
                }
              }
            },
            "description": "Deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Delete a roll table",
        "tags": [
          "tier-adventurer"
        ]
      },
      "get": {
        "description": "Reads a single roll table with the full `results` array. **404** when the table doesn't exist.",
        "operationId": "getRollTable",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RollTableResponse"
                }
              }
            },
            "description": "Roll table with entries."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Get a roll table with all entries",
        "tags": [
          "tier-adventurer"
        ]
      },
      "patch": {
        "description": "Partially updates a roll table's top-level fields. **Does NOT modify the `results` array** — that is a documented wire-protocol limitation; to change entries, delete and re-create the table. Empty body is a no-op.\n\nReturns the updated table with its current `results`.",
        "operationId": "updateRollTable",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateRollTableRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RollTableResponse"
                }
              }
            },
            "description": "Updated roll table."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Update a roll table (metadata only)",
        "tags": [
          "tier-adventurer"
        ]
      }
    },
    "/roll-tables/{id}/reset": {
      "post": {
        "description": "Clears the `drawn` flag on every entry of a roll table. Only meaningful when `replacement: false` — once every entry has been drawn, the table refuses further rolls until reset.\n\nReturns the table id/name and `reset_count` (number of entries whose flag was cleared).",
        "operationId": "resetRollTable",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ResetRollTableResponse"
                }
              }
            },
            "description": "Reset outcome."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Reset a roll table's drawn state",
        "tags": [
          "tier-adventurer"
        ]
      }
    },
    "/roll-tables/{id}/roll": {
      "post": {
        "description": "Rolls the table's formula and returns the matched entries. **`display_chat` is required** — explicitly choose whether the roll is broadcast to Foundry chat (true) or kept silent (false).\n\nThe response `data.results` is an ARRAY — a single roll can match multiple entries when their ranges overlap or the formula straddles a boundary. For tables with `replacement: false`, drawn entries are flagged on the table and excluded from future draws until reset.",
        "operationId": "rollOnTable",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RollOnTableRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RollOnTableResponse"
                }
              }
            },
            "description": "Roll outcome."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Roll on a table",
        "tags": [
          "tier-adventurer"
        ]
      }
    },
    "/scenes": {
      "get": {
        "description": "Returns a lightweight summary for every scene in the calling tenant's world (`{id, name, active, img}`). Use `GET /v1/scenes/{id}` for the full scene with tokens / walls / lights / tiles / drawings / regions.\n\nResult is always a non-nil array — empty when the world has no scenes.",
        "operationId": "listScenes",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SceneListResponse"
                }
              }
            },
            "description": "Scene summaries."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "List all scenes in the world",
        "tags": [
          "tier-dm"
        ]
      },
      "post": {
        "description": "Creates a new scene. Only `name` is required. All grid fields default to Foundry's values when omitted (`type: square`, `size: 100`, `units: ft`, `distance: 5`). `folder` is a folder ID (NOT a name — mirrors items/roll-tables convention).\n\n201 with the created scene's CRUD summary (use GET for full detail with tokens/walls/etc).",
        "operationId": "createScene",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateSceneRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SceneSummaryResponse"
                }
              }
            },
            "description": "Scene created.",
            "headers": {
              "Location": {
                "description": "Path to the new scene — `/v1/scenes/{id}`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Create a scene",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/scenes/{id}": {
      "delete": {
        "description": "Removes a scene and all its placed content (cascades on Foundry side). **Irreversible.**\n\n**Foundry rejects deletion of the currently active scene** — that surfaces as 502 with the bridge / Foundry error message. Activate another scene first.",
        "operationId": "deleteScene",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteSceneResponse"
                }
              }
            },
            "description": "Deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Delete a scene",
        "tags": [
          "tier-dm"
        ]
      },
      "get": {
        "description": "Reads a single scene with the full placed-content set: notes, walls, lights, tiles, drawings, regions, tokens. The shape differs from the CRUD summary returned by POST/PATCH/clone — this is the live state of the scene canvas.\n\n**404** when the scene doesn't exist.",
        "operationId": "getScene",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SceneResponse"
                }
              }
            },
            "description": "Scene detail."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Get a scene with all placed content",
        "tags": [
          "tier-dm"
        ]
      },
      "patch": {
        "description": "Partially updates scene metadata (name, dimensions, grid, navigation, fog, darkness, folder). Does NOT modify placed content (tokens / walls / lights / etc).\n\n**Tri-state for `nav_name` and `folder`:**\n\n- `nav_name: \"Outside\"` → set sidebar nav label.\n- `clear_nav_name: true` → clear nav label (Foundry stores null).\n- both omitted → leave the field alone.\n- both set → 422 (handler rejects the combination).\n\nSame contract for `folder` / `clear_folder`. Empty body is a no-op.\n\nReturns the updated scene's CRUD summary (call GET for full detail with placed content).",
        "operationId": "updateScene",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateSceneRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SceneSummaryResponse"
                }
              }
            },
            "description": "Updated scene summary."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Update a scene (metadata)",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/scenes/{id}/activate": {
      "post": {
        "description": "Makes the scene the world's active scene — every connected player switches to it in their canvas. Foundry deactivates the previously active scene automatically.\n\nReturns confirmation with the activated scene's id/name and `active: true`. Does NOT include full scene detail — call `GET /v1/scenes/{id}` to materialise it.",
        "operationId": "activateScene",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActivateSceneResponse"
                }
              }
            },
            "description": "Scene activated."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Set a scene as the world's active scene",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/scenes/{id}/clone": {
      "post": {
        "description": "Duplicates an existing scene including its placed content. Optional `name` overrides the default `\u003csource\u003e (Copy)` name; optional `folder` (folder ID) overrides the source's folder.\n\nReturns 201 with the cloned scene's CRUD summary. **404** if the source scene doesn't exist (bridge error: `Source scene not found: \u003csourceId\u003e`).",
        "operationId": "cloneScene",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CloneSceneRequest"
              }
            }
          },
          "required": false
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SceneSummaryResponse"
                }
              }
            },
            "description": "Scene cloned.",
            "headers": {
              "Location": {
                "description": "Path to the cloned scene — `/v1/scenes/{cloned_id}`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Clone a scene",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/scenes/{id}/view": {
      "post": {
        "description": "Opens the scene in the GM's local canvas without making it the world's active scene. Players are NOT switched. Use this to inspect / edit a scene without disrupting the table.\n\nReturns `{scene_id, viewed: true}`.",
        "operationId": "viewScene",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ViewSceneResponse"
                }
              }
            },
            "description": "Scene viewed by GM."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Open a scene in the GM's canvas view",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/scenes/{scene_id}/notes": {
      "get": {
        "description": "Returns every note pinned on the scene's canvas. Notes may be anchored to a journal entry (`entry_id`/`page_id`), carry free-text (`text`), or both.\n\n`notes[]` is always a non-nil array; empty when the scene has no notes.",
        "operationId": "listSceneNotes",
        "parameters": [
          {
            "in": "path",
            "name": "scene_id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NoteListResponse"
                }
              }
            },
            "description": "Notes on scene."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "List map-pinned notes on a scene",
        "tags": [
          "tier-dm"
        ]
      },
      "post": {
        "description": "Creates a map-pinned note at pixel coordinates `(x, y)`. Notes do NOT require a journal binding — free-text via `text` works standalone.\n\n201 with the created note.",
        "operationId": "createNote",
        "parameters": [
          {
            "in": "path",
            "name": "scene_id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateNoteRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NoteResponse"
                }
              }
            },
            "description": "Note created.",
            "headers": {
              "Location": {
                "description": "Path to the new note — `/v1/scenes/{scene_id}/notes/{id}`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Pin a note on a scene",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/scenes/{scene_id}/notes/{id}": {
      "delete": {
        "description": "Removes the note from the scene. Does NOT affect any bound journal entry. **Irreversible.**",
        "operationId": "deleteNote",
        "parameters": [
          {
            "in": "path",
            "name": "scene_id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteNoteResponse"
                }
              }
            },
            "description": "Deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Delete a pinned note",
        "tags": [
          "tier-dm"
        ]
      },
      "patch": {
        "description": "Partially updates a note's position, icon, text, or journal binding. **Five fields are tri-state nullable** (`entry_id`, `page_id`, `text`, `icon_tint`, `text_color`): omit to leave alone, send a value to set, or send the matching `clear_*: true` boolean to clear (Foundry stores null).\n\nEach `(field, clear_field)` pair is mutually exclusive — sending both → 422.",
        "operationId": "updateNote",
        "parameters": [
          {
            "in": "path",
            "name": "scene_id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateNoteRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NoteResponse"
                }
              }
            },
            "description": "Updated note."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Update a pinned note",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/scenes/{scene_id}/walls": {
      "get": {
        "description": "Returns every wall segment placed on the scene's canvas. Wall fields use string enum codes (`door`, `door_state`, `move`/`sense`/`sound`/`light`, `dir`) for clarity — the canonicalisation mirrors how the bridge exposes them.\n\n`walls[]` is always a non-nil array; empty when the scene has no walls.",
        "operationId": "listSceneWalls",
        "parameters": [
          {
            "in": "path",
            "name": "scene_id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WallListResponse"
                }
              }
            },
            "description": "Walls on scene."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "List walls on a scene",
        "tags": [
          "tier-dm"
        ]
      },
      "post": {
        "description": "Creates a wall with the given endpoint coordinates `c: [x1, y1, x2, y2]` (pixels). All enum fields use the bridge's defaults when omitted: `door: 'none'`, `door_state: 'closed'`, restrictions: `'normal'`, `dir: 'both'`.\n\n201 with the created wall.",
        "operationId": "createWall",
        "parameters": [
          {
            "in": "path",
            "name": "scene_id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateWallRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WallResponse"
                }
              }
            },
            "description": "Wall created.",
            "headers": {
              "Location": {
                "description": "Path to the new wall — `/v1/scenes/{scene_id}/walls/{id}`.",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Create a wall segment on a scene",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/scenes/{scene_id}/walls/{id}": {
      "delete": {
        "description": "Removes a wall segment from the scene. **Irreversible.**",
        "operationId": "deleteWall",
        "parameters": [
          {
            "in": "path",
            "name": "scene_id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteWallResponse"
                }
              }
            },
            "description": "Deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Delete a wall",
        "tags": [
          "tier-dm"
        ]
      },
      "patch": {
        "description": "Partially updates wall properties — coordinates, door type/state, restrictions, direction. All fields optional; empty body is a no-op.\n\n**404** when the wall doesn't exist.",
        "operationId": "updateWall",
        "parameters": [
          {
            "in": "path",
            "name": "scene_id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateWallRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WallResponse"
                }
              }
            },
            "description": "Updated wall."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Update a wall",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/scenes/{scene_id}/walls/{id}/door": {
      "post": {
        "description": "Changes the door state of a wall that has been configured as a door (`door: 'door'` or `'secret'`). The body's `state` is required.\n\n**502** when the wall exists but isn't a door (`door: 'none'`) — bridge returns `\"Wall X is not a door\"`. **404** when the wall (or scene) doesn't exist.\n\nResponse echoes the door type plus the previous and new state for client confirmation.",
        "operationId": "setDoorState",
        "parameters": [
          {
            "in": "path",
            "name": "scene_id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetDoorStateRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetDoorStateResponse"
                }
              }
            },
            "description": "Door state changed."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Open / close / lock a door",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/tokens": {
      "get": {
        "description": "Returns every token currently placed on a scene. `scene_id` query param targets a specific scene; omit it for the active scene.\n\n`tokens[]` is always non-nil; empty when the scene has no tokens. Each entry is the canonical `Token` shape — see schema for details on the canonicalisation between the bridge's `TokenResult` and `TokenDetail` shapes.",
        "operationId": "listSceneTokens",
        "parameters": [
          {
            "description": "Scene to list tokens from. Omit for the active scene.",
            "in": "query",
            "name": "scene_id",
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SceneTokensResponse"
                }
              }
            },
            "description": "Tokens on scene."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "List tokens placed on a scene",
        "tags": [
          "tier-dm"
        ]
      },
      "post": {
        "description": "Creates a new token by instancing an actor onto a scene at pixel coordinates `(x, y)`. Required: `actor_id`, `x`, `y`. Optional: `scene_id` (omit → active scene), `hidden`, `elevation`, `rotation` (degrees), `scale`.\n\n201 with the created `Token`. The response `scene_id` mirrors the request (or is empty when omitted — call `GET /v1/scenes` to resolve the active one).",
        "operationId": "createToken",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateTokenRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                }
              }
            },
            "description": "Token created.",
            "headers": {
              "Location": {
                "description": "Path to the new token — `/v1/tokens/{id}` (with `?scene_id=` when scene was specified).",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Place a token on a scene",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/tokens/by-actor/{actor_id}": {
      "get": {
        "description": "Convenience lookup: returns the first token on the target scene whose `actor_id` matches the path parameter. Useful for going from actor → placed-token without listing every token on the scene.\n\n**404** when no token on the scene references the actor (bridge error: `No token for actor \u003cactor_id\u003e on scene \u003cscene_id\u003e`).",
        "operationId": "getTokenByActor",
        "parameters": [
          {
            "in": "path",
            "name": "actor_id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Scene to look on. Omit for the active scene.",
            "in": "query",
            "name": "scene_id",
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                }
              }
            },
            "description": "Token bound to the actor."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Find the first token on a scene bound to an actor",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/tokens/clear-targets": {
      "post": {
        "description": "Releases every token currently in the GM's target set (global, not scene-scoped — affects targets across all scenes the GM has visited).\n\nIdempotent: returns `count: 0` when there were no targets to begin with.",
        "operationId": "clearTargets",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClearTargetsResponse"
                }
              }
            },
            "description": "Targets cleared."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Clear all of the GM's targeted tokens",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/tokens/in-range": {
      "get": {
        "description": "Returns tokens on a scene within `range` scene units (ft / m / etc — depends on scene grid config) of pixel coordinates `(origin_x, origin_y)`. Distance is Chebyshev (8-directional grid).\n\nResults are sorted by `distance` ascending. The lightweight `TokenRangeEntry` shape carries only the fields useful for tactical queries (id, name, x, y, distance, actor_id, disposition) — for full token detail, follow up with `GET /v1/tokens/{id}`.\n\nUse `exclude_token_id` to omit a specific token (e.g. when querying neighbours of a known token, exclude the token itself).",
        "operationId": "getTokensInRange",
        "parameters": [
          {
            "description": "Scene to search. Omit for the active scene.",
            "in": "query",
            "name": "scene_id",
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Origin X coordinate IN PIXELS.",
            "in": "query",
            "name": "origin_x",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "description": "Origin Y coordinate IN PIXELS.",
            "in": "query",
            "name": "origin_y",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "description": "Range IN SCENE UNITS (the scene's grid distance unit — typically ft or m).",
            "in": "query",
            "name": "range",
            "required": true,
            "schema": {
              "format": "double",
              "minimum": 0,
              "type": "number"
            }
          },
          {
            "description": "Optional token id to exclude from results.",
            "in": "query",
            "name": "exclude_token_id",
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokensInRangeResponse"
                }
              }
            },
            "description": "Tokens within range."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "List tokens within range of a pixel origin",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/tokens/{id}": {
      "delete": {
        "description": "Deletes a token from its scene. The bound actor is NOT affected — only the placed token instance is removed. **Irreversible.**",
        "operationId": "deleteToken",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Scene the token lives on. Omit for the active scene.",
            "in": "query",
            "name": "scene_id",
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeleteTokenResponse"
                }
              }
            },
            "description": "Deleted."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Remove a token from a scene",
        "tags": [
          "tier-dm"
        ]
      },
      "get": {
        "description": "Reads the canonical `Token` for a single token id. Bridge returns the `TokenDetail` shape which is canonicalised on our side (string `disposition` → int, `textureSrc` → `img`, `hp.current` → `hp.value`). **404** when not found.",
        "operationId": "getToken",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          },
          {
            "description": "Scene to look in. Omit for the active scene.",
            "in": "query",
            "name": "scene_id",
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                }
              }
            },
            "description": "Token detail."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Get a single token by id",
        "tags": [
          "tier-dm"
        ]
      },
      "patch": {
        "description": "Partially updates token metadata (visibility, elevation, rotation, scale, name, display flags, disposition, lock-rotation). Does NOT reposition — use `POST /v1/tokens/{id}/move` to change `x`/`y`.\n\nAll body fields optional; empty body is a no-op. `disposition` accepts the int enum: -2 (secret), -1 (hostile), 0 (neutral), 1 (friendly).",
        "operationId": "updateToken",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateTokenRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                }
              }
            },
            "description": "Updated token."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Update a token's metadata",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/tokens/{id}/move": {
      "post": {
        "description": "Repositions a token to pixel coordinates `(x, y)`. The bridge first attempts a direct move; if walls block the path, it runs A* pathfinding around them (assuming the destination is reachable). When `can_open_doors: true`, regular closed doors (Foundry door type 1, state 0) encountered along the A* path are opened en route (door state changes persist on the scene).\n\nResponse includes `path_cost` (grid cells traversed) and `doors_opened` (wall IDs that were opened) WHEN pathfinding actually ran; both omitted for direct moves.\n\nOptional `animate` (default true) — only the final waypoint animates when pathfinding is used; intermediate waypoints jump instantly.\n\n502 with `\"Path blocked — no valid route to destination\"` when the target is unreachable.",
        "operationId": "moveToken",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MoveTokenRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TokenResponse"
                }
              }
            },
            "description": "Token moved."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Move a token (with optional A* pathfinding + door-opening)",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/tokens/{id}/target": {
      "post": {
        "description": "Adds (`targeted: true`) or removes (`targeted: false`) a token from the GM's target set. **Local to the calling user (the GM); not broadcast to players.**\n\n`release_others` (default `true` on the bridge) controls multi-target behaviour: when true, the GM's other targets are released first; when false, the change is additive (multi-target).\n\n**`targeted` is required** — clients must explicitly choose to target or untarget. (Modelled as nullable + validated-required because go-validator would otherwise reject the literal `false` as the zero value.)",
        "operationId": "setTokenTarget",
        "parameters": [
          {
            "in": "path",
            "name": "id",
            "required": true,
            "schema": {
              "minLength": 1,
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetTokenTargetRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SetTokenTargetResponse"
                }
              }
            },
            "description": "Target state updated."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/TierForbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "422": {
            "$ref": "#/components/responses/Unprocessable"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Target or untarget a token (GM's local target set)",
        "tags": [
          "tier-dm"
        ]
      }
    },
    "/world": {
      "get": {
        "description": "Returns the metadata snapshot of the tenant's currently-loaded Foundry world (id, title, game system, engine and system versions). Backed by a `get-world-info` command sent over the bridge module's WebSocket connection — requires the user's Foundry instance to be online and connected.",
        "responses": {
          "200": {
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WorldInfoResponse"
                }
              }
            },
            "description": "World metadata"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "502": {
            "$ref": "#/components/responses/BadGateway"
          },
          "503": {
            "$ref": "#/components/responses/ServiceUnavailable"
          },
          "504": {
            "$ref": "#/components/responses/GatewayTimeout"
          }
        },
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "summary": "Get world metadata",
        "tags": [
          "tier-guest"
        ]
      }
    }
  },
  "servers": [
    {
      "description": "API v1",
      "url": "/v1"
    }
  ],
  "tags": [
    {
      "description": "Endpoints reachable without authentication. Used for health checks and other infrastructure-level probes.",
      "name": "public",
      "x-displayName": "Public"
    },
    {
      "description": "Available to any caller with a valid API key, regardless of Patreon subscription status. The minimum authenticated surface.",
      "name": "tier-guest",
      "x-displayName": "Guest tier"
    },
    {
      "description": "Available to free Patreon subscribers and above (`free`, `adventurer`, `dm`).",
      "name": "tier-free",
      "x-displayName": "Free tier"
    },
    {
      "description": "Available to Adventurer Patreon subscribers and above (`adventurer`, `dm`).",
      "name": "tier-adventurer",
      "x-displayName": "Adventurer tier"
    },
    {
      "description": "Available to Dungeon Master Patreon subscribers only — the highest tier.",
      "name": "tier-dm",
      "x-displayName": "DM tier"
    }
  ]
}