# Battlemat API Skill (for AI Agents)

Use this guide when controlling Battlemat over HTTP. It documents the routes an automation agent should prefer, plus the sharp edges that matter in practice.

`GET /openapi.json` is available as the source of truth for advertised request and response shapes. Internal, admin, webhook, legacy, credential-management, billing-session, and development-only routes are intentionally excluded from the generated schema and rendered docs at `/docs` and `/redoc`.

## 1) Authentication

All agent traffic will use a bearer API token that starts with `bmat_pat_`.

- `POST /auth/api-tokens` creates a token.
- `GET /auth/api-tokens` lists existing tokens.
- `DELETE /auth/api-tokens/{api_token_id}` revokes a token.

Important details:

- These API-token management routes require a human access JWT, not an API token.
- Prefer `kind: "agent"` when minting a token for an automated workflow.
- `scopes` are stored and returned but are not currently enforced server-side. Treat them as metadata, not a hard permission boundary.

Create an agent token:

```bash
curl -X POST "$BASE_URL/auth/api-tokens" \
  -H "Authorization: Bearer $HUMAN_ACCESS_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "automation",
    "kind": "agent",
    "scopes": ["*"]
  }'
```

Use the returned token for subsequent requests:

```http
Authorization: Bearer bmat_pat_...your_token...
```

## 2) Agent-Preferred Route Surface

Default routes for autonomous work:

- Game lifecycle: `POST /games`, `GET /games/{game_id}`, `DELETE /games/{game_id}`
- Mats: `POST /games/{game_id}/mats`, `GET /games/{game_id}/mats`, `GET|PUT|PATCH|DELETE /mats/{mat_id}`
- Active mat: `PUT /games/{game_id}/active-mat/{mat_id}`
- Tokens: `POST /tokens`, `GET|PUT|DELETE /tokens/{token_id}`, `GET /users/{user_id}/tokens`
- Token roll definitions: `GET /tokens/{token_id}/rolls`, `DELETE /tokens/{token_id}/rolls/{roll_id}`
- User discovery: `GET /users/{user_id}/games`, `GET /users/{user_id}/accessible-games`, `GET /users/{user_id}/assets`, `GET /users/{user_id}/tokens`
- Assets: `POST /assets`, `POST /games/{game_id}/assets`, `GET /games/{game_id}/assets`, `GET /assets/{asset_id}`, `GET /assets/{asset_id}/data`
- Backgrounds: `PUT /mats/{mat_id}/background/{asset_id}`, `DELETE /mats/{mat_id}/background`
- AI image generation: `POST /assets/generate`, `GET /users/me/image-generation-usage`
- Roll log: `POST /games/{game_id}/rolls`, `GET /games/{game_id}/rolls`
- Player management: `GET /games/{game_id}/players`, `POST /games/{game_id}/players?player_id={player_id}`, `DELETE /games/{game_id}/players/{player_id}`

Routes to avoid unless the task explicitly calls for them:

- Human login UX: `POST /auth/email`, `POST /auth/login`, `POST /auth/refresh`
- Billing: `/billing/*`
- Public invite/join flow: `GET /games/{game_id}/invite`, `POST /games/{game_id}/join`

## 3) Core Workflow: game -> mat -> active mat -> token metadata -> board state

### Create a game

`POST /games?name=<game_name>`

```bash
curl -X POST "$BASE_URL/games" \
  -G --data-urlencode "name=Session 1" \
  -H "Authorization: Bearer $BATTLEMAT_API_TOKEN"
```

Response includes `id` (`game_id`) and the owning `user_id`.

### Create a mat in a game

`POST /games/{game_id}/mats?name=<mat_name>&rows=48&columns=48`

```bash
curl -X POST "$BASE_URL/games/$GAME_ID/mats" \
  -G \
  --data-urlencode "name=Main Map" \
  --data-urlencode "rows=48" \
  --data-urlencode "columns=48" \
  -H "Authorization: Bearer $BATTLEMAT_API_TOKEN"
```

Notes:

