Skip to main content
A world is a programmable graph: nodes are states (what a scene is) and edges are events (what the player can do to move between them). Generating a world gives you a complete, valid graph; the Graph-Editing API lets you keep programming it — add a room, wire a new exit, retarget the entrance, or push a whole batch of changes at once — all over the same /v1 API-key model you already use. There are two tiers for changing a graph:
  • The deterministic tier (available now). Precise, scripted control: state and event CRUD, a batch op vocabulary, and the kernel validate/lint gate. This is what third-party editors and your own tooling build on. Everything on this page is in this tier.
  • The agent tier. A natural-language endpoint — POST /v1/worlds/{id}/edit — where you describe the change in prose and the kernel agent authors it for you. It returns the validated result only, never the authoring brain. See Editing with natural language. Both tiers funnel through the exact same validation gate described below, so a world edited either way behaves identically in the editor and at runtime.
Reading the graph needs a publishable (pk_) or secret (sk_) key with worlds:read. Every write needs a secret key with worlds:write. Never ship a secret key to a browser.

The validation gate

This is the load-bearing rule of the whole surface: every write persists only after passing the kernel grammar and the full doctrine lint suite, fail-closed. If a mutation would produce an invalid world, the API rejects it with 422 and a list of diagnostics — and nothing is persisted. There is no weaker path; the kernel is the sole authority that can bless a change, and the API is only a way to propose one. A diagnostic looks like this:
{
  "lint": "dangling-ref",
  "severity": "error",
  "path": "event[Step outside]",
  "message": "event 'Step outside' points at unknown state 'ghost_room'"
}
  • Structural lints (dangling-ref, transition-needs-to, kernel-cycle, the slot-* family, …) are always fatal errors and block the write.
  • Doctrine and budget lints (negation, whiteout, lethal-override, budget, …) are advisory — they come back as warning/info and don’t block a save, so you can fix them iteratively. A common one: prompt prose must describe pixels, not authorial intent (“a dim stone corridor,” not “the player feels trapped”).
When a write succeeds, the response carries the new world, any advisory diagnostics, and a fresh rev (see concurrency).

Read the graph

GET /v1/worlds/{id}/scene returns the whole graph; /states and /events return the node map and edge list on their own. States are addressed by their id; events are always addressed by their unique name, never by index.
curl 'https://api.alakazam.gg/v1/worlds/WORLD_ID/scene' \
  -H 'Authorization: Bearer pk_live_…'
Every read returns the current rev both in the body and as the ETag response header — hold onto it for your next write.

Add a state and wire an edge

Adding a node, then an edge into it, is the bread-and-butter of programming a graph. Omit id on a state to have one auto-assigned; the response tells you which id (or event name) was created.
# 1. Add a node.
curl -X POST 'https://api.alakazam.gg/v1/worlds/WORLD_ID/states' \
  -H 'Authorization: Bearer sk_live_…' \
  -H 'Content-Type: application/json' \
  -d '{
    "id": "cellar",
    "base": "a low stone cellar, one guttering candle, damp flagstones underfoot"
  }'

# 2. Wire a transition into it from an existing state.
curl -X POST 'https://api.alakazam.gg/v1/worlds/WORLD_ID/events' \
  -H 'Authorization: Bearer sk_live_…' \
  -H 'Content-Type: application/json' \
  -d '{
    "kind": "transition",
    "from": "hallway",
    "to": "cellar",
    "name": "Descend the stairs"
  }'
A transition must carry a to; an override (a self-loop that re-renders the current state without moving) must not. Break either rule — or reference a state that doesn’t exist — and the op can’t be applied: the write comes back 400 with a GraphOpError, caught before the kernel ever runs. A 422 (GraphValidationError) is the other failure mode: the ops applied cleanly, but the resulting graph failed the kernel grammar or doctrine lint, so its blocking diagnostics (like dangling-ref or kernel-cycle) come back in the body and nothing is persisted. Patch a node or edge with PATCH (send the partial fields, or wrap them in { "patch": { … } }); remove one with DELETE. Deleting a state that is the entrance, or that any event still references, is rejected with 400 — detach those edges first. Retarget the entry point with PATCH /v1/worlds/{id}/entrance:
curl -X PATCH 'https://api.alakazam.gg/v1/worlds/WORLD_ID/entrance' \
  -H 'Authorization: Bearer sk_live_…' \
  -H 'Content-Type: application/json' \
  -d '{ "state": "cellar" }'

Batch ops

POST /v1/worlds/{id}/ops applies an ordered batch of operations atomically — it backs nearly every mutation the visual editor makes. The op vocabulary is a stable, curated surface (add_state, update_state, delete_state, add_event, update_event, delete_event, set_entrance, add_variant, remove_variant). The resulting world is validated once, as a whole: if the batch leaves the world invalid, the entire batch is rejected 422 and nothing persists.
curl -X POST 'https://api.alakazam.gg/v1/worlds/WORLD_ID/ops' \
  -H 'Authorization: Bearer sk_live_…' \
  -H 'Content-Type: application/json' \
  -d '{
    "ops": [
      { "op": "add_state", "id": "vault", "base": "a steel vault, fluorescent hum, one heavy door" },
      { "op": "add_event", "kind": "transition", "from": "cellar", "to": "vault", "name": "Force the door" },
      { "op": "set_entrance", "state": "cellar" }
    ]
  }'
Individual ops that can’t be applied (say, deleting a state that doesn’t exist) are collected non-fatally in applyErrors — mirroring the editor’s behavior — so one bad op in a batch doesn’t sink the rest. A 422, by contrast, means the final world failed the kernel gate as a whole.

Validate and lint without saving

Before you commit a change — or to check a world you’ve assembled client-side — run it through the same gate without persisting.
  • POST /v1/worlds/{id}/validate runs the strict gate. It returns 200 with the resolved world when clean, or 422 with diagnostics when not.
  • POST /v1/worlds/{id}/lint is advisory — it never returns 4xx on doctrine hits. It reports every finding plus counts by severity and the promptBudget (1900) your assembled prompts must stay under.
Both accept an optional inline world (or data) in the body to check a candidate world; omit it to check the stored one.
curl -X POST 'https://api.alakazam.gg/v1/worlds/WORLD_ID/lint' \
  -H 'Authorization: Bearer pk_live_…'

Optimistic concurrency

The editor, your scripts, and (soon) the agent can all edit one world. To keep a stale write from clobbering a fresh one, every write is guarded by a revision token, rev. Each read and each successful write returns the current rev — in the body and as the ETag header. Pass it back on your next write, either as the If-Match request header or as an expectedRev body field. If it no longer matches the stored revision, the write is rejected with 409 and you should reload and retry.
# Read returns ETag: "2026-06-28T12:00:00.000Z"
curl -X PATCH 'https://api.alakazam.gg/v1/worlds/WORLD_ID/states/cellar' \
  -H 'Authorization: Bearer sk_live_…' \
  -H 'Content-Type: application/json' \
  -H 'If-Match: 2026-06-28T12:00:00.000Z' \
  -d '{ "base": "a low stone cellar, the candle guttering lower now" }'

Status codes

StatusMeaning
200Write applied. The body carries the new world, diagnostics, and rev.
400An op couldn’t be applied — a missing reference, a duplicate event name, deleting a referenced or entrance state. Caught before validation.
404The world (or addressed state/event) doesn’t exist, or isn’t yours.
409The rev you asserted is stale — another writer got there first. Reload and retry.
422The result failed the kernel gate. The body carries the blocking diagnostics; nothing was persisted.