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.

ModeHeaderWhen to use
JWT BearerAuthorization: Bearer <token>Human players, web UI
API Keyx-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", "apPotionBalance": 0, "shop": [ { "sku": "ap_potion_5", "itemId": "ap_potion", "grantQuantity": 5, "priceCents": 100, "currency": "USD", "purchasable": true } ] } } ``

apPotionBalance is unallocated paid AP potion stock on the account (DEC-014). Paid Creem packs credit this balance; allocate to a character bag via POST /api/v1/billing/shop/allocate before 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" } ``

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.

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_BALANCE with apPotionBalance.
  • 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/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, "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 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

ProfessionPlaystyle Focus
medicHealing, infection management
militiaCombat, firearms
mechanicCrafting, building repair
scavengerLooting, carry weight
pharmacistConsumables, infection mitigation
preacherAlliance support
smugglerTrade, stealth
drifterBalanced 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 aliasRegistry action
movemove
attackattack
shootshoot
reloadreload
healheal
lootloot
craftcraft
tradetrade
accept-tradeaccept-trade
enter-buildingenter
enterenter
exitexit
climbclimb
use-itemconsume
consumeconsume
restrest
exploreexplore
scoutscout
fortifyfortify
breach-doorbreach-door
repair-devicerepair-device
repair-gearrepair-gear
sabotage-devicesabotage-device
drop-itemdrop-item
pickup-itempickup-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
  • rename
  • storage/deposit
  • storage/withdraw

Immediate AP costs:

ActionAP
repair4
power4
lock2
door1
broadcast6
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.

ActionBodyRules
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 storageAlliance warehouse
EndpointPOST /api/v1/buildings/:id?action=storage/deposit|storage/withdrawPOST /api/v1/alliances?action=warehouse/deposit|warehouse/withdraw
ScopeOne building only; not shared across buildingsOne shared pool per alliance, reachable from any building the alliance controls
AccessCharacter must be inside that building; warehouse_read / warehouse_write apply only when the building is alliance-controlledwarehouse_read / warehouse_write plus being inside a building controlled by the same alliance
Capacitygame_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

  • 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 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 (molotov is 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 not downed
  • antidote — infection −30; only on living human / infected
  • vaccine — infection → 0, grants 12h infectionSuppressedUntil, and infectedhuman; only on living human / infected
  • reversal_injection — start 6h revertinghuman; only on:
  • zombie
  • dead_awaiting_revival
  • dead_zombie_awaiting_revival
  • Reversal notes:
  • Not usable via consume / use_item — must be heal
  • 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 event reversion_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 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

#### demolish — Demolish a controlled alliance outpost

``json { "buildingId": "<buildingId>" } ``

  • AP cost: 16
  • Requires: human; inside the outpost alone; outpost controllingAllianceId matches your alliance; build_modify (leader default true; officer/member false unless granted)
  • Scope: type = outpost only (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×2 to inventory; actor ends outdoors (buildingId cleared)

#### 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 lockpicking skill 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: 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=BodyDescription
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/withdraw now require:
  • role permission (warehouse_read or warehouse_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/* on POST /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 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 keyDescription
inviteCan invite new members
kick_recruitCan kick recruits
warehouse_readCan view warehouse
warehouse_writeCan deposit/withdraw
build_modifyCan fortify / repair buildings; lock/unlock doors and close doors on alliance-controlled buildings (`POST /buildings/:id?action=lockdoor`)
intel_viewCan see alliance intel
broadcastCan use broadcast station
manage_ranksCan 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 StatusCodeCommon Cause
400VALIDATION_ERRORInvalid request body
401UNAUTHORIZEDMissing or invalid credentials
404NOT_FOUNDResource doesn't exist
409CONFLICTDuplicate (e.g., username taken)
422INVALID_ACTIONAction not allowed in current state
422INSUFFICIENT_APNot 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