- `rows` and `columns` default to `48` and must each be between `5` and `100`.
- New mats start with `data.gridSize` and `data.matPermissions`.

### Set or clear the active mat

Set active mat:

```bash
curl -X PUT "$BASE_URL/games/$GAME_ID/active-mat/$MAT_ID" \
  -H "Authorization: Bearer $BATTLEMAT_API_TOKEN"
```

Clear active mat:

```bash
curl -X PUT "$BASE_URL/games/$GAME_ID/active-mat/none" \
  -H "Authorization: Bearer $BATTLEMAT_API_TOKEN"
```

### Create token metadata

Create a mat-scoped token:

`POST /tokens?mat_id={mat_id}`

```bash
curl -X POST "$BASE_URL/tokens?mat_id=$MAT_ID" \
  -H "Authorization: Bearer $BATTLEMAT_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "entity_type": "token",
    "name": "Goblin",
    "description": "Small green raider",
    "hidden": false,
    "initiative_modifier": 2,
    "hit_points": 7
  }'
```

Important details:

- `POST /tokens` accepts `mat_id` as an optional query param. There is no `game_id` query param on this endpoint.
- Library tokens omit `mat_id` and must use `entity_type: "token"`.
- Mat-scoped tokens may use `entity_type: "token"` or `entity_type: "object"`.
- For mat-scoped tokens, you may optionally provide `id` in the JSON body if you need a caller-chosen token ID.
- `source_token_id` must reference a library token owned by the caller.

Update token metadata:

`PUT /tokens/{token_id}`

```bash
curl -X PUT "$BASE_URL/tokens/$TOKEN_ID" \
  -H "Authorization: Bearer $BATTLEMAT_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Goblin Boss",
    "initiative_modifier": 4,
    "hit_points": 21
  }'
```

### Patch board-visible state in `mat.data`

Battlemat separates token metadata from board state:

- `/tokens` stores token metadata such as `name`, `description`, `asset_id`, `initiative_modifier`, and `hit_points`.
- `mat.data` stores the live board state such as positions, initiative order, fog, paint, and placed objects.

For a visible board token, the common flow is:

1. Create token metadata with `POST /tokens?mat_id=...`
2. Patch `mat.data.tokensById` and `mat.data.tokenOrder` to add or move the visual token entry

## 4) Mat Data and JSON Patch

`PUT /mats/{mat_id}` replaces the entire `data` object.

`PATCH /mats/{mat_id}` applies RFC 6902 JSON Patch with body shape:

```json
{
  "patch": [
    {"op": "replace", "path": "/tokensById/token-1/x", "value": 11}
  ]
}
```

Important details:

- `mat.data` is canonical `MatStateV3`; legacy `tokens`, `placedObjects`, `cellColors`, and `fogCells` fields are rejected by runtime validation.
- `PATCH /mats/{mat_id}` accepts RFC 6902 JSON Patch either as a raw array or as `{ "patch": [...] }`.
- Successful mutating patches increment `revision`. Use `If-Match: <revision>` or a JSON Patch `test` operation on `/revision` for conflict detection.
- The server validates the full state after every patch and rejects malformed results atomically.
- WebSocket entity changes use `matPatch` messages with RFC 6902 JSON Patch operations. Paint and fog gestures use `matOverlayMutation`, which the server merges into canonical vector geometry. Older granular messages such as `tokenCreate`, `cellColorBatchUpdate`, and `gridSizeUpdate` are rejected.

Recommended `mat.data` shape:

