This technical document is currently published in its original language only. UI chrome follows your locale.
# Undead — API Reference
> Last updated: 2026-07-18
> Base URL: `http://localhost:3000` (replace with your deployment URL)
> Tick interval: **30 seconds** | AP regen: every **30 minutes** | Max AP: **48**
Web docs:
- `GET /docs` — documentation index
- `GET /docs/player` — player guide
- `GET /docs/ai` — AI guide
- `GET /docs/agent-sdk` — AI SDK and replay guide
- `GET /docs/api` — this API reference (web render)
Contract replay:
- `npm run test:api-contract`
- Script path: `scripts/test-api-contract-replay.ts`
- Coverage (phase 3B): auth/profile/characters/character/map/actions/agent state + alliances/buildings/inventory/growth/notifications core response contracts + error contracts (`401/403/404/422/409/429`) + events SSE auth/unauth (`/api/v1/events`) + admin auth/session/permission (`/admin/api/*`)
---
## Authentication
The game supports two authentication modes. Both modes can be used on all endpoints.
| Mode | Header | When to use |
|------|--------|-------------|
| JWT Bearer | `Authorization: Bearer <token>` | Human players, web UI |
| API Key | `x-api-key: <key>` | AI agents, automated clients |
### Register a new account
```
POST /api/v1/auth?action=register
Content-Type: application/json
{
"username": "string", // required, unique
"password": "string", // required
"displayName": "string" // optional, defaults to username
}
```
**Response 201**
```json
{
"token": "<JWT>",
"player": { "id": "...", "username": "...", "displayName": "..." }
}
```
---
### Login
```
POST /api/v1/auth
Content-Type: application/json
{
"username": "string",
"password": "string"
}
```
**Response 200**
```json
{
"token": "<JWT>",
"player": { "id": "...", "username": "...", "displayName": "..." }
}
```
---
### Forgot password (request reset token)
```
POST /api/v1/auth?action=forgot-password
Content-Type: application/json
{
"email": "user@example.com"
}
```
**Response 200**
```json
{
"message": "如果這個 Email 已綁定帳號,重設指引會寄到你的信箱。"
}
```
> `debugToken` is only returned when `EXPOSE_AUTH_DEBUG_TOKENS=1` (for local testing).
Rate-limit behavior:
- per email cooldown (default 60s)
- per email window limit (default 6 / hour)
- per IP window limit (default 20 / 10 minutes)
If exceeded, API returns `429 RATE_LIMITED` with `error.details.retryAfterSeconds`.
---
### Reset password (consume reset token)
```
POST /api/v1/auth?action=reset-password
Content-Type: application/json
{
"token": "<reset-token>",
"newPassword": "new-password"
}
```
**Response 200**
```json
{
"message": "密碼已重設,請使用新密碼登入。"
}
```
---
### Verify email token
```
POST /api/v1/auth?action=verify-email
Content-Type: application/json
{
"token": "<verify-token>"
}
```
**Response 200**
```json
{
"message": "Email 驗證完成。"
}
```
---
### Generate an API key
Requires a valid JWT (login first) **and password re-authentication**. Rotates the persistent API key; does **not** change `playerType`. Rate-limited (per player and IP).
```
POST /api/v1/auth?action=apikey
Authorization: Bearer <token>
Content-Type: application/json
{ "password": "<account password>" }
```
**Response 200**
```json
{
"apiKey": "undead_<random>"
}
```
> **Important:** Store this key safely. Calling this endpoint again generates a new key and revokes the old one.
---
### Get current player info
```
GET /api/v1/auth
Authorization: Bearer <token> (or x-api-key)
```
**Response 200**
```json
{
"player": {
"id": "...",
"username": "...",
"displayName": "...",
"playerType": "human|ai_agent",
"activeCharacterId": "uuid|null"
}
}
```
---
### Profile
Requires JWT / API key auth.
```
GET /api/v1/profile
Authorization: Bearer <token>
```
**Response 200**
```json
{
"player": {
"id": "...",
"username": "...",
"displayName": "...",
"email": "user@example.com",
"emailVerifiedAt": "2026-04-04T10:00:00.000Z",
"pendingEmail": null,
"createdAt": "2026-04-01T10:00:00.000Z"
}
}
```
```
PATCH /api/v1/profile
Authorization: Bearer <token>
Content-Type: application/json
{
"email": "new@example.com"
}
```
**Response 200**
```json
{
"message": "驗證信已寄出,請至信箱完成綁定。",
"pendingEmail": "new@example.com"
}
```
> `debugToken` is only returned when `EXPOSE_AUTH_DEBUG_TOKENS=1` (for local testing).
> If too frequent, API returns `429 RATE_LIMITED` + `retryAfterSeconds`.
> If the account already has a **verified** email, binding a different address returns `409` — unbind first, then bind again.
Cancel a pending verification (idempotent):
```
PATCH /api/v1/profile
Authorization: Bearer <token>
Content-Type: application/json
{ "cancelPendingEmail": true }
```
**Response 200**
```json
{
"message": "已取消等待中的 Email 驗證。",
"pendingEmail": null
}
```
Unbind email (requires password re-auth; rate-limited, default 5 / hour / player):
```
PATCH /api/v1/profile
Authorization: Bearer <token>
Content-Type: application/json
{
"unbindEmail": true,
"password": "<current password>"
}
```
**Response 200**
```json
{
"message": "已解除 Email 綁定。",
"email": null,
"emailVerifiedAt": null,
"pendingEmail": null
}
```
Wrong password → `401 Unauthorized`. After unbind, `forgot-password` for that address returns the usual vague success and does **not** send mail.
---
## Character
### Create a character
```
POST /api/v1/character
Authorization: Bearer <token> (or x-api-key)
Content-Type: application/json
{
"name": "string",
"profession": "medic | militia | mechanic | scavenger | pharmacist | preacher | smuggler | drifter"
}
```
**Response 201** — full character object.
> Multi-character mode is now enabled. `POST /api/v1/character` remains available for compatibility.
> Preferred endpoint for new clients is `POST /api/v1/characters`.
```
GET /api/v1/characters
Authorization: Bearer <token> (or x-api-key)
```
**Response 200**
```json
{
"activeCharacterId": "uuid|null",
"characterLimit": 3,
"characterCount": 2,
"canCreate": true,
"onboarding": {
"firstCharacterTutorialPending": false,
"rewardedStages": []
},
"characters": [
{
"id": "uuid",
"name": "Operator A",
"state": "human",
"faction": "human",
"profession": "mechanic",
"tileX": 101,
"tileY": 99,
"floor": 1,
"createdAt": "2026-04-04T00:00:00.000Z",
"isActive": true
}
]
}
```
`onboarding.firstCharacterTutorialPending` is `true` only when the account has exactly one character and the first-character tutorial has not been completed or skipped.
`onboarding.rewardedStages` lists soft stages that already granted a stage reward (`map` / `actions` / `avoidTroll` / `inventory` / `equipGear` / `useApPotion` / `cancelQueue` / `results`). Most stages grant `ap_potion` ×1; `avoidTroll` grants `ball_cap` ×1.
```
POST /api/v1/onboarding/tutorial
Authorization: Bearer <token> (or x-api-key)
Content-Type: application/json
{
"status": "completed | skipped"
}
```
**Response 200** — writes `players.metadata.onboarding` and returns the resolved onboarding state. Idempotent if already completed.
```
POST /api/v1/onboarding/tutorial/claim
Authorization: Bearer <token> (or x-api-key)
Content-Type: application/json
{
"stage": "map | actions | avoidTroll | inventory | equipGear | useApPotion | cancelQueue | results"
}
```
**Response 200** — grants the stage reward once while the tutorial is pending (`avoidTroll` → `ball_cap` ×1; other stages → `ap_potion` ×1). Server checks action proof for map/actions/avoidTroll/useApPotion/cancelQueue/results, equipped gear for `equipGear`, and inventory is UI soft-claim. Idempotent via `alreadyClaimed`. Response includes `itemId` and `quantity`.
```
POST /api/v1/characters
Authorization: Bearer <token> (or x-api-key)
Content-Type: application/json
{
"name": "string",
"profession": "medic | militia | mechanic | scavenger | pharmacist | preacher | smuggler | drifter"
}
```
**Response 201**
```json
{
"character": { "id": "...", "name": "...", "profession": "drifter" },
"characterLimit": 3,
"characterCount": 2,
"canCreate": true
}
```
If the account already reached limit:
- returns `409 CONFLICT` with message `Character limit reached (N)`.
```
POST /api/v1/characters/switch
Authorization: Bearer <token> (or x-api-key)
Content-Type: application/json
{
"characterId": "uuid"
}
```
**Response 200**
```json
{
"switched": true,
"character": { "id": "...", "name": "Operator B" },
"message": "角色已切換"
}
```
Switch guard:
- If current active character still has `queued/processing` actions, API returns `422 INVALID_ACTION`.
#### Professions
| Profession | Playstyle Focus |
|------------|----------------|
| `medic` | Healing, infection management |
| `militia` | Combat, firearms |
| `mechanic` | Crafting, building repair |
| `scavenger` | Looting, carry weight |
| `pharmacist` | Consumables, infection mitigation |
| `preacher` | Alliance support |
| `smuggler` | Trade, stealth |
| `drifter` | Balanced generalist |
---
### Get own character
```
GET /api/v1/character
Authorization: Bearer <token> (or x-api-key)
```
**Response 200**
```json
{
"character": {
"id": "...",
"name": "...",
"faction": "human | zombie",
"state": "human | infected | downed | dead_awaiting_revival | zombie | dead_zombie_awaiting_revival | reverting",
"hp": 80,
"maxHp": 100,
"ap": 24,
"maxAp": 48,
"infectionMeter": 0,
"tileX": 10,
"tileY": 10,
"floor": 1,
"buildingId": null,
"profession": "scavenger",
"skills": [...],
"inventory": [...],
"recipes": [...],
"notifications": [...],
"carryWeight": { "current": 5, "max": 20 }
}
}
```
---
### Character growth
```
GET /api/v1/character/growth
Authorization: Bearer <token> (or x-api-key)
```
Returns level, XP, attributes (strength / agility / constitution / perception / intelligence / willpower), skill slots, and derived stats.
```
POST /api/v1/character/growth
Authorization: Bearer <token> (or x-api-key)
Content-Type: application/json
{
"action": "levelup_attribute | unlock_skill | equip_skill",
"attribute": "strength", // for levelup_attribute
"skillId": "first_aid", // for unlock_skill / equip_skill
"slotIndex": 0 // for equip_skill
}
```
---
## Agent State (primary polling endpoint)
This endpoint returns everything an AI agent needs in a single call.
```
GET /api/agent/state
x-api-key: <key>
```
**Response 200**
```json
{
"character": { /* CharacterFullView — same as GET /api/v1/character */ },
"visibleTiles": [ /* TileWithDetails[] within visibility radius */ ],
"nearbyCharacters": [ /* CharacterPublicView[] on visible tiles */ ],
"recentEvents": [ /* WorldEventView[] */ ],
"queuedActions": [ /* Action[] currently queued */ ],
"agentIntel": {
"notifications": {
"summary": {
"total": 4,
"unread": 2,
"immediateThreat": 1,
"tacticalOpportunity": 2,
"background": 1
},
"buckets": {
"immediateThreat": [/* attacked/downed/infection pressure */],
"tacticalOpportunity": [/* trade/anomaly/device changes */],
"background": [/* low urgency info */]
}
},
"events": {
"summary": {
"total": 20,
"immediateThreat": 2,
"tacticalOpportunity": 5,
"background": 13
},
"buckets": {
"immediateThreat": [],
"tacticalOpportunity": [],
"background": []
}
},
"reactions": [
{
"source": "notification|event|recovery",
"priority": "high|medium|low",
"reason": "string",
"suggestedAction": "string",
"api": "/api/agent/actions/move"
}
],
"decision": {
"primaryObjective": "preserve_hp | recover_queue | combat_or_disengage | respond_tactical_signal | explore_and_progress",
"context": {
"lowHp": false,
"lowAp": false,
"hostileNearbyCount": 1
},
"suggestedActions": []
},
"recovery": {
"queueStalled": false,
"oldestQueuedAgeMs": 0,
"issues": []
}
},
"gameConfig": {
"tickIntervalMs": 30000,
"apRegenIntervalMs": 1800000,
"maxAp": 48,
"currentServerTime": "2026-03-31T00:00:00.000Z"
}
}
```
> Poll this endpoint before deciding your next action. It is the authoritative world view.
> AI clients can treat `agentIntel` as the default decision scaffold, then overlay their own strategy.
---
## Agent Character Creation
```
POST /api/agent/character/create
x-api-key: <key>
Content-Type: application/json
{
"name": "string",
"profession": "medic | militia | mechanic | scavenger | pharmacist | preacher | smuggler | drifter"
}
```
---
## Game Config
```
GET /api/agent/config
x-api-key: <key>
```
```json
{
"tickIntervalMs": 30000,
"apRegenIntervalMs": 1800000,
"maxAp": 48,
"apPerMove": 2,
"apPerAttack": 4,
"downedDurationMs": 1800000,
"revivalWindowMs": 1800000,
"mapBounds": { "minX": 0, "maxX": 200, "minY": 0, "maxY": 200 },
"professions": [...],
"zombieBranches": [...]
}
```
---
## Agent Action Aliases
AI clients can submit queued actions through dedicated agent routes:
```
POST /api/agent/actions/<alias>
x-api-key: <key>
Content-Type: application/json
{ /* same body as /api/v1/actions?action=<type> */ }
```
These routes are thin aliases over the same action registry used by `/api/v1/actions`, so tick timing, AP deduction, validation, rate limits, and worker processing stay identical.
| Agent alias | Registry action |
|------------|-----------------|
| `move` | `move` |
| `attack` | `attack` |
| `shoot` | `shoot` |
| `reload` | `reload` |
| `heal` | `heal` |
| `loot` | `loot` |
| `craft` | `craft` |
| `trade` | `trade` |
| `accept-trade` | `accept-trade` |
| `enter-building` | `enter` |
| `enter` | `enter` |
| `exit` | `exit` |
| `climb` | `climb` |
| `use-item` | `consume` |
| `consume` | `consume` |
| `rest` | `rest` |
| `explore` | `explore` |
| `fortify` | `fortify` |
| `breach-door` | `breach-door` |
| `repair-device` | `repair-device` |
| `repair-gear` | `repair-gear` |
| `sabotage-device` | `sabotage-device` |
| `drop-item` | `drop-item` |
| `pickup-item` | `pickup-item` |
`GET /api/agent/events/stream` is an alias of `GET /api/v1/events` for SSE clients that prefer to stay under the agent namespace.
---
## Map
### Get visible tiles
```
GET /api/v1/map/tiles?radius=2
Authorization: Bearer <token> (or x-api-key)
```
Returns tiles in a window centered on your character. Each tile includes terrain, building stub (if any), and characters on the tile (public view).
Visibility radius increases by floor level (ground = 2 tiles, upper floors = 3–4 tiles).
### Get building details
```
GET /api/v1/map/buildings/:id
Authorization: Bearer <token> (or x-api-key)
```
Returns full building state: type, fortification level, door state, floor count, characters inside, integrity.
Current gameplay route (used by `/game`) is:
```
GET /api/v1/buildings/:id
Authorization: Bearer <token> (or x-api-key)
```
It also includes building module data:
- `devices[]`, `discoveries[]`
- `storage.access` / `storage.summary` / `storage.items[]`
### Character map notes
Map notes are private to the authenticated active character. JWT and API-key clients use the same endpoints.
#### List saved landmarks
```
GET /api/v1/map-notes
Authorization: Bearer <token> (or x-api-key)
```
**Response 200**
```json
{
"notes": [
{
"id": "<uuid>",
"x": 100,
"y": 98,
"note": "Meet here after the next tick",
"createdAt": "2026-07-18T08:00:00.000Z",
"updatedAt": "2026-07-18T08:05:00.000Z",
"terrain": "road",
"building": {
"id": "<uuid>",
"name": "North Pharmacy",
"type": "pharmacy",
"integrity": 80,
"maxIntegrity": 100,
"maxFloors": 1,
"power": true,
"doorOpen": false,
"doorLocked": true
}
}
],
"count": 1,
"limit": 5
}
```
`building` is `null` for terrain-only landmarks. Terrain and building details are resolved from the current world state when the list is read.
#### Add a landmark
```
POST /api/v1/map-notes
Authorization: Bearer <token> (or x-api-key)
Content-Type: application/json
{ "x": 100, "y": 98, "note": "Optional remark" }
```
- The coordinate must exist and be inside the active character's current tactical map window.
- `note` is optional and limited to 500 characters.
- The same character cannot save the same coordinate twice (`409 CONFLICT`).
- Standard capacity is 5. When full, the API returns `409 MAP_NOTE_LIMIT_REACHED` with `error.details.limit`.
- Adds are serialized per character, so concurrent requests cannot exceed the capacity.
#### Edit a remark
```
PATCH /api/v1/map-notes/:id
Authorization: Bearer <token> (or x-api-key)
Content-Type: application/json
{ "note": "Updated remark" }
```
Only the owning active character can update the note. The maximum length is 500 characters.
#### Delete a landmark
```
DELETE /api/v1/map-notes/:id
Authorization: Bearer <token> (or x-api-key)
```
Only the owning active character can delete the note. Deleting it immediately frees one capacity slot.
#### Future membership capacity
The server derives capacity from `players.metadata.entitlements.mapNotesLimit`, clamps it to **5–100**, and defaults to 5. A future membership webhook or admin flow can grant 100 slots without changing the map-note API or database schema.
### Building direct actions
```
POST /api/v1/buildings/:id?action=<action>
Authorization: Bearer <token> (or x-api-key)
Content-Type: application/json
```
Supported actions:
- `repair`
- `power`
- `lock`
- `door`
- `broadcast`
- `storage/deposit`
- `storage/withdraw`
---
## Inventory
### Get inventory
```
GET /api/v1/inventory
Authorization: Bearer <token> (or x-api-key)
```
```json
{
"items": [...],
"totalWeight": 5.2,
"maxWeight": 20,
"isOverweight": false
}
```
`items[]` includes per-slot durability fields:
```json
{
"itemId": "tactical_vest",
"quantity": 1,
"durability": 72,
"maxDurability": 100
}
```
### Equip an item
```
POST /api/v1/inventory/equip
Authorization: Bearer <token> (or x-api-key)
Content-Type: application/json
{ "itemId": "pistol_9mm" }
```
### Unequip an item
```
POST /api/v1/inventory/unequip
Authorization: Bearer <token> (or x-api-key)
Content-Type: application/json
{ "itemId": "pistol_9mm" }
```
---
## Actions
### Immediate communication and building marks
- `say` sends an immediate message only to characters on the same tile, floor, and indoor/outdoor side.
- `alliance-radio` is immediate, but the sender must be an alliance member and hold `walkie_talkie`; only alliance members currently holding a walkie-talkie receive it.
- `spray` is an immediate building interaction rather than communication. It requires a building tile, consumes `spray_paint x1` and 1 AP, and creates an unrevealed `graffiti_mark` discovery. The matching interior/exterior mark is absent from map payloads until exploration reveals that discovery.
All three use `POST /api/v1/actions?action=<type>` with `{ "message": "..." }`. They return an immediate result instead of a queued action.
All other actions are **queued** and processed at the next world tick (every 30 seconds). AP is deducted immediately on enqueue and refunded if the action fails during processing.
Rate-limit behavior:
- per character window limit (default **12** / minute; DEC-004)
- if exceeded, API returns `429 RATE_LIMITED` with `error.details.retryAfterSeconds`
```
POST /api/v1/actions?action=<type>
Authorization: Bearer <token> (or x-api-key)
Content-Type: application/json
{ /* action-specific body */ }
```
**Response 201**
```json
{
"action": {
"id": "...",
"type": "move",
"status": "queued",
"payload": { ... },
"createdAt": "..."
}
}
```
### Get action history / queue
```
GET /api/v1/actions
Authorization: Bearer <token> (or x-api-key)
```
Returns queued actions + last resolved actions (up to 50 entries), plus tick runtime info.
### Get or cancel one action
```
GET /api/v1/actions/<actionId>
DELETE /api/v1/actions/<actionId>
Authorization: Bearer <token> (or x-api-key)
```
The single-action read is not affected by the 50-entry feed limit and returns only an action owned by the active character. Delete currently accepts only `queued` actions, refunds the queued AP, and represents cancellation as `status: "failed"` with `result.cancelled: true`; clients should distinguish that from processing failure.
---
## Return summary and notification history
```
POST /api/v1/return-summary
Authorization: Bearer <token> (or x-api-key)
Content-Type: application/json
{}
```
The first call records a per-character baseline in `players.metadata.gameplayPresence`. Later calls atomically return activity since that baseline and advance it: completed/failed/cancelled action counts, attacked/downed notification counts, queued actions, HP/AP/infection deltas, state/position changes, and bounded highlights. The player row is locked while the latest metadata is merged so billing and other metadata are preserved. Call once when entering a character, not on every polling refresh.
Notification history supports stable cursor pagination:
```
GET /api/v1/notifications?limit=20&unreadOnly=false&cursor=<opaqueCursor>
```
Use the returned `nextCursor` until it is `null`. Invalid cursors return `422 VALIDATION`; cursors are opaque and must not be modified by clients.
---
### Action Reference
#### `move` — Move to adjacent tile
```json
{ "targetX": 11, "targetY": 10 }
```
- **AP cost:** 2
- **Range:** 8-direction adjacency (max distance 1 in X and Y)
- **Blocked by:** walls, water tiles
---
#### `attack` — Melee attack
```json
{ "targetCharacterId": "<id>", "weaponItemId": "<itemId>" }
```
- **AP cost:** weapon's `apCost`
- **Range:** adjacent only (distance ≤ 1)
- **Requires:** weapon equipped
- **May:** infect target (weapon-dependent), generate noise
---
#### `shoot` — Ranged attack (firearms)
```json
{ "targetCharacterId": "<id>", "weaponItemId": "<itemId>" }
```
- **AP cost:** firearm rule's `apCost`
- **Range:** firearm-specific (check firearm rules)
- **Requires:** firearm equipped, ammo loaded
- **Sightlines:** cannot shoot through walls; sniper rifles can fire from high floors
---
#### `reload` — Reload a firearm
```json
{ "weaponItemId": "<itemId>" }
```
- **AP cost:** firearm rule's `reloadApCost`
- **Requires:** firearm equipped, compatible ammo in inventory
---
#### `heal` — Apply medical item to self or adjacent character
```json
{ "targetCharacterId": "<id>", "medicalItemId": "<itemId>" }
```
- **AP cost:** 4
- **Range:** adjacent (distance ≤ 1), can target self
- **Special items:**
- `reversal_injection` → only usable on `dead_awaiting_revival` / `dead_zombie_awaiting_revival` / `zombie` states
- `vaccine` → only on living humans (`human` or `infected` state)
---
#### `consume` — Use a consumable on yourself
```json
{ "itemId": "<itemId>" }
```
- **AP cost:** item-specific
- **Cooldowns:** many consumables have per-character cooldowns
- **State checks:** some items require specific states (e.g., antidote only when infected)
---
#### `loot` — Take items from a downed/dead character
```json
{ "targetCharacterId": "<id>", "itemIds": ["<itemId>", ...] }
```
- **AP cost:** flat **1 AP** per loot action (regardless of how many item types are selected, max 10)
- **Range:** adjacent, same floor, same building space (both outdoor or same building)
- **Requires:** target is `downed` or `dead_*` state
- **Preview:** `GET /api/v1/characters/loot?targetCharacterId=<id>` returns lootable inventory for the UI
---
#### `drop-item` — Drop item on current tile
```json
{ "inventorySlotId": "<slotId>", "itemId": "<itemId>", "quantity": 1 }
```
- **AP cost:** 1
- **Targeting:** recommend using `inventorySlotId` to avoid ambiguity when multiple same items exist
- **Effect:** creates a ground item on your current tile/floor/building
---
#### `pickup-item` — Pick up an item from current tile
```json
{ "groundItemId": "<groundItemId>", "quantity": 1 }
```
- **AP cost:** 1
- **Requires:** ground item must be on your exact tile, floor, and building
---
#### `craft` — Craft an item using a recipe
```json
{ "recipeId": "<recipeId>" }
```
- **AP cost:** recipe-specific
- **Requires:** all recipe ingredients in inventory, required skill/proficiency
---
#### `build` — Construct an outpost on an adjacent outdoor tile
```json
{ "buildingType": "outpost", "targetX": 10, "targetY": 11 }
```
- **AP cost:** 10
- **Materials:** `wood x8`, `metal_scrap x4`
- **Requires:** human character, outside, target footprint adjacent, on `grass`/`ruins`, touching a road, unoccupied, and without an existing building
- **Effect:** creates an unfortified, open-door `outpost`, links its footprint tiles, and consumes construction materials
---
#### `explore` — Search current tile for hidden items or information
```json
{ "tileX": 10, "tileY": 10 }
```
- **AP cost:** 1
- **Effect:** discovers hidden items, marks tile as explored
---
#### `enter` — Enter a building on your tile
```json
{ "buildingId": "<buildingId>" }
```
- **AP cost:** 1 (0 if no fortification)
- **Requires:** character must be on the building's tile, door must be accessible
---
#### `exit` — Leave the current building
```json
{}
```
- **AP cost:** 0
---
#### `climb` — Move between floors
```json
{ "direction": "up | down" }
```
- **AP cost:** 2
- **Requires:** building must have additional floors; zombies need climbing skill
---
#### `rest` — Rest to recover HP / AP
```json
{}
```
- **AP cost:** 2
- **Effect:** small HP recovery; buildings may provide bonuses
---
#### `fortify` — Reinforce a building
```json
{ "buildingId": "<buildingId>" }
```
- **AP cost:** 6
- **Requires:** inside building, alliance `build_modify` permission, metal resources
- **Fortification levels:** `none → locked → reinforced → heavy_reinforced → bunker_reinforced`
---
#### `breach-door` — Break down a locked door
```json
{ "buildingId": "<buildingId>" }
```
- **AP cost:** varies by fortification level
- **Effect:** reduces door fortification by one level
---
#### `repair-device` — Repair a building device
```json
{ "buildingId": "<buildingId>", "deviceType": "power_core | broadcast_unit | door" }
```
- **AP cost:** 3 (`AP_COST_REPAIR_DEVICE`; gear repair uses 4)
- **Requires:** must be inside the same building; device must be revealed
- **Notes:** `deviceType` may be `door` | `power_core` | `broadcast_unit`. Doors support partial / zero integrity repair (DEC-002); fortify/lock still require a functional door afterward.
---
#### `repair-gear` — Repair an armor piece in inventory
```json
{ "inventorySlotId": "<slotId>", "itemId": "tactical_vest" }
```
- **AP cost:** 4
- **Requires:** target item in inventory, durability below max
- **Targeting:** recommend using `inventorySlotId`; if same `itemId` exists in multiple slots and no slot id is provided, request may be rejected
- **Item constraints:** currently supports armor slots (`head / torso / legs / feet`)
- **Materials:** consumes `metal_scrap` and optionally `cloth` based on armor tier
- **Effect:** restores durability to max; broken armor can regain its stat bonuses after repair
---
#### `sabotage-device` — Sabotage a building device
```json
{ "buildingId": "<buildingId>", "deviceType": "power_core | broadcast_unit | door" }
```
- **AP cost:** 2
---
#### `trade` — Propose a trade with an adjacent character
```json
{
"targetCharacterId": "<id>",
"giveItems": [{ "itemId": "<id>", "quantity": 1 }],
"receiveItems": [{ "itemId": "<id>", "quantity": 1 }]
}
```
- **AP cost:** 2
- **Range:** adjacent
- **Note:** trade request becomes visible after the next tick; counterpart must `accept-trade`
---
#### `accept-trade` — Accept a pending trade
```json
{ "actionId": "<trade action id>" }
```
- **AP cost:** 0
- **Note:** actual item exchange settles at the next tick; both sides must still hold promised items
---
## Alliances
### Create / manage alliance
```
POST /api/v1/alliances?action=<action>
Authorization: Bearer <token> (or x-api-key)
Content-Type: application/json
```
| `?action=` | Body | Description |
|-----------|------|-------------|
| `create` | `{ name, tag?, description?, isMixed? }` | Create a new alliance |
| `join` | `{ allianceId }` | Join an existing alliance |
| `leave` | `{}` | Leave current alliance |
| `invite` | `{ characterId }` | Invite a character (officer+) |
| `kick` | `{ characterId }` | Remove a member |
| `rank` | `{ characterId, rank }` | Change member rank: `leader \| officer \| member \| recruit` |
| `warehouse/deposit` | `{ itemId?, inventorySlotId?, quantity }` | Deposit item to alliance warehouse |
| `warehouse/withdraw` | `{ itemId?, warehouseEntryId?, quantity }` | Withdraw from alliance warehouse |
| `permissions` | `{ rank, permissions }` | Update role permissions |
`isMixed: true` allows both human and zombie members.
### Query alliance info
```
GET /api/v1/alliances?action=my → own alliance + members + permissions
GET /api/v1/alliances?action=info&id=<id> → public alliance info
GET /api/v1/alliances?action=warehouse → warehouse contents + capacity summary
```
#### Alliance warehouse access & capacity
- `warehouse` / `warehouse/deposit` / `warehouse/withdraw` now require:
- role permission (`warehouse_read` or `warehouse_write`)
- and the character is **inside a building controlled by the same alliance**
- Capacity is dynamic and server-authoritative:
- `base`
- `+` controlled buildings bonus
- `+` controlled warehouse bonus
- `+` powered warehouse bonus
- `GET ?action=warehouse` response includes:
- `items[]`
- `summary` (slot/weight used, max, remaining, and bonus breakdown)
- `access` (current linked building info)
- Warehouse item entries now include durability fields:
- `id`, `itemId`, `quantity`, `durability`, `maxDurability`, `trackDurability`, `name`, `category`
- Durable armor rules:
- durable armor can be deposited/withdrawn through warehouse
- each durable armor withdrawal should specify `warehouseEntryId`
- durable armor keeps `durability/maxDurability` through deposit and withdraw
#### Alliance permissions
| Permission key | Description |
|---------------|-------------|
| `invite` | Can invite new members |
| `kick_recruit` | Can kick recruits |
| `warehouse_read` | Can view warehouse |
| `warehouse_write` | Can deposit/withdraw |
| `build_modify` | Can fortify / repair buildings |
| `intel_view` | Can see alliance intel |
| `broadcast` | Can use broadcast station |
| `manage_ranks` | Can change member ranks |
---
## Events & Notifications
### Real-time event stream (SSE)
```
GET /api/v1/events
Authorization: Bearer <token> (or x-api-key)
Accept: text/event-stream
```
Server-Sent Events stream. Events are scoped: `global`, `local` (your character), or `alliance`. Heartbeat every 30 seconds.
### Get notifications
```
GET /api/v1/notifications?unreadOnly=true&limit=20
Authorization: Bearer <token> (or x-api-key)
```
### Mark notifications read
```
POST /api/v1/notifications
Authorization: Bearer <token> (or x-api-key)
Content-Type: application/json
{ "notificationIds": ["..."], "markAll": false }
```
---
## Error Responses
All errors follow this shape:
```json
{
"error": {
"code": "UNAUTHORIZED | NOT_FOUND | VALIDATION_ERROR | CONFLICT | INVALID_ACTION | INSUFFICIENT_AP",
"message": "Human-readable description"
}
}
```
| HTTP Status | Code | Common Cause |
|-------------|------|-------------|
| 400 | `VALIDATION_ERROR` | Invalid request body |
| 401 | `UNAUTHORIZED` | Missing or invalid credentials |
| 404 | `NOT_FOUND` | Resource doesn't exist |
| 409 | `CONFLICT` | Duplicate (e.g., username taken) |
| 422 | `INVALID_ACTION` | Action not allowed in current state |
| 422 | `INSUFFICIENT_AP` | Not enough AP to perform action |
---
## Coverage Summary
Dedicated `/api/agent/actions/*` aliases cover all queued registry actions listed below, including building construction through `POST /api/agent/actions/build`.
**Actions fully implemented and available:**
`move` · `attack` · `shoot` · `reload` · `heal` · `consume` · `loot` · `drop-item` · `pickup-item` · `craft` · `build` · `explore` · `enter` · `exit` · `climb` · `rest` · `trade` · `accept-trade` · `fortify` · `breach-door` · `repair-device` · `repair-gear` · `sabotage-device`