User Guide — The Prosie API

← Back to the Guide

The Prosie API (for power users)

Prosie exposes a token-authenticated HTTP API so you can drive your stories from your own tools — a backup script, an Obsidian plugin, an iOS Shortcut, a Discord bot, or a keyboard macro. The API carries the same voice-preserving context as the web app (your prose style, lore, characters and story-so-far), so a continuation generated over the API reads just like one generated in the editor.

It's a small, predictable REST API: JSON in, JSON out for application resources, streamed downloads for exports, one resource per URL. Everything below is scoped to the token owner — you can only ever see and touch your own work.

Using the official Prosie CLI

jonbaldie/prosie-cli is the official command-line client for Prosie's headless API. Use it from your terminal, scripts, or coding agent to manage books and chapters, export prose, and generate continuations. It connects to https://prosie.app by default and handles API requests and authentication for you.

Install and sign in
brew install jonbaldie/tap/prosie
prosie auth login
prosie auth status

The login command opens your browser to approve access and saves the login locally. For read-only access, use prosie auth login --scopes read. Without Homebrew, download a binary from the GitHub releases. For scripts, you can supply an API token through the PROSIE_API_TOKEN environment variable.

Read and export your work

Start with the book list, then use the returned IDs. Replace BOOK_ID and CHAPTER_ID below with your own IDs. The CLI calls them books and chapters; the HTTP API below calls them stories and scenes.

prosie book list
prosie book show BOOK_ID
prosie chapter list BOOK_ID
prosie chapter show CHAPTER_ID
prosie book export BOOK_ID --output manuscript.md
prosie book list --json

Use --json for structured output in scripts and coding agents. Run prosie --help for commands covering series, codex entries, chat, and more.

Create a book or generate prose
prosie book create --title "My Novel"
prosie generate continue CHAPTER_ID --no-persist --json

Creating a book needs write permission. Generation needs generate permission and configured LLM settings; it uses your LLM credits. The example returns a continuation without saving it. Omit --no-persist to append it to the chapter, which also needs write permission.

Using Prosie from a coding agent

Want a coding agent (Codex, Claude Code, …) to drive Prosie for you? Create a token, then hand the agent the copyable block below. It carries everything the agent needs — base URL, headers, what each permission means, and the safety rules to follow.

1. Create a token

Open API Tokens from the account menu (top-right) — it's an account setting, not a per-story one. Give the token a name that says what it's for (e.g. claude-code-readonly) so you recognise it later.

2. Pick the least permission it needs
  • read — the agent can list and inspect your stories, scenes, series, codex entries and exports, but change nothing. Safest starting point.
  • write — the agent can create, import, update, reorder and delete story data. Grant only when you want it editing.
  • generate — the agent can run AI continuations, rewrites and summaries. This spends your LLM credits.

Recipes: read-only inspection → read; importing or editing books → read + write; letting it generate prose → add generate. Prefer the smallest set — you can always mint another token later. Every headless API route is limited to 60 requests per minute per account. Over that, the API returns 429. Generation calls also have a tighter limit of 30 per minute.

3. Keep the token secret

Prosie shows the token's value once, at creation — copy it then. Never commit it to a repository or paste it into project files. Give it to your agent through a secure local mechanism instead: an environment variable, a secret store, or a private prompt. The placeholder below is <YOUR_PROSIE_API_TOKEN> — replace it with your real token only in that secure place.

4. Copy the agent instructions

You can drive my Prosie account through its headless REST API. Follow these rules.

## Base URL
https://prosie.app/api

## Authentication
Send my token as a bearer header on every request. The token value will be provided
to you securely (e.g. an environment variable) — never hard-code or commit it.

    Authorization: Bearer <YOUR_PROSIE_API_TOKEN>
    Accept: application/json
    Content-Type: application/json

Everything is scoped to the token's owner: you can only see and touch my own work.