```json
{
  "schemaVersion": 3,
  "revision": 0,
  "gridSize": {"rows": 48, "columns": 48},
  "matPermissions": {
    "playersCanPlaceTokens": true,
    "playersCanPlaceObjects": true,
    "playersCanUsePaintbrush": true
  },
  "tokensById": {
    "token-1": {
      "id": "token-1",
      "name": "Goblin",
      "icon": "G",
      "color": "#5a8f29",
      "x": 10,
      "y": 12,
      "stackPosition": 0,
      "initiative": 14,
      "initiativeModifier": 2,
      "initiativeOrder": 0,
      "hitPoints": 7,
      "size": "medium",
      "owner": "user-123",
      "background_asset_id": null,
      "game_id": "game-123",
      "isActive": false,
      "conditions": ["poisoned"]
    }
  },
  "tokenOrder": ["token-1"],
  "paintRegionsByColor": {
    "#ffcc00": [[[[9, 11], [10, 11], [10, 12], [9, 12], [9, 11]]]]
  },
  "fogRegions": [[[[8, 11], [9, 11], [9, 13], [8, 13], [8, 11]]]],
  "objectsById": {
    "1": {"id": 1, "x": 8, "y": 8, "emoji": "🌲", "size": "medium", "owner": "user-123"}
  },
  "objectOrder": ["1"],
  "nextTokenId": 2,
  "nextObjectId": 2
}
```

Practical conventions:

- `tokensById` contains board-visible instances keyed by stringified token ID. `tokenOrder` preserves rendering/order semantics.
- `objectsById` and `objectOrder` follow the same map-plus-order pattern for map objects.
- Token metadata still lives in `/tokens`. Prefer using the real token ID returned by `POST /tokens` as the board token `id`.
- `paintRegionsByColor` maps each exact color string to a `MultiPolygon`; `fogRegions` is a `MultiPolygon`. Coordinates are continuous grid units and rings are closed.
- Polygon arrays use exterior-ring-first ordering followed by holes. Geometry is clipped to the board, snapped to `0.01`, non-overlapping across paint colors, and rendered with even-odd filling.
- Prefer semantic WebSocket overlay commands for paint/fog edits. They are revision-checked and merged authoritatively, so a stale client cannot overwrite another editor's geometry. Direct HTTP patching should only replace complete, already-canonical overlay fields.
- Keep `gridSize.rows` and `gridSize.columns` between `5` and `100` to match normal mat creation constraints.
- `nextTokenId` and `nextObjectId` are coordination counters used by interactive clients. If your automation always uses explicit token IDs from `/tokens` and caller-chosen object IDs, you usually do not need to touch either counter.
- Only one board token should have `isActive: true` at a time. The WebSocket protocol has a dedicated `activeToken` message; over HTTP, emulate it by patching all affected token entries in one request.

Examples:

Move a token:

```bash
curl -X PATCH "$BASE_URL/mats/$MAT_ID" \
  -H "Authorization: Bearer $BATTLEMAT_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "If-Match: $REVISION" \
  -d '{
    "patch": [
      {"op": "replace", "path": "/tokensById/'"$TOKEN_ID"'/x", "value": 11},
      {"op": "replace", "path": "/tokensById/'"$TOKEN_ID"'/y", "value": 13}
    ]
  }'
```

Add a new board token entry after creating its metadata:

```bash
curl -X PATCH "$BASE_URL/mats/$MAT_ID" \
  -H "Authorization: Bearer $BATTLEMAT_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "patch": [
      {
        "op": "add",
        "path": "/tokensById/'"$TOKEN_ID"'",
        "value": {
          "id": "'"$TOKEN_ID"'",
          "name": "Goblin",
          "icon": "G",
          "color": "#5a8f29",
          "x": 10,
          "y": 12,
          "initiative": 14,
          "initiativeModifier": 2,
          "hitPoints": 7,
          "size": "medium"
        }
      },
      {"op": "add", "path": "/tokenOrder/-", "value": "'"$TOKEN_ID"'"}
    ]
  }'
```

Paint a continuous blue stroke over WebSocket (after subscribing to the mat):

```json
{
  "type": "matOverlayMutation",
  "requestId": "paint-1",
  "mat_id": "mat-uuid",
  "baseRevision": 7,
  "overlay": "color",
  "operation": "paint",
  "color": "#33aaff",
  "diameter": 2,
  "points": [[11.25, 13.5], [14.75, 13.5]]
}
```

Erase fog with the same protocol:

