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 indexGET /docs/player— player guideGET /docs/ai— AI guideGET /docs/agent-sdk— AI SDK and replay guideGET /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 已綁定帳號,重設指引會寄到你的信箱。" } ``
debugTokenis only returned whenEXPOSE_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", "apPotionBalance": 0, "shop": [ { "sku": "ap_potion_5", "itemId": "ap_potion", "grantQuantity": 5, "priceCents": 100, "currency": "USD", "purchasable": true } ] } } ``
apPotionBalanceis unallocated paid AP potion stock on the account (DEC-014). Paid Creem packs credit this balance; allocate to a character bag viaPOST /api/v1/billing/shop/allocatebefore in-game drink. Tutorial / weekly-board free potions still go straight to character inventory.
``` PATCH /api/v1/profile Authorization: Bearer <token> Content-Type: application/json
{ "email": "new@example.com" } ```
Response 200 ``json { "message": "驗證信已寄出,請至信箱完成綁定。", "pendingEmail": "new@example.com" } ``
debugTokenis only returned whenEXPOSE_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.
Shop checkout and allocate (DEC-014)
Paid AP potion packs do not require an active character at checkout. Fulfillment credits players.ap_potion_balance.
``` POST /api/v1/billing/shop/checkout Authorization: Bearer <token> Content-Type: application/json
{ "sku": "ap_potion_5" } ```
Response 201 ``json { "checkoutUrl": "https://…", "checkoutId": "…", "sku": "ap_potion_5", "itemId": "ap_potion", "grantQuantity": 5, "environment": "test" } ``
Legacy body { "sku": "ap_potion" } maps to the 5-pack. Missing Creem env → 503 CREEM_NOT_CONFIGURED / SHOP_NOT_CONFIGURED.
``` POST /api/v1/billing/shop/allocate Authorization: Bearer <token> Content-Type: application/json
{ "characterId": "<uuid>", "quantity": 2 } ```
Response 200 ``json { "characterId": "…", "characterName": "…", "quantity": 2, "apPotionBalance": 3, "itemId": "ap_potion" } ``
- Character must belong to the authenticated player → otherwise
403 CHARACTER_NOT_FOUND. - Insufficient account stock →
422 INSUFFICIENT_AP_POTION_BALANCEwithapPotionBalance. - CAS debit under row lock; concurrent allocates cannot overdraw.
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/characterremains 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, "firstCharacterId": "uuid|null", "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 when the account still has an unfinished first-character tutorial. Eligibility is tied to onboarding.firstCharacterId (stamped on first create), not current characterCount. Creating a second character must not permanently kill an unfinished tutorial. onboarding.firstCharacterId is the character the tutorial/rewards apply to. Stage claims require that character to be active. onboarding.rewardedStages lists soft stages that already granted a stage reward. Current tutorial: map (move) and actions (explore). Each grants ap_potion ×1. DEC-015: stage claim proof accepts action status queued / processing / completed (enqueue-to-claim; players need not wait for Tick success). Later failed / cancel does not claw back the potion. onboarding.contextualHintsSeen lists finished post-tutorial tips (enterBuilding / useApPotion).
``` 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. Skipping marks all contextual hints as seen so they will not appear later.
``` POST /api/v1/onboarding/tutorial/claim Authorization: Bearer <token> (or x-api-key) Content-Type: application/json
{ "stage": "map | actions" } ```
Response 200 — grants ap_potion ×1 once while the tutorial is pending and the eligible first character is active. Server proof: map requires a move action; actions requires explore or enter_building. Idempotent via alreadyClaimed. Response includes itemId and quantity.
``` POST /api/v1/onboarding/hint Authorization: Bearer <token> (or x-api-key) Content-Type: application/json
{ "hint": "enterBuilding | useApPotion" } ```
Response 200 — marks one contextual tip as seen after the base tutorial is completed. Requires tutorial completed (not available while pending). Idempotent if already seen.
``` 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 CONFLICTwith messageCharacter 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/processingactions, API returns422 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
agentIntelas 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 |
scout | scout |
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.
noteis 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_REACHEDwitherror.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:
repairpowerlockdoorbroadcastrenamestorage/depositstorage/withdraw
Immediate AP costs:
| Action | AP |
|---|---|
repair | 4 |
power | 4 |
lock | 2 |
door | 1 |
broadcast | 6 |
rename / storage/* | 0 |
#### Door lock / open (alliance-friendly path)
Do not treat every locked door as a breach-door target. Friendly alliance doors should be unlocked with the building API.
| Action | Body | Rules | ||
|---|---|---|---|---|
lock | {} (toggles) | 2 AP, immediate. Character must be inside or on the building footprint. If controllingAllianceId is set, requires membership + `build_modify` (leader always has full rights). If uncontrolled (controllingAllianceId null), anyone may operate. Building fortification must not be none. Toggling to locked also forces the door closed. | ||
door | `{ "mode": "open" \ | "close" \ | "toggle" }` | 1 AP, immediate. Same location rule. Zombies cannot operate doors. Open is refused while doorLocked is true — unlock first. Close requires build_modify when the building is alliance-controlled (same permission helper as lock). |
Hostile locked doors (not your alliance): use queued pick-lock (needs active lockpicking skill) or breach-door (damages door/fortification). Never breach a door your alliance controls if you can unlock it.
See player guide §10.2 and Skill contract “Door decision tree”.
#### Building storage vs alliance warehouse
storage/* here is building storage (building_storage), which is a separate store from the alliance warehouse (alliance_warehouse):
| Building storage | Alliance warehouse | |
|---|---|---|
| Endpoint | POST /api/v1/buildings/:id?action=storage/deposit|storage/withdraw | POST /api/v1/alliances?action=warehouse/deposit|warehouse/withdraw |
| Scope | One building only; not shared across buildings | One shared pool per alliance, reachable from any building the alliance controls |
| Access | Character must be inside that building; warehouse_read / warehouse_write apply only when the building is alliance-controlled | warehouse_read / warehouse_write plus being inside a building controlled by the same alliance |
| Capacity | game_config keys building_storage_* (base + warehouse-type bonus + powered bonus) | Derived from controlled buildings (see alliance warehouse section) |
Depositing through storage/deposit leaves the items in that single building; they never appear in GET /api/v1/alliances?action=warehouse. Both stores keep durability-tracked gear as separate entries (1 per call, withdraw by entry id) and neither costs AP.
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
saysends an immediate message only to characters on the same tile, floor, and indoor/outdoor side.alliance-radiois immediate, but the sender must be an alliance member and holdwalkie_talkie; only alliance members currently holding a walkie-talkie receive it.sprayis an immediate building interaction rather than communication. It requires a building tile, consumesspray_paint x1and 1 AP, and creates an unrevealedgraffiti_markdiscovery. 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_LIMITEDwitherror.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 accepts only queued actions. Cancelling one action also cancels later queued actions for the same character (queue order: tickPriority, then createdAt), refunds AP for each, and marks them status: "cancelled" with result.cancelled: true. Response shape: { action, cascadeCancelled, apRefunded }. Clients should treat cascade rows as cancelled too; distinguish from processing failed.
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: leave-terrain cost — road 1, forest 3, grass/ruins 2; equipment may add AP
- 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
#### throw — Throw a throwable at a tile
``json { "itemId": "molotov", "targetX": 11, "targetY": 10 } ``
- AP cost: throwable weapon's
apCost(molotovis 5) - Range: 1 through the throwable weapon's range
- Effect: consumes one item, rolls hit, damages characters in the impact space, and may damage a door
- Requires: human faction and a throwable item in inventory
#### 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(intelligence may discount;minimum 1)
- Range: adjacent (Chebyshev distance ≤ 1), can target self
- Special items:
bandage/medkit— heal HP; rejected if target is already full HP and notdownedantidote— infection −30; only on livinghuman/infectedvaccine— infection → 0, grants 12hinfectionSuppressedUntil, andinfected→human; only on livinghuman/infectedreversal_injection— start 6hreverting→human; only on:zombiedead_awaiting_revivaldead_zombie_awaiting_revival- Reversal notes:
- Not usable via
consume/use_item— must beheal - On success: target
state=reverting,stateDeadline ≈ now+6h; item consumed at tick resolve - When deadline elapses (world tick):
human+faction=human+ infection 0 + HP ≈ 20% maxHp; world eventreversion_complete - Player-facing walkthrough:
docs/player-guide.md§11.5 - Web ActionMenu: Heal → pick medical item (including
reversal_injection) → pick compatible target
#### 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
downedordead_*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
inventorySlotIdto 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
#### demolish — Demolish a controlled alliance outpost
``json { "buildingId": "<buildingId>" } ``
- AP cost: 16
- Requires: human; inside the outpost alone; outpost
controllingAllianceIdmatches your alliance;build_modify(leader default true; officer/member false unless granted) - Scope:
type = outpostonly (player-built or map-seeded) when controlled by your alliance - Effect: deletes the building; dumps building storage and indoor ground items to outdoor ground at the actor tile; refunds
wood×4+metal_scrap×2to inventory; actor ends outdoors (buildingIdcleared)
#### 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
#### scout — Recon same or adjacent tile (intel only)
``json { "tileX": 10, "tileY": 10 } ``
- AP cost: 1
- Requires: target Chebyshev distance ≤ 1 from projected position (current tile or end of queued moves)
- Effect: returns heat / scent / dog-pressure summary and outdoor-search hints; does not reveal discoveries, mark
explored, consume outdoor explore refresh, grant loot, or run dog combat encounters - Aliases:
tileX/tileY,targetX/targetY, or omit for projected tile
#### enter — Enter a building on your tile
``json { "buildingId": "<buildingId>" } ``
- AP cost: 1
- Requires: character must be on the building's tile, door must be accessible
#### exit — Leave the current building
``json {} ``
- AP cost: 1
#### climb — Move between floors
``json { "direction": "up | down" } ``
- AP cost: 2
- Requires: character is inside the building, target floor exists, and hostile floor suppression does not block entry
#### rest — Rest to recover HP
``json {} ``
- AP cost: 2
- Effect: recovers
floor(maxHp × 10%); does not restore AP and has no building bonus - Requires: current HP is below max HP
#### fortify — Reinforce a building
``json { "buildingId": "<buildingId>" } ``
- AP cost: next tier costs 4 / 8 / 12 / 20 AP; active engineering may reduce it
- Requires: human faction, inside the same building, enough metal, and sufficient door integrity
- Fortification levels:
none → locked → reinforced → heavy_reinforced → bunker_reinforced
#### breach-door — Break down a locked door
``json { "buildingId": "<buildingId>" } ``
- AP cost: 2
- Effect: damages a locked door according to the equipped weapon's fortification damage; a destroyed door becomes passable
#### pick-lock — Quietly attempt to unlock a door
``json { "buildingId": "<buildingId>" } ``
- AP cost: 3
- Requires: active
lockpickingskill and a locked door on the current building tile or interior - Effect: success unlocks the door; failure leaves it locked and creates noise
#### 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:
deviceTypemay bedoor|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 sameitemIdexists 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_scrapand optionallyclothbased 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 | |||
set-emblem | { baseId, patternId, symbolId } | Leader locks alliance emblem once (allowlist layers; irreversible) |
isMixed: true allows both human and zombie members.
Emblem layers must be official catalog ids (GET /api/v1/alliances?action=emblems). Once set, set-emblem returns 409 and cannot be changed.
Query alliance info
`` GET /api/v1/alliances?action=my → own alliance + members + permissions (+ emblem) GET /api/v1/alliances?action=info&id=<id> → public alliance info (+ emblem) GET /api/v1/alliances?action=emblems → emblem catalog (bases / patterns / symbols) GET /api/v1/alliances?action=list → public directory (+ emblem, seats) GET /api/v1/alliances?action=warehouse → warehouse contents + capacity summary ``
alliance.emblem is null until locked, then: { baseId, patternId, symbolId, locked: true, layers: { base, pattern, symbol } }.
Directory (action=list) and heroes alliance-control board also include the same emblem field (or null).
#### Alliance warehouse access & capacity
warehouse/warehouse/deposit/warehouse/withdrawnow require:- role permission (
warehouse_readorwarehouse_write) - and the character is inside a building controlled by the same alliance
- This is the alliance-wide shared pool. It is not the same store as per-building
storage/*onPOST /api/v1/buildings/:id; items deposited through the building endpoint stay in that building and are never listed here. See "Building storage vs alliance warehouse". - Capacity is dynamic and server-authoritative:
base+controlled buildings bonus+controlled warehouse bonus+powered warehouse bonusGET ?action=warehouseresponse 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/maxDurabilitythrough 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; lock/unlock doors and close doors on alliance-controlled buildings (`POST /buildings/:id?action=lock | door`) |
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 · throw · reload · heal · consume · loot · drop-item · pickup-item · craft · build · explore · scout · enter · exit · climb · rest · trade · accept-trade · fortify · breach-door · pick-lock · repair-device · repair-gear · sabotage-device