## Token abilities (permissions)
Every operation in the OpenAPI spec below carries an `x-ability` field naming the
ability it needs: `read`, `write`, or `generate` (running AI continuations, rewrites
and summaries — this spends my LLM credits). If a request returns 403, the token
most likely lacks that ability. Report that clearly ("this needs a token with the
`write` ability") instead of failing silently. Every headless API route is limited to 60 requests per minute per account (429 if exceeded). Generation endpoints are also rate
limited (429 if exceeded, default 30/minute per account).

## Safety rules
- Ask me before any destructive write (delete, overwrite, bulk reorder).
- Do NOT call `x-ability: generate` endpoints unless I explicitly ask — they cost money.
- Prefer read calls to understand state before changing anything.
- On 422, read the `errors` object — it names the invalid field(s).

## First smoke test (harmless read — needs the `read` ability)
    curl https://prosie.app/api/stories \
      -H "Authorization: Bearer <YOUR_PROSIE_API_TOKEN>" \
      -H "Accept: application/json"

If that returns a JSON list of my stories, the token works.

## Full API spec (OpenAPI 3.1)
Below is the complete, authoritative spec for every endpoint — paths, methods,
request/response schemas and the `x-ability` each one needs. Treat it as ground
truth over anything else you've inferred.
{
  "openapi": "3.1.0",
  "info": {
    "title": "Prosie API",
    "version": "1.0.0",
    "description": "Headless REST API for Prosie. Every route is scoped to the Sanctum token owner and gated on a token ability (read/write/generate). Every route is rate limited to 60 requests per minute per account. Generation routes have an additional limit of 30 requests per minute per account."
  },
  "servers": [
    { "url": "https://prosie.app/api" }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Sanctum personal access token."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "message": { "type": "string" },
          "errors": {
            "type": "object",
            "additionalProperties": { "type": "array", "items": { "type": "string" } },
            "description": "Present on 422 validation failures only, keyed by field."
          }
        },
        "required": ["message"]
      },
      "Story": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "title": { "type": "string" },
          "subtitle": { "type": ["string", "null"] },
          "series_id": { "type": ["integer", "null"] },
          "pov": { "type": "string", "enum": ["first", "second", "third"] },
          "tense": { "type": "string", "enum": ["past", "present", "future"] },
          "lore": { "type": ["string", "null"] },
          "characters": { "type": ["string", "null"] },
          "story_so_far": { "type": ["string", "null"] },
          "prose_style": { "type": ["string", "null"] },
          "target_word_count": { "type": ["integer", "null"] },
          "filter_using_story_so_far": { "type": "boolean" },
          "chapters": { "type": "array", "items": { "$ref": "#/components/schemas/Scene" }, "description": "Ordered chapters, included when creating or reading a single book." },
          "word_count": { "type": "integer" },
          "target_progress_percent": { "type": ["integer", "null"] },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "Scene": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "story_id": { "type": "integer" },
          "order": { "type": "integer" },
          "name": { "type": ["string", "null"] },
          "custom_pov": { "type": ["string", "null"] },
          "custom_character": { "type": ["string", "null"] },
          "content": { "type": ["string", "null"], "description": "HTML." },
          "summary": { "type": ["string", "null"] },
          "word_count": { "type": "integer" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "Series": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "name": { "type": "string" },
          "description": { "type": ["string", "null"] },
          "story_ids": { "type": "array", "items": { "type": "integer" } },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "CodexEntry": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "story_id": { "type": ["integer", "null"] },
          "series_id": { "type": ["integer", "null"] },
          "parent_id": { "type": ["integer", "null"], "description": "Set when this is a per-book append to a series-shared entry." },
          "category": { "type": "string", "enum": ["lore", "character"] },
          "name": { "type": "string" },
          "aliases": { "type": ["string", "null"] },
          "content": { "type": "string" },
          "order": { "type": "integer" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "RewritePrompt": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "key": { "type": "string", "description": "Pass this as the rewrite endpoint's `action` field." },
          "label": { "type": "string" },
          "instruction": { "type": "string" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "RewriteAction": {
        "type": "object",
        "properties": {
          "key": { "type": "string" },
          "label": { "type": "string" },
          "instruction": { "type": "string" }
        }
      },
      "LlmConfig": {
        "type": "object",
        "properties": {
          "provider": { "type": ["string", "null"], "enum": ["openai", "anthropic", "openrouter", null] },
          "model": { "type": ["string", "null"] },
          "has_openai_key": { "type": "boolean" },
          "has_anthropic_key": { "type": "boolean" },
          "has_openrouter_key": { "type": "boolean" }
        }
      },
      "GenerationUsage": {
        "type": "object",
        "properties": {
          "prompt_tokens": { "type": "integer" },
          "completion_tokens": { "type": "integer" },
          "total_tokens": { "type": "integer" }
        }
      },
      "ContinueResult": {
        "type": "object",
        "properties": {
          "prose": { "type": "string" },
          "html": { "type": "string" },
          "persisted": { "type": "boolean" },
          "model": { "type": "string" },
          "usage": { "$ref": "#/components/schemas/GenerationUsage" },
          "estimated_prompt_tokens": { "type": "integer" }
        }
      },
      "RewriteResult": {
        "type": "object",
        "properties": {
          "prose": { "type": "string" },
          "model": { "type": "string" },
          "usage": { "$ref": "#/components/schemas/GenerationUsage" },
          "estimated_prompt_tokens": { "type": "integer" }
        }
      },
      "SummaryResult": {
        "type": "object",
        "properties": {
          "summary": { "type": "string" },
          "persisted": { "type": "boolean" },
          "model": { "type": "string" },
          "usage": { "$ref": "#/components/schemas/GenerationUsage" },
          "estimated_prompt_tokens": { "type": "integer" }
        }
      },
      "ChatMessage": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "conversation_id": { "type": "integer" },
          "role": { "type": "string", "enum": ["user", "assistant"] },
          "content": { "type": "string" },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      },
      "Conversation": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "story_id": { "type": "integer" },
          "title": { "type": ["string", "null"] },
          "model": { "type": ["string", "null"] },
          "up_to_scene_id": { "type": ["integer", "null"] },
          "fidelity": { "type": "string", "enum": ["summary", "verbatim"] },
          "expires_in_days": { "type": "integer" },
          "messages": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/ChatMessage" }
          },
          "created_at": { "type": "string", "format": "date-time" },
          "updated_at": { "type": "string", "format": "date-time" }
        }
      }
    },
    "responses": {
      "ValidationError": {
        "description": "422 Unprocessable Content - request failed validation.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "Forbidden": {
        "description": "403 Forbidden - the token lacks the required ability, or the resource belongs to another account.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "NotFound": {
        "description": "404 Not Found.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "TooManyRequests": {
        "description": "429 Too Many Requests - the account's generation rate limit (default 30/min) was exceeded.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      },
      "UpstreamGenerationError": {
        "description": "502 Bad Gateway - the upstream LLM provider failed.",
        "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
      }
    }
  },
  "security": [{ "bearerAuth": [] }],
  "paths": {
    "/stories": {
      "get": {
        "summary": "List your stories",
        "x-ability": "read",
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Story" } } } } } } }
        }
      },
      "post": {
        "summary": "Create a story",
        "x-ability": "write",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["title"],
                "properties": {
                  "title": { "type": "string", "maxLength": 255 },
                  "subtitle": { "type": ["string", "null"], "maxLength": 255 },
                  "series_id": { "type": ["integer", "null"], "description": "Must be a series you own." },
                  "pov": { "type": "string", "enum": ["first", "second", "third"], "default": "third" },
                  "tense": { "type": "string", "enum": ["past", "present", "future"], "default": "past" },
                  "lore": { "type": ["string", "null"] },
                  "characters": { "type": ["string", "null"] },
                  "story_so_far": { "type": ["string", "null"] },
                  "prose_style": { "type": ["string", "null"] },
                  "target_word_count": { "type": ["integer", "null"], "minimum": 1, "maximum": 100000000 },
                  "filter_using_story_so_far": { "type": "boolean" },
                  "scaffold": { "type": "boolean", "default": true, "description": "false skips the default 'Chapter 1' scene." }
                }
              }
            }
          }
        },
        "responses": {
          "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Story" } } } } } },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/stories/import-docx": {
      "post": {
        "summary": "Import a DOCX story, using Heading 1 paragraphs as scene boundaries",
        "x-ability": "write",
        "requestBody": {
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["document", "title"],
                "properties": {
                  "document": { "type": "string", "format": "binary", "description": ".docx file." },
                  "title": { "type": "string", "maxLength": 255 },
                  "subtitle": { "type": ["string", "null"] },
                  "series_id": { "type": ["integer", "null"] },
                  "pov": { "type": "string", "enum": ["first", "second", "third"] },
                  "tense": { "type": "string", "enum": ["past", "present", "future"] },
                  "lore": { "type": ["string", "null"] },
                  "characters": { "type": ["string", "null"] },
                  "story_so_far": { "type": ["string", "null"] },
                  "prose_style": { "type": ["string", "null"] },
                  "target_word_count": { "type": ["integer", "null"] },
                  "filter_using_story_so_far": { "type": "boolean" }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": { "application/json": { "schema": { "type": "object", "properties": {
              "data": { "$ref": "#/components/schemas/Story" },
              "scenes": { "type": "array", "items": { "$ref": "#/components/schemas/Scene" } }
            } } } }
          },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/stories/{story}": {
      "get": {
        "summary": "Fetch one story",
        "x-ability": "read",
        "parameters": [{ "name": "story", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Story" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      },
      "patch": {
        "summary": "Update any story field",
        "x-ability": "write",
        "parameters": [{ "name": "story", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "description": "All fields optional; see POST /stories for the field list." } } }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Story" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      },
      "delete": {
        "summary": "Delete a story",
        "x-ability": "write",
        "parameters": [{ "name": "story", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "204": { "description": "No Content" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/stories/{story}/duplicate": {
      "post": {
        "summary": "Duplicate an owned story with its scenes and codex entries",
        "x-ability": "write",
        "parameters": [{ "name": "story", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Story" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/series": {
      "get": {
        "summary": "List your series",
        "x-ability": "read",
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Series" } } } } } } }
        }
      },
      "post": {
        "summary": "Create a series",
        "x-ability": "write",
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "required": ["name"], "properties": {
            "name": { "type": "string", "maxLength": 255 },
            "description": { "type": ["string", "null"], "maxLength": 5000 }
          } } } }
        },
        "responses": {
          "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Series" } } } } } },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/series/{series}": {
      "get": {
        "summary": "Fetch one series, including its member book IDs",
        "x-ability": "read",
        "parameters": [{ "name": "series", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Series" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      },
      "patch": {
        "summary": "Update a series",
        "x-ability": "write",
        "parameters": [{ "name": "series", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "properties": {
            "name": { "type": "string", "maxLength": 255 },
            "description": { "type": ["string", "null"], "maxLength": 5000 }
          } } } }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Series" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      },
      "delete": {
        "summary": "Delete a series",
        "x-ability": "write",
        "parameters": [{ "name": "series", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "204": { "description": "No Content" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/series/{series}/stories/{story}": {
      "put": {
        "summary": "Attach one of your books to a series",
        "x-ability": "write",
        "parameters": [
          { "name": "series", "in": "path", "required": true, "schema": { "type": "integer" } },
          { "name": "story", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Story" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      },
      "delete": {
        "summary": "Detach a member book from a series",
        "x-ability": "write",
        "parameters": [
          { "name": "series", "in": "path", "required": true, "schema": { "type": "integer" } },
          { "name": "story", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": {
          "204": { "description": "No Content" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/series/{series}/codex-entries": {
      "get": {
        "summary": "List a series' shared base codex entries",
        "x-ability": "read",
        "parameters": [{ "name": "series", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/CodexEntry" } } } } } } }
        }
      },
      "post": {
        "summary": "Add a shared base codex entry to a series",
        "x-ability": "write",
        "parameters": [{ "name": "series", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "required": ["category", "name", "content"], "properties": {
            "category": { "type": "string", "enum": ["lore", "character"] },
            "name": { "type": "string", "maxLength": 120 },
            "aliases": { "type": ["string", "null"], "maxLength": 500 },
            "content": { "type": "string", "maxLength": 50000 },
            "order": { "type": "integer", "minimum": 0 }
          } } } }
        },
        "responses": {
          "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/CodexEntry" } } } } } },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/series-codex-entries/{codexEntry}": {
      "patch": {
        "summary": "Update a shared base codex entry",
        "x-ability": "write",
        "parameters": [{ "name": "codexEntry", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "properties": {
            "category": { "type": "string", "enum": ["lore", "character"] },
            "name": { "type": "string", "maxLength": 120 },
            "aliases": { "type": ["string", "null"], "maxLength": 500 },
            "content": { "type": "string", "maxLength": 50000 },
            "order": { "type": "integer", "minimum": 0 }
          } } } }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/CodexEntry" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "delete": {
        "summary": "Delete a shared base codex entry",
        "x-ability": "write",
        "parameters": [{ "name": "codexEntry", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "204": { "description": "No Content" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/stories/{story}/series-codex-entries/{codexEntry}/append": {
      "put": {
        "summary": "Create or update this book's append to a shared series codex entry",
        "x-ability": "write",
        "parameters": [
          { "name": "story", "in": "path", "required": true, "schema": { "type": "integer" } },
          { "name": "codexEntry", "in": "path", "required": true, "schema": { "type": "integer" }, "description": "The base entry's id." }
        ],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "required": ["content"], "properties": {
            "content": { "type": "string", "maxLength": 50000 }
          } } } }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/CodexEntry" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "description": "The base entry isn't shared with this book's current series.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      },
      "delete": {
        "summary": "Delete this book's append to a shared series codex entry",
        "x-ability": "write",
        "parameters": [
          { "name": "story", "in": "path", "required": true, "schema": { "type": "integer" } },
          { "name": "codexEntry", "in": "path", "required": true, "schema": { "type": "integer" } }
        ],
        "responses": {
          "204": { "description": "No Content" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/stories/{story}/scenes": {
      "get": {
        "summary": "List a story's scenes, in order",
        "x-ability": "read",
        "parameters": [{ "name": "story", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Scene" } } } } } } }
        }
      },
      "post": {
        "summary": "Add a scene, appended to the end unless order is given",
        "x-ability": "write",
        "parameters": [{ "name": "story", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "properties": {
            "name": { "type": ["string", "null"], "maxLength": 255 },
            "order": { "type": "integer", "minimum": 0 },
            "custom_pov": { "type": ["string", "null"], "maxLength": 255 },
            "custom_character": { "type": ["string", "null"], "maxLength": 255 },
            "content": { "type": ["string", "null"], "description": "HTML." },
            "summary": { "type": ["string", "null"] }
          } } } }
        },
        "responses": {
          "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Scene" } } } } } },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/stories/{story}/scene-summaries": {
      "patch": {
        "summary": "Bulk update scene summaries for one story, atomically",
        "x-ability": "write",
        "parameters": [{ "name": "story", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "required": ["summaries"], "properties": {
            "summaries": { "type": "array", "minItems": 1, "items": { "type": "object", "required": ["scene_id", "summary"], "properties": {
              "scene_id": { "type": "integer" },
              "summary": { "type": ["string", "null"] }
            } } }
          } } } }
        },
        "responses": {
          "200": { "description": "OK - every referenced scene belonged to this story", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Scene" } } } } } } },
          "422": { "description": "A referenced scene_id does not belong to this story; nothing was changed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/stories/{story}/scenes/reorder": {
      "post": {
        "summary": "Reorder a story's scenes from an ordered list of scene_ids",
        "x-ability": "write",
        "parameters": [{ "name": "story", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "required": ["scene_ids"], "properties": {
            "scene_ids": { "type": "array", "items": { "type": "integer" }, "description": "Ids not belonging to this story and repeated ids are ignored. Omitted chapters follow the requested chapters in their existing order; positions are contiguous from zero." }
          } } } }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Scene" } } } } } } }
        }
      }
    },
    "/stories/{story}/codex-entries": {
      "get": {
        "summary": "List a story's standalone codex entries",
        "x-ability": "read",
        "parameters": [{ "name": "story", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/CodexEntry" } } } } } } }
        }
      },
      "post": {
        "summary": "Add a standalone codex entry to a story",
        "x-ability": "write",
        "parameters": [{ "name": "story", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "required": ["category", "name", "content"], "properties": {
            "category": { "type": "string", "enum": ["lore", "character"] },
            "name": { "type": "string", "maxLength": 120 },
            "aliases": { "type": ["string", "null"], "maxLength": 500 },
            "content": { "type": "string", "maxLength": 50000 },
            "order": { "type": "integer", "minimum": 0 }
          } } } }
        },
        "responses": {
          "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/CodexEntry" } } } } } },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/stories/{story}/codex-entries/bulk": {
      "post": {
        "summary": "Create several standalone codex entries on a story in one call",
        "x-ability": "write",
        "parameters": [{ "name": "story", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "required": ["entries"], "properties": {
            "entries": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "object", "required": ["category", "name", "content"], "properties": {
              "category": { "type": "string", "enum": ["lore", "character"] },
              "name": { "type": "string", "maxLength": 120 },
              "aliases": { "type": ["string", "null"], "maxLength": 500 },
              "content": { "type": "string", "maxLength": 50000 },
              "order": { "type": "integer", "minimum": 0 }
            } } }
          } } } }
        },
        "responses": {
          "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/CodexEntry" } } } } } } },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/codex-entries/{codexEntry}": {
      "get": {
        "summary": "Fetch one standalone codex entry",
        "x-ability": "read",
        "parameters": [{ "name": "codexEntry", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/CodexEntry" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      },
      "patch": {
        "summary": "Update a standalone codex entry",
        "x-ability": "write",
        "parameters": [{ "name": "codexEntry", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "properties": {
            "category": { "type": "string", "enum": ["lore", "character"] },
            "name": { "type": "string", "maxLength": 120 },
            "aliases": { "type": ["string", "null"], "maxLength": 500 },
            "content": { "type": "string", "maxLength": 50000 },
            "order": { "type": "integer", "minimum": 0 }
          } } } }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/CodexEntry" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      },
      "delete": {
        "summary": "Delete a standalone codex entry",
        "x-ability": "write",
        "parameters": [{ "name": "codexEntry", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "204": { "description": "No Content" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/scenes/{scene}": {
      "get": {
        "summary": "Fetch one scene",
        "x-ability": "read",
        "parameters": [{ "name": "scene", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Scene" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      },
      "patch": {
        "summary": "Update a scene's name, content, summary, POV or character",
        "x-ability": "write",
        "parameters": [{ "name": "scene", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "properties": {
            "name": { "type": ["string", "null"], "maxLength": 255 },
            "order": { "type": "integer", "minimum": 0 },
            "custom_pov": { "type": ["string", "null"], "maxLength": 255 },
            "custom_character": { "type": ["string", "null"], "maxLength": 255 },
            "content": { "type": ["string", "null"] },
            "summary": { "type": ["string", "null"] }
          } } } }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Scene" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      },
      "delete": {
        "summary": "Delete a scene. Refused if it's the story's last one",
        "x-ability": "write",
        "parameters": [{ "name": "scene", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "204": { "description": "No Content" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "description": "A story must keep at least one scene.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/scenes/{scene}/export": {
      "get": {
        "summary": "Export one scene as Markdown or Word",
        "x-ability": "read",
        "parameters": [
          { "name": "scene", "in": "path", "required": true, "schema": { "type": "integer" } },
          { "name": "format", "in": "query", "required": false, "schema": { "type": "string", "enum": ["markdown", "docx"], "default": "markdown" } }
        ],
        "responses": {
          "200": { "description": "Streamed file download (text/markdown or .docx).", "content": { "text/markdown": {}, "application/vnd.openxmlformats-officedocument.wordprocessingml.document": {} } },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/stories/{story}/export": {
      "get": {
        "summary": "Export a whole story as Markdown or Word",
        "x-ability": "read",
        "parameters": [
          { "name": "story", "in": "path", "required": true, "schema": { "type": "integer" } },
          { "name": "format", "in": "query", "required": false, "schema": { "type": "string", "enum": ["markdown", "docx"], "default": "markdown" } }
        ],
        "responses": {
          "200": { "description": "Streamed file download (text/markdown or .docx).", "content": { "text/markdown": {}, "application/vnd.openxmlformats-officedocument.wordprocessingml.document": {} } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/scenes/{scene}/reject-continuation": {
      "post": {
        "summary": "Drop the most recent AI continuation from a scene",
        "x-ability": "write",
        "parameters": [{ "name": "scene", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": { "description": "OK - a no-op when the scene doesn't end in generated prose.", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Scene" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/scenes/{scene}/rewrite/undo": {
      "post": {
        "summary": "Revert the most recent in-place rewrite on a scene",
        "x-ability": "write",
        "parameters": [{ "name": "scene", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": { "description": "OK - reverts scene content to pre-rewrite baseline.", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Scene" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/llm-config": {
      "get": {
        "summary": "Read your provider, model and which keys are stored",
        "x-ability": "read",
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/LlmConfig" } } } } } }
        }
      },
      "patch": {
        "summary": "Change provider, model or API keys (keys are write-only)",
        "x-ability": "write",
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "properties": {
            "provider": { "type": "string", "enum": ["openai", "anthropic", "openrouter"] },
            "model": { "type": "string", "maxLength": 255 },
            "openai_api_key": { "type": ["string", "null"], "description": "Omitted or empty keeps the currently stored key." },
            "anthropic_api_key": { "type": ["string", "null"] },
            "openrouter_api_key": { "type": ["string", "null"] }
          } } } }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/LlmConfig" } } } } } },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/llm-models": {
      "get": {
        "summary": "The model catalogue per provider, plus which providers you've keyed",
        "x-ability": "read",
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "object", "properties": {
            "providers": { "type": "object", "description": "Model list keyed by provider." },
            "configured": { "type": "array", "items": { "type": "string" }, "description": "Providers you hold an API key for." }
          } } } } } } }
        }
      }
    },
    "/rewrite-prompts": {
      "get": {
        "summary": "List your own rewrite prompts",
        "x-ability": "read",
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/RewritePrompt" } } } } } } }
        }
      },
      "post": {
        "summary": "Create a rewrite prompt",
        "x-ability": "write",
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "required": ["label", "instruction"], "properties": {
            "label": { "type": "string", "maxLength": 60 },
            "instruction": { "type": "string", "maxLength": 2000 }
          } } } }
        },
        "responses": {
          "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/RewritePrompt" } } } } } },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/rewrite-prompts/{id}": {
      "get": {
        "summary": "Fetch one rewrite prompt",
        "x-ability": "read",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/RewritePrompt" } } } } } },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      },
      "patch": {
        "summary": "Update a rewrite prompt",
        "x-ability": "write",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "properties": {
            "label": { "type": "string", "maxLength": 60 },
            "instruction": { "type": "string", "maxLength": 2000 }
          } } } }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/RewritePrompt" } } } } } },
          "404": { "$ref": "#/components/responses/NotFound" },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      },
      "delete": {
        "summary": "Delete a rewrite prompt",
        "x-ability": "write",
        "parameters": [{ "name": "id", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "204": { "description": "No Content" },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/rewrite-actions": {
      "get": {
        "summary": "Every rewrite action (built-ins + yours), keyed for the rewrite endpoint's action field",
        "x-ability": "read",
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/RewriteAction" } } } } } } }
        }
      }
    },
    "/story-templates": {
      "get": {
        "summary": "List story structure templates",
        "x-ability": "read",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "key": { "type": "string" },
                          "name": { "type": "string" },
                          "description": { "type": "string" },
                          "beat_count": { "type": "integer" }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/story-templates/{key}": {
      "get": {
        "summary": "Fetch one story structure template with its beats",
        "x-ability": "read",
        "parameters": [{ "name": "key", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "object",
                      "properties": {
                        "key": { "type": "string" },
                        "name": { "type": "string" },
                        "description": { "type": "string" },
                        "beats": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "name": { "type": "string" },
                              "guidance": { "type": "string" }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "404": { "$ref": "#/components/responses/NotFound" }
        }
      }
    },
    "/scenes/{scene}/continue": {
      "post": {
        "summary": "Generate a continuation. Saves & appends by default (needs write too)",
        "x-ability": "generate",
        "description": "Rate-limited to 30 requests/minute per account (config prosie.llm_rate_limit).",
        "parameters": [{ "name": "scene", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "properties": {
            "instruction": { "type": ["string", "null"], "description": "Optional beat/steering instruction." },
            "persist": { "type": "boolean", "default": true, "description": "false returns the prose without saving it (needs only generate, not write)." }
          } } } }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/ContinueResult" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "502": { "$ref": "#/components/responses/UpstreamGenerationError" }
        }
      }
    },
    "/scenes/{scene}/continue/stream": {
      "post": {
        "summary": "The same continuation call, streamed as Server-Sent Events",
        "x-ability": "generate",
        "description": "Emits one `event: delta` per fragment ({\"delta\": \"...\"}), then a terminal `event: done` (ContinueResult payload) or `event: error`. Same persist/ability rules as /continue. Rate-limited to 30 requests/minute per account.",
        "parameters": [{ "name": "scene", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "properties": {
            "instruction": { "type": ["string", "null"] },
            "persist": { "type": "boolean", "default": true }
          } } } }
        },
        "responses": {
          "200": { "description": "text/event-stream", "content": { "text/event-stream": {} } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/scenes/{scene}/continue/cancel": {
      "post": {
        "summary": "Stop a continuation stream in flight",
        "x-ability": "generate",
        "parameters": [{ "name": "scene", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": { "description": "OK" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/scenes/{scene}/rewrite": {
      "post": {
        "summary": "Rewrite a passage. Returns the new passage for you to place; persist: true applies it in place (requires write)",
        "x-ability": "generate",
        "description": "Rate-limited to 30 requests/minute per account.",
        "parameters": [{ "name": "scene", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "required": ["selection"], "properties": {
            "selection": { "type": "string", "description": "The passage to rewrite." },
            "action": { "type": "string", "description": "A key from GET /rewrite-actions, e.g. 'tighten' or 'user-{id}'. Unknown keys are rejected with 422; put free text in instruction." },
            "instruction": { "type": "string", "description": "Free-text alternative to action." },
            "persist": { "type": "boolean", "default": false, "description": "Whether to apply the rewritten prose in-place and save the scene (requires write ability)." }
          } } } }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/RewriteResult" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "502": { "$ref": "#/components/responses/UpstreamGenerationError" }
        }
      }
    },
    "/scenes/{scene}/rewrite/stream": {
      "post": {
        "summary": "The same rewrite call, streamed as Server-Sent Events",
        "x-ability": "generate",
        "description": "Emits one `event: delta` per fragment ({\"delta\": \"...\"}), then a terminal `event: done` or `event: error`. Rate-limited to 30 requests/minute per account.",
        "parameters": [{ "name": "scene", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "required": ["selection"], "properties": {
            "selection": { "type": "string", "description": "The passage to rewrite." },
            "action": { "type": "string", "description": "A key from GET /rewrite-actions, e.g. 'tighten' or 'user-{id}'. Unknown keys are rejected with 422; put free text in instruction." },
            "instruction": { "type": "string", "description": "Free-text alternative to action." },
            "persist": { "type": "boolean", "default": false, "description": "Whether to apply in-place and save (requires write ability)." }
          } } } }
        },
        "responses": {
          "200": { "description": "text/event-stream", "content": { "text/event-stream": {} } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/scenes/{scene}/summarize": {
      "post": {
        "summary": "Summarise a scene. Saves the summary & rebuilds story-so-far by default (needs write too)",
        "x-ability": "generate",
        "description": "Rate-limited to 30 requests/minute per account.",
        "parameters": [{ "name": "scene", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "properties": {
            "persist": { "type": "boolean", "default": true }
          } } } }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/SummaryResult" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "502": { "$ref": "#/components/responses/UpstreamGenerationError" }
        }
      }
    },
    "/stories/{story}/conversations": {
      "get": {
        "summary": "List chat conversations for an owned story",
        "x-ability": "read",
        "parameters": [{ "name": "story", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "type": "array", "items": { "$ref": "#/components/schemas/Conversation" } } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      },
      "post": {
        "summary": "Create a new chat conversation under an owned story",
        "x-ability": "write",
        "parameters": [{ "name": "story", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "content": { "application/json": { "schema": { "type": "object", "properties": {
            "title": { "type": ["string", "null"] },
            "up_to_scene_id": { "type": ["integer", "null"] },
            "fidelity": { "type": "string", "enum": ["summary", "verbatim"] },
            "model": { "type": ["string", "null"] }
          } } } }
        },
        "responses": {
          "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Conversation" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/conversations/{conversation}": {
      "get": {
        "summary": "Fetch an owned chat conversation and its message history",
        "x-ability": "read",
        "parameters": [{ "name": "conversation", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Conversation" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      },
      "delete": {
        "summary": "Delete an owned chat conversation thread",
        "x-ability": "write",
        "parameters": [{ "name": "conversation", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "204": { "description": "No Content" },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/conversations/{conversation}/export": {
      "get": {
        "summary": "Export an owned chat conversation as a JSON download",
        "x-ability": "read",
        "parameters": [{ "name": "conversation", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": { "description": "application/json", "content": { "application/json": {} } },
          "403": { "$ref": "#/components/responses/Forbidden" }
        }
      }
    },
    "/stories/{story}/conversations/import": {
      "post": {
        "summary": "Import a chat conversation into an owned story",
        "x-ability": "write",
        "parameters": [{ "name": "story", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "content": {
            "multipart/form-data": { "schema": { "type": "object", "properties": { "file": { "type": "string", "format": "binary" } } } },
            "application/json": { "schema": { "type": "object" } }
          }
        },
        "responses": {
          "201": { "description": "Created", "content": { "application/json": { "schema": { "type": "object", "properties": { "data": { "$ref": "#/components/schemas/Conversation" } } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationError" }
        }
      }
    },
    "/conversations/{conversation}/messages": {
      "post": {
        "summary": "Send a message turn to a chat conversation",
        "x-ability": "generate",
        "description": "Rate-limited to 30 requests/minute per account.",
        "parameters": [{ "name": "conversation", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["content"], "properties": { "content": { "type": "string" } } } } }
        },
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": {
            "data": { "type": "object", "properties": {
              "message": { "$ref": "#/components/schemas/ChatMessage" },
              "model": { "type": "string" },
              "usage": { "$ref": "#/components/schemas/GenerationUsage" }
            } }
          } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "502": { "$ref": "#/components/responses/UpstreamGenerationError" }
        }
      }
    },
    "/conversations/{conversation}/messages/stream": {
      "post": {
        "summary": "Stream assistant response tokens over Server-Sent Events",
        "x-ability": "generate",
        "description": "Rate-limited to 30 requests/minute per account.",
        "parameters": [{ "name": "conversation", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "type": "object", "required": ["content"], "properties": { "content": { "type": "string" } } } } }
        },
        "responses": {
          "200": { "description": "text/event-stream", "content": { "text/event-stream": {} } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/TooManyRequests" }
        }
      }
    },
    "/conversations/{conversation}/retry": {
      "post": {
        "summary": "Retry generation of the latest assistant message in a conversation",
        "x-ability": "generate",
        "description": "Rate-limited to 30 requests/minute per account.",
        "parameters": [{ "name": "conversation", "in": "path", "required": true, "schema": { "type": "integer" } }],
        "responses": {
          "200": { "description": "OK", "content": { "application/json": { "schema": { "type": "object", "properties": {
            "data": { "type": "object", "properties": {
              "message": { "$ref": "#/components/schemas/ChatMessage" },
              "model": { "type": "string" },
              "usage": { "$ref": "#/components/schemas/GenerationUsage" }
            } }
          } } } } },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "422": { "$ref": "#/components/responses/ValidationError" },
          "429": { "$ref": "#/components/responses/TooManyRequests" },
          "502": { "$ref": "#/components/responses/UpstreamGenerationError" }
        }
      }
    }
  }
}

If clipboard access is blocked, expand the block and select the text manually. The endpoint details below are the canonical reference the instructions point to.

Authentication

Create a token under API Tokens (in the user menu) and send it as a bearer header on every request. The base URL is https://prosie.app.

Authorization: Bearer <your-token>
Accept: application/json
Content-Type: application/json

Each token is scoped to one or more abilities, so a backup script can be given read only and can never mutate your manuscript or spend your LLM credits:

read

List and fetch stories & scenes, and pull Markdown / Word exports.

write

Create, update and delete stories & scenes.

generate

Run AI continuations, rewrites and summaries (this spends your LLM API credits).

Your first request

List your stories with a read token:

Request

 curl https://prosie.app/api/stories \
  -H "Authorization: Bearer <your-token>" \
  -H "Accept: application/json"

Response 200 OK

 {
  "data": [
    {
      "id": 1,
      "title": "The Salt Road",
      "subtitle": null,
      "series_id": null,
      "pov": "third",
      "tense": "past",
      "lore": null,
      "characters": null,
      "story_so_far": null,
      "prose_style": null,
      "target_word_count": 90000,
      "filter_using_story_so_far": true,
      "word_count": 12480,
      "target_progress_percent": 14,
      "created_at": "2026-06-01T09:12:44+00:00",
      "updated_at": "2026-06-14T18:03:10+00:00"
    }
  ]
}

Single-resource endpoints return the same object under a top-level data key; collections return an array.

Stories

GET /api/stories
List your stories.
read
GET /api/stories/{id}
Fetch one story, including its word_count and (if you set a goal) target_progress_percent.
read
POST /api/stories
Create a story.
write
POST /api/stories/import-docx
Import a DOCX story using Heading 1 paragraphs as scene boundaries. Accepts the same core story metadata as story create/update and returns the created scenes.
write
PATCH /api/stories/{id}
Update any story field.
write
DELETE /api/stories/{id}
Delete a story. Returns 204 No Content.
write

A new story starts with a “Chapter 1” scene by default; send scaffold: false for a bare story. You can set series_id (a series you own) and toggle filter_using_story_so_far — the same controls the editor exposes.

DOCX imports accept story metadata including subtitle, series, POV, tense, lore, characters, story-so-far, prose style, target word count and codex filtering. The response includes the story under data and ordered scene resources under scenes, including scene IDs and word counts for verification. Prosie word counts are computed from stored editor HTML after stripping markup, so they can differ from shell or DOCX tool counts.

Request

 curl https://prosie.app/api/stories \
  -X POST \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "title": "The Salt Road",
    "pov": "third",
    "tense": "past",
    "target_word_count": 90000
  }'

Response 201 Created

 {
  "data": {
    "id": 7,
    "title": "The Salt Road",
    "subtitle": null,
    "series_id": null,
    "pov": "third",
    "tense": "past",
    "lore": null,
    "characters": null,
    "story_so_far": null,
    "prose_style": null,
    "target_word_count": 90000,
    "filter_using_story_so_far": true,
    "word_count": 0,
    "target_progress_percent": 0,
    "created_at": "2026-06-16T10:01:22+00:00",
    "updated_at": "2026-06-16T10:01:22+00:00"
  }
}

Series

Manage multi-book series, attach books, and keep a shared codex in sync from external tools. Shared entries are base lore or character records for the whole series; a book can add its own append to one shared entry without replacing the base text.

GET /api/series
List your series.
read
GET /api/series/{id}
Fetch one series, including its member book IDs.
read
POST /api/series
Create a series.
write
PATCH /api/series/{id}
Update a series.
write
DELETE /api/series/{id}
Delete a series.
write
PUT /api/series/{series}/stories/{story}
Attach one of your books to a series.
write
DELETE /api/series/{series}/stories/{story}
Detach a member book from a series.
write
GET /api/series/{id}/codex-entries
List a series' shared codex entries.
read
POST /api/series/{id}/codex-entries
Add a shared codex entry.
write
PATCH /api/series-codex-entries/{id}
Update a shared codex entry.
write
DELETE /api/series-codex-entries/{id}
Delete a shared codex entry.
write
PUT /api/stories/{story}/series-codex-entries/{entry}/append
Create or update that book's append to a shared entry.
write
DELETE /api/stories/{story}/series-codex-entries/{entry}/append
Delete that book's append.
write

Scenes

GET /api/stories/{id}/scenes
List a story's scenes, in order.
read
GET /api/scenes/{id}
Fetch one scene (its prose lives in content).
read
POST /api/stories/{id}/scenes
Add a scene (appended to the end unless you pass order).
write
PATCH /api/scenes/{id}
Update a scene's name, content, summary, POV or character.
write
PATCH /api/stories/{id}/scene-summaries
Bulk update scene summaries for one story from summaries[].scene_id and summaries[].summary.
write
DELETE /api/scenes/{id}
Delete a scene. A story always keeps at least one, so deleting the last is refused (422).
write
POST /api/stories/{id}/scenes/reorder
Reorder scenes from an ordered list of scene_ids (the API version of dragging cards in the Outline).
write
POST /api/scenes/{id}/reject-continuation
Drop the most recent AI continuation from a scene (the editor's Reject button).
write

Bulk summary updates are story-scoped and atomic: every referenced scene must belong to the target story, or the request returns 422 without changing any summaries.

Example — fetch a scene

 curl https://prosie.app/api/scenes/42 \
  -H "Authorization: Bearer <your-token>" \
  -H "Accept: application/json"

Response 200 OK

 {
  "data": {
    "id": 42,
    "story_id": 7,
    "order": 0,
    "name": "Chapter 1",
    "custom_pov": null,
    "custom_character": null,
    "content": "

The wind came off the flats carrying salt and silence.

", "summary": null, "word_count": 11, "created_at": "2026-06-16T10:01:22+00:00", "updated_at": "2026-06-16T10:04:55+00:00" } }

AI generation

These are the voice engine. continue and summarize save their result by default (just like the editor), so a default call needs write on top of generate — pass persist: false to get the prose or summary back without touching your manuscript (needs only generate). rewrite does not save by default — pass persist: true (needs write) to apply it in place, and undo that with POST /api/scenes/{id}/rewrite/undo. Every response also carries the real usage and an estimated_prompt_tokens figure so you can watch costs.

POST /api/scenes/{id}/continue
Generate a continuation. Appends & saves by default (also needs write).
generate
POST /api/scenes/{id}/continue/stream
The same, streamed as Server-Sent Events.
generate
POST /api/scenes/{id}/continue/cancel
Stop a stream in flight.
generate
POST /api/scenes/{id}/rewrite
Rewrite a passage. Returns the new passage for you to place; persist: true applies it in place (also needs write). action must be a key from /api/rewrite-actions; free text goes in instruction.
generate
POST /api/scenes/{id}/rewrite/undo
Revert the most recent in-place rewrite.
write
POST /api/scenes/{id}/summarize
Summarise a scene. Saves the summary & rebuilds story-so-far by default (also needs write).
generate

Example — continue a scene

 curl https://prosie.app/api/scenes/42/continue \
  -X POST \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{ "instruction": "Bring in the harbourmaster.", "persist": false }'

Response 200 OK

 {
  "data": {
    "prose": "The harbourmaster met them at the jetty, lantern swinging...",
    "html": "

The harbourmaster met them at the jetty...

", "persisted": false, "model": "openai/gpt-5.4-mini", "usage": { "prompt_tokens": 1840, "completion_tokens": 212, "total_tokens": 2052 }, "estimated_prompt_tokens": 1795 } }

Example — rewrite a passage

 curl https://prosie.app/api/scenes/42/rewrite \
  -X POST \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
    "selection": "He walked very quickly to the door.",
    "action": "tighten"
  }'

Pass either a free-text instruction or an action key (a built-in like tighten, or one of your own prompts — discover keys via GET /api/rewrite-actions).

Response 200 OK

 {
  "data": {
    "prose": "He strode to the door.",
    "model": "openai/gpt-5.4-mini",
    "usage": { "prompt_tokens": 1320, "completion_tokens": 9, "total_tokens": 1329 },
    "estimated_prompt_tokens": 1298
  }
}

Example — streamed continuation (SSE)

 curl -N https://prosie.app/api/scenes/42/continue/stream \
  -X POST \
  -H "Authorization: Bearer <your-token>" \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{ "instruction": "Keep the tension high." }'

Response 200 OK

 event: delta
data: {"delta":"The harbourmaster "}

event: delta
data: {"delta":"met them at the jetty"}

event: done
data: {"prose":"The harbourmaster met them at the jetty...","html":"

...

","persisted":true,"model":"openai/gpt-5.4-mini","usage":{"prompt_tokens":1840,"completion_tokens":212,"total_tokens":2052},"estimated_prompt_tokens":1795}

You get one delta event per fragment, then a terminal done (or error) event. An upstream LLM failure returns 502 on the buffered endpoints.

Codex (your story bible)

The discrete lore & character elements the editor's Codex panel manages. Each entry has a category (lore or character), a name, optional aliases, and content — these drive Prosie's mention-aware context, so an external story bible can keep them in sync.

GET /api/stories/{id}/codex-entries
List a book's codex entries.
read
POST /api/stories/{id}/codex-entries
Add an entry.
write
GET /api/codex-entries/{id}
Fetch one entry.
read
PATCH /api/codex-entries/{id}
Update an entry.
write
DELETE /api/codex-entries/{id}
Delete an entry.
write

Series-shared entries and per-book appends use the Series endpoints above.

Settings & export

GET /api/llm-config
Read your provider, model and which keys are stored.
read
PATCH /api/llm-config
Change provider, model or API keys (keys are write-only).
write
GET /api/llm-models
The model catalogue per provider, plus which providers you've keyed.
read
GET /api/rewrite-actions
Every rewrite action (built-ins + yours) with the key the rewrite endpoint accepts.
read
GET /api/rewrite-prompts
List your own rewrite prompts.
read
GET /api/rewrite-prompts/{id}
Fetch one rewrite prompt.
read
POST /api/rewrite-prompts
Create a rewrite prompt.
write
PATCH /api/rewrite-prompts/{id}
Update a rewrite prompt.
write
DELETE /api/rewrite-prompts/{id}
Delete a rewrite prompt.
write
GET /api/scenes/{id}/export
Export one scene as Markdown (or ?format=docx for Word).
read
GET /api/stories/{id}/export
Export a whole story as Markdown (or ?format=docx).
read

Example — read your LLM config

 curl https://prosie.app/api/llm-config \
  -H "Authorization: Bearer <your-token>" \
  -H "Accept: application/json"

Response 200 OK

 {
  "data": {
    "provider": "openai",
    "model": "openai/gpt-5.4-mini",
    "has_openai_key": true,
    "has_anthropic_key": false,
    "has_openrouter_key": false
  }
}