```json
{
  "type": "matOverlayMutation",
  "requestId": "fog-erase-1",
  "mat_id": "mat-uuid",
  "baseRevision": 8,
  "overlay": "fog",
  "operation": "erase",
  "diameter": 1,
  "points": [[10.5, 10.5]]
}
```

Place or edit map objects:

```bash
curl -X PATCH "$BASE_URL/mats/$MAT_ID" \
  -H "Authorization: Bearer $BATTLEMAT_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "patch": [
      {
        "op": "add",
        "path": "/objectsById/1",
        "value": {
          "id": 1,
          "emoji": "🌲",
          "x": 8,
          "y": 8,
          "size": "large",
          "owner": "'"$USER_ID"'"
        }
      },
      {"op": "add", "path": "/objectOrder/-", "value": "1"},
      {"op": "replace", "path": "/nextObjectId", "value": 2}
    ]
  }'
```

Move or resize an existing object:

```bash
curl -X PATCH "$BASE_URL/mats/$MAT_ID" \
  -H "Authorization: Bearer $BATTLEMAT_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "patch": [
      {"op": "replace", "path": "/objectsById/1/x", "value": 9},
      {"op": "replace", "path": "/objectsById/1/y", "value": 9},
      {"op": "replace", "path": "/objectsById/1/size", "value": "giant"}
    ]
  }'
```

Delete an object:

```bash
curl -X PATCH "$BASE_URL/mats/$MAT_ID" \
  -H "Authorization: Bearer $BATTLEMAT_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "patch": [
      {"op": "remove", "path": "/objectsById/1"},
      {"op": "remove", "path": "/objectOrder/0"}
    ]
  }'
```

Clear all paint or fog with one semantic command per overlay:

```json
{
  "type": "matOverlayMutation",
  "requestId": "clear-paint-1",
  "mat_id": "mat-uuid",
  "baseRevision": 9,
  "overlay": "color",
  "operation": "clear"
}
```

Fog also supports `"operation": "fill"`, producing one canonical board-sized rectangle.

Resize the grid or update mat permissions:

```bash
curl -X PATCH "$BASE_URL/mats/$MAT_ID" \
  -H "Authorization: Bearer $BATTLEMAT_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "patch": [
      {"op": "replace", "path": "/gridSize", "value": {"rows": 36, "columns": 54}},
      {"op": "replace", "path": "/matPermissions", "value": {
        "playersCanPlaceTokens": true,
        "playersCanPlaceObjects": false,
        "playersCanUsePaintbrush": false
      }}
    ]
  }'
```

Set the active initiative token over HTTP:

```bash
curl -X PATCH "$BASE_URL/mats/$MAT_ID" \
  -H "Authorization: Bearer $BATTLEMAT_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "patch": [
      {"op": "replace", "path": "/tokensById/token-1/isActive", "value": false},
      {"op": "replace", "path": "/tokensById/token-2/isActive", "value": true}
    ]
  }'
```

## 5) Read State

- `GET /games/{game_id}` returns game metadata, including `active_mat_id`
- `GET /games/{game_id}/mats` lists mats for a game
- `GET /mats/{mat_id}` returns the full mat, including `data`
- `GET /tokens/{token_id}` returns token metadata
- `GET /users/{user_id}/games` lists games owned by the caller
- `GET /users/{user_id}/accessible-games` lists owned and shared games for the caller
- `GET /users/{user_id}/assets` lists the caller's personal and game-linked assets they own
- `GET /users/{user_id}/tokens` lists the caller's library tokens
- `GET /games/{game_id}/players` lists players with access

## 6) Assets and Backgrounds

Upload a personal asset:

- `POST /assets` with `multipart/form-data`

Upload a game asset:

- `POST /games/{game_id}/assets` with `multipart/form-data`
- Optional query param: `mat_id`

Supported uploads:

- MIME types: `image/png`, `image/jpeg`, `image/webp`
- Extensions: `.png`, `.jpg`, `.jpeg`, `.webp`
- Max size: `10 MB`

Generate an image asset asynchronously:

`POST /assets/generate`

