/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. Most of 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. One canned agent move, Arcadeify, also lives on this page. 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 with422 and a
list of diagnostics, and nothing is persisted. There is no weaker path. The
kernel is the sole authority that can approve a change, and the API is only a way to
propose one.
A diagnostic looks like this:
- Structural lints (
dangling-ref,transition-needs-to,kernel-cycle, theslot-*family, …) are always fatalerrors and block the write. - Doctrine and budget lints (
negation,whiteout,lethal-override,budget, …) are advisory: they come back aswarning/infoand 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”).
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.
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. Omitid on a state to have one auto-assigned. The response tells you
which id (or event name) was created.
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:
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, set_subject_ref, 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.
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.
Attach a character (set_subject_ref)
set_subject_ref stamps a world’s canonical character reference: the pixel
subject lock that POST /v1/worlds/{id}/scenes paints
new scenes from, so the same character carries across scenes, levels and chapters.
It is metadata-only: it sets world.subjectRef and changes no states or edges,
but it funnels through the same kernel gate as every other write.
Use it to attach a character to a world that was created without one, or to rotate
the character on an existing world (no re-generation):
imageUrl must be a hosted, fetchable URL: a data: URI is rejected 400
(host it first) so later scene generations always have a durable reference and no
multi-MB blob is stored on the world. descriptor is optional; when a scene is
painted it defaults to the world’s subject. The same reference can also be set at
create time with the subject_ref field on POST /v1/worlds: the two write
surfaces agree, and both reject data: URIs. See
Character consistency for the full flow.
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}/validateruns the strict gate. It returns200with the resolvedworldwhen clean, or422withdiagnosticswhen not.POST /v1/worlds/{id}/lintis advisory: it never returns4xxon doctrine hits. It reports every finding pluscountsby severity and thepromptBudget(1900) your assembled prompts must stay under.
world (or data) in the body to check a
candidate world; omit it to check the stored one.
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.
Arcadeify
POST /v1/worlds/{id}/arcadeify turns any world into a shoot-for-score arcade
in one call: a progression, hostiles, and a win condition derived from the
world’s own fiction, not a bolted-on template. It runs the exact same canned
kernel-agent ask as the Studio’s one-click Arcadeify button, so a world
converted over the API and one converted in the editor come out identical. Like
/edit, it’s the agent authoring behind the same fail-closed
gate: the result persists only after passing the kernel grammar and doctrine
suite, and the response is result-only (world, diagnostics, reply, plus the
versionId of the snapshot).
Because it invokes the authoring agent, it meters against your daily edit
quota: the worst case (a few agent rounds) is reserved before the agent runs.
A reject or agent error refunds the whole reservation; on a committed edit, a
no-change run, or a concurrency conflict the agent rounds actually burned are
metered and only the unused slice is refunded: not the generation quota. Send an Idempotency-Key so a
retry never double-charges.
force semantics. If the world already has a progression, the call is
refused 409 (world already has progression, pass force:true to rebuild it)
before any quota is reserved, so you never pay to clobber an arcade by
accident. Pass { "force": true } to rebuild it deliberately. The other 409
cause is the usual stale-rev concurrency conflict.
Rollback. Every success snapshots a version titled arcadeify (source ai)
onto HEAD, so the conversion is one step from undo: list the tree with
GET /v1/worlds/{id}/versions, then POST /v1/worlds/{id}/checkout the parent
version to restore the pre-arcade graph. (If that post-persist snapshot fails, the
conversion still persists and versionId comes back null.)

