# Content-engine API

## Connect or resume — always do this first

An invitation is for initial enrollment, not login. Even when the user pastes an old invitation URL, FIRST look for saved credentials. An already-used or expired invitation does not mean the saved API key has expired.

1. Read .content-engine.json in the game project if present. It contains only service_url, workspace_id and project_id (no secrets). Check ~/.config/content-engine/credentials.json for this exact HTTPS origin. Its format is {"services":{"https://service.example":{"accounts":{"ws_example":{"api_key":"<secret>","credential_id":"key_example"}},"pending_api_key":"<key saved before enrollment, if any>"}}}. Expand ~ to the current user's home directory on every OS. Use the account matching the project binding; if several accounts exist and no binding selects one, ask which account instead of guessing.
2. Older sessions may have saved the key elsewhere. Check prior setup notes/agent memory and credential paths referenced by the project's existing configuration; check the content-engine directory under ~/.config. Read only relevant credential files privately, without printing keys or scanning unrelated secrets. Do not assume a missing canonical file means a new user. Validate a relevant saved key with GET /v1/me using Authorization: Bearer <key>, sent ONLY to its stored HTTPS origin. If valid, skip enrollment entirely, including when the invite is used or expired. Preserve existing keys when migrating a valid older credential into the canonical store.
3. GET /v1/projects and resume the bound project. If no binding exists, inspect the existing projects and bind the matching game; ask if ambiguous. Do not create a duplicate project just because this is a new chat. Continue the user's authoring request after authentication; they should not need to tell you to search for the key.
4. Only for initial signup, after checking existing credentials: extract ce_inv_ from #invite= in the supplied URL. Generate ce_key_ plus 32 cryptographically random bytes in lowercase hex. Save it as pending_api_key for this origin BEFORE POST /v1/enroll with {"invitation":"<fragment value>","api_key":"<saved key>"}. Reuse a previously saved pending key after a lost response. On success, move it into accounts keyed by the returned workspace_id, save credential_id, and remove that pending entry. Preserve other services/accounts and never overwrite a working key. After project selection/creation, save .content-engine.json with the plain service_url, workspace_id and project_id.
5. If enrollment reports invitation_used or invalid_invitation, follow next_action: return to saved-key discovery and GET /v1/me. Do not stop at “invite already used” or generate more keys. If no valid saved credential is accessible, explain that another machine or a lost/revoked key needs recovery access to the SAME workspace from the owner.

Keep the credential store outside the repository with owner-only access (directory 0700, file 0600 on POSIX, equivalent owner-only ACL on Windows). Never print, commit, or place keys/invitations in .content-engine.json. Do not overwrite unrelated configuration. Invitations normally expire in 24 hours; registered keys remain valid until revoked. The service cannot inspect your local files: credential discovery and storage are the calling agent's responsibility.

For future sessions, the user can say: “Use <plain service URL> for this game and continue writing dialogue.” No invite is needed.


Author game writing from reusable project context. The caller gathers context, reviews the writing and integrates it. The service makes one bounded authoring call and validates structure and references.

1. Read GET /v1/schema for project fields and built-in contracts under x-presets. Read GET /v1/schema/author for the author request envelope.
2. POST /v1/startup with known configuration ({} is valid). It returns unanswered setup questions. Required: brief ≤500 characters, style.instructions, rules.allow_new, rules.instructions and enabled content_types. Reuse settled answers; ask the user only unresolved questions. POST the completed configuration to /v1/projects.
3. GET /v1/projects/:id to read configuration and effective limits. Edit with PUT and If-Match: "<revision>". Add relevant nouns, relationships, sourced story_facts and approved style examples progressively; do not invent canon to finish setup.
4. POST /v1/projects/:id/author with Idempotency-Key and {project_revision, content_type, noun_ids, input, limits?}. Select registered subjects; direct relationships supply background unless include_related is false.

Content types: text authors short prose; scene_revision revises existing parts marked keep/revise; conversation_draft authors an opening, player choices and branch replies. Enable a built-in with an empty configuration object. Custom types require input/output schemas.

For conversation_draft, input contains instruction, encounter, outcomes, fact_ids, effects, conditions and retained_choices. Encounter contains entry_point, initiation, situation, present_ids, player_exposure and speaker_knowledge. Explicit unknowns and empty exposure lists are valid. Outcomes are what the player should learn or decide during the scene, not prerequisites. Use the schema's exact shape. Fact IDs reuse registered story_facts; effect/condition IDs bind existing game hooks to retained choices. Only selected present non-player speakers speak. An optional player_id identifies the player; a unique present narrative_kind player is also recognized. Optional pacing.max_opening_exchanges lowers the eight-exchange ceiling.

The conversation model writes only: opening speeches, retained-choice text/replies, and optional additional choices. It does not decide whether the caller is ready to author or return clarification questions. The service assigns graph IDs/transitions and attaches exact hook bindings. Public content remains {entry_id,nodes,choices,blocked_reason}, with blocked_reason empty. retained_choice_bindings accompanies the content. One decision point is supported; nested decision points/loops are not generated. Review before translating into a different game graph.

Project facts, initial speaker knowledge, player exposure and generated dialogue are distinct. A knowledgeable speaker can introduce a fact to a player who has never heard it. Unknown player history need not be supplied for a neutral introduction. The caller resolves genuinely missing authoring facts and checks the result; generated content does not update canon or game state.

Nouns have narrative_kind and optional game_kind (project-defined labels), name, summary and optional backstory/style/relationships. Legacy kind is accepted. Relationships use {kind,target_id,description?}. Story facts use {text,source,noun_ids,status?,holder_id?}; belief/speculation requires an attributed holder. Related background does not add speakers. Approved prefer/avoid examples guide voice.

Outcomes: ok returns content; needs_context reports missing setup/input before inference (scene_revision also supports a blocked scene); unavailable reports provider, budget or structural failure. Follow next_action and reason.questions for missing fields: check known sources, then ask the user only unresolved essential questions. Do not ask them to repeat an explicit unknown. Missing projects return setup navigation. No automatic retries run. For conversation_draft, top-level review: true adds one optional advisory review call after writing; notes or review failure never withhold the valid draft. Review usage is separate in review.usage. Graphs support one decision point, not nested choices. Advisory review_context and review findings are not quality verdicts.

Local scene/conversation defaults are 8,192 output tokens and 120 seconds, within this server's 120000-ms ceiling. limits.max_output_tokens may raise or lower the output allowance up to 16,384 without a project edit. Input/deadline limits may only be lowered per request. Read content_type_limits. Length cutoffs return increase_output_budget guidance; they are not missing story context. No partial invalid content is returned.

Identical body/key replays its receipt for seven days, including failures. Use a new key after changing input or deliberately making another attempt. In-progress requests return 409. Interrupted requests are not automatically rerun. Receipts include timing and provider-reported usage when known.

Recommendations are optional and should reflect the actual content library; request receipts and repeated generation attempts are not a content library. The user chooses scope and adoption.