```bash
curl -X POST "$BASE_URL/assets/generate" \
  -H "Authorization: Bearer $BATTLEMAT_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "prompt": "top-down ruined stone altar in a swamp",
    "size": "1024x1024"
  }'
```

Notes:

- The response returns an `Asset` immediately with `generation_status: "pending"`.
- Poll `GET /assets/{asset_id}` until `generation_status` becomes `completed` or `failed`.
- Check quota with `GET /users/me/image-generation-usage`.

Set a mat background:

```bash
curl -X PUT "$BASE_URL/mats/$MAT_ID/background/$ASSET_ID" \
  -H "Authorization: Bearer $BATTLEMAT_API_TOKEN"
```

Remove a mat background:

```bash
curl -X DELETE "$BASE_URL/mats/$MAT_ID/background" \
  -H "Authorization: Bearer $BATTLEMAT_API_TOKEN"
```

## 7) Rolls

Create a roll-log entry:

`POST /games/{game_id}/rolls`

```bash
curl -X POST "$BASE_URL/games/$GAME_ID/rolls" \
  -H "Authorization: Bearer $BATTLEMAT_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": "'"$USER_ID"'",
    "username": "Automation",
    "roll_expression": "1d20+5",
    "result": 18
  }'
```

Important details:

- `entry.user_id` must match the authenticated caller.
- Read rolls with `GET /games/{game_id}/rolls?limit=50&offset=0`.
- Token descriptions may auto-generate roll definitions in the background.
- Read generated token rolls with `GET /tokens/{token_id}/rolls`.
- `GET /tokens/{token_id}/rolls` returns roll definitions, not executed results.
- There is no server-side "roll this token action now" endpoint. Agents are expected to evaluate the dice locally, then append the outcome to the game roll log.

Roll a specific token:

1. `GET /tokens/{token_id}` to inspect the token metadata.
2. Compute `rolls_source_id = source_token_id || token_id`.
3. `GET /tokens/{rolls_source_id}/rolls`.
4. Choose a roll definition and evaluate it locally as `num_dice` rolls of a `dice_type`-sided die, then apply `modifier`.
5. `POST /games/{game_id}/rolls` with a human-readable `roll_expression` such as `1d20+5` and the final numeric `result`.

Important details for mat tokens:

- If a placed token was created from a library token, its generated roll definitions usually live on the library token referenced by `source_token_id`, not only on the placed token itself.
- A good default `roll_expression` is `${num_dice}d${dice_type}` plus the signed modifier when non-zero.

## 8) Permissions and Gotchas

- API-token auth resolves to the token owner for normal API calls.
- API-token creation, listing, and revocation require a human access JWT.
- DM-only routes include game deletion, mat creation/deletion, full mat mutation, active-mat updates, player add/remove, and background assignment.
- Mat token CRUD uses mat access checks, not DM-only checks. Client-side `matPermissions` exist in `mat.data`, but the REST token endpoints do not enforce those permission flags.
- `GET /games/{game_id}/invite` and `POST /games/{game_id}/join` are intentionally public. Only use them when automating the invite flow explicitly.
- Prefer the unified roll endpoints under `/tokens/{token_id}/rolls...`; older compatibility aliases are intentionally not documented in the published schema.
- Treat `DELETE /tokens/{token_id}/rolls/{roll_id}` as an advanced/manual route unless you already have a concrete `roll_id` from the response you are consuming. The published schema focuses on roll-definition fields, not identifier discovery.
- Use `GET /assets/{asset_id}` and `GET /assets/{asset_id}/data` only for assets you already own or can legitimately access through game membership. Do not treat those endpoints as your authorization oracle.

## 9) Minimal Automation Loop

1. Mint or obtain a bearer API token.
2. `POST /games`
3. `POST /games/{game_id}/mats`
4. `PUT /games/{game_id}/active-mat/{mat_id}` if the mat should be the live board
5. Create token metadata with `POST /tokens?mat_id=...`
6. `PATCH /mats/{mat_id}` to place or move board tokens, paint, fog, or objects
7. Re-read `GET /mats/{mat_id}` before applying the next patch batch
