# The Iron Republic — Play API

The permanent API for 2-4 human/bot multiplayer rooms in The Iron Republic, a hidden-information area-control game. Create invitations for the PWA, join human seats, start the room, and submit seat-redacted actions. The legacy /games API remains available for solo play against hosted bots. Fetch /openapi.json for the machine spec, / for examples, or /llms.txt for a compact flow.

- Machine spec: [`/openapi.json`](/openapi.json) (OpenAPI 3.1)
- LLM pointer: [`/llms.txt`](/llms.txt)

## Multiplayer room auth
Every `/v2/rooms` credential is a secret bearer value sent only as
`Authorization: Bearer <capability>`. Query parameters and cookies are never accepted for v2.
- The deployment's creator bearer can create rooms, but cannot host, inspect a seat, or act.
- `host_capability` can start/delete its room and read public metadata; it cannot see seat-private game state or act.
- Each human seat `capability` can join, see only that seat's redacted state, and act only for that seat.
Capabilities are random, room-scoped secrets stored server-side only as hashes. Do not log or share them.
Creation returns each seat's `join_descriptor` and PWA `invite_url`; the URL fragment carries the descriptor to the PWA and is not API authentication.

## Multiplayer room endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | `/v2/rooms` | Creator bearer: create a 2-4 human/bot room and invitations |
| POST | `/v2/rooms/{id}/join` | Seat bearer: join with `display_name` |
| POST | `/v2/rooms/{id}/start[?detail=compact|full]` | Host bearer: start after all humans join |
| GET | `/v2/rooms/{id}/state[?detail=compact|full]` | Host metadata or this seat's redacted state |
| POST | `/v2/rooms/{id}/actions[?detail=compact|full]` | Seat bearer: submit one revision-bound action |
| DELETE | `/v2/rooms/{id}` | Host bearer: permanently delete the room |

## Multiplayer flow

1. Create a room. Controllers are in seat order; bot `agent` must be hosted:
```
POST /v2/rooms
Authorization: Bearer $CREATOR_TOKEN
{"controllers":[{"kind":"human"},{"kind":"bot","agent":"heuristic"},{"kind":"human"}],"seed":7,"open_hand":false,"objectives":true,"set":"iron-standard"}
```
The response contains room metadata, `host_capability`, and one `{seat, capability, join_descriptor, invite_url}` record per human seat.

2. Open each `invite_url` in the PWA, or join directly with its seat capability:
```
POST /v2/rooms/0123456789abcdef0123456789abcdef/join
Authorization: Bearer $SEAT_CAPABILITY
{"display_name":"Alice"}
```

3. Start with the host capability, then each seat reads its own state:
```
POST /v2/rooms/0123456789abcdef0123456789abcdef/start
Authorization: Bearer $HOST_CAPABILITY
GET /v2/rooms/0123456789abcdef0123456789abcdef/state
Authorization: Bearer $SEAT_CAPABILITY
```

4. When `acting_seat == you`, submit the current revision and briefing action:
```
POST /v2/rooms/0123456789abcdef0123456789abcdef/actions
Authorization: Bearer $SEAT_CAPABILITY
{"expected_revision":3,"request_id":"client-unique-1","decision_id":"r1-d0","action":"objkeep"}
```
Retry the exact body with the same `request_id` after an ambiguous network failure. A stale `expected_revision`, reused request id with different content, or concurrent mutation returns 409.
Active seat state may contain `briefing`; finished seat state contains `scores`, `coins`, `winners`, and `final_summary`. Host state remains metadata-only.

## Legacy solo API
The legacy `/games` flow is one human against hosted bots. Its deployment bearer uses the older browser behavior: `Authorization`, `?t=<token>`, or a cookie may be accepted.
- `POST /games[?detail=compact|full]` creates a solo game.
- `GET /games/{id}[?detail=compact|full]` recovers current or terminal state.
- `GET /games/{id}/pending[?format=md&detail=compact|full]` returns the current briefing when pending.
- `POST /games/{id}/action` submits `{decision_id, action}`.
- `GET /games/{id}/transcript` returns the markdown transcript.
