{"openapi":"3.1.0","info":{"title":"Calliope Writer API","version":"1.0.0","description":"A plain HTTP/JSON API over your own Calliope manuscript. Authenticate with a personal API key (create one at /account/api) sent as `Authorization: Bearer cal_…`.\n\nThree tiers, by what a call costs us:\n- **read** — free. Outline, chapters, search, map, snippets.\n- **ai** — spends your Calliope credits (or your subscription allowance). Detect, check, generate synopsis. No subscription needed.\n- **write** — needs an active Calliope subscription. Snippets, structure, the map, and **your chapter prose**.\n\n**About prose write:** unlike the MCP connector (which is always LLM-driven and may never touch the body), this API can overwrite chapter prose — it's meant for *your own* scripts (import/export round-trips, deterministic transforms), not for an agent to author your book.\n\n**What prose write costs you:** the body is stored as a rich document; plain-text/Markdown writes drop **mention anchors** (map links) and, when a paragraph is rewritten or removed, the **marginalia** anchored to it. Prefer `PATCH …/paragraphs/{blockId}` for targeted edits (it preserves anchors by keeping the paragraph's id). Every prose response reports `mentionsDropped` — re-run `POST …/detect` to re-anchor the map."},"servers":[{"url":"https://localhost:8080/api/v1"}],"security":[{"apiKey":[]}],"components":{"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"Your personal key, e.g. `cal_xxx`. Create one at /account/api."}},"responses":{"Unauthorized":{"description":"Missing or invalid API key."},"SubscriptionRequired":{"description":"This write needs an active Calliope subscription."},"InsufficientCredits":{"description":"Out of AI credits (top up to continue)."},"NotFound":{"description":"Not found, or you don't own it."}}},"paths":{"/books":{"get":{"x-tier":"read","summary":"List your books","responses":{"200":{"description":"Your books (id + title)."}}}},"/books/{bookId}":{"get":{"x-tier":"read","summary":"Get a book's outline + goal + progress","parameters":[{"name":"bookId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Outline: parts, chapters, goal, recent progress."},"404":{"$ref":"#/components/responses/NotFound"}}}},"/books/{bookId}/search":{"get":{"x-tier":"read","summary":"Keyword search across prose + cast (no AI)","parameters":[{"name":"bookId","in":"path","required":true,"schema":{"type":"string"}},{"name":"q","in":"query","required":true,"description":"The search text.","schema":{"type":"string"}},{"name":"limit","in":"query","required":false,"description":"Max hits (default 20, max 50).","schema":{"type":"string"}}],"responses":{"200":{"description":"Matching chapters (excerpts) + facts."}}}},"/books/{bookId}/facts":{"get":{"x-tier":"read","summary":"List the story map (facts / cast)","parameters":[{"name":"bookId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The book's facts."}}},"post":{"x-tier":"write","summary":"Add a fact to the map","parameters":[{"name":"bookId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name"],"properties":{"name":{"type":"string"},"kind":{"type":"string","description":"e.g. character, place."},"aliases":{"type":"array","items":{"type":"string"}},"attributes":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string"},"value":{"type":"string"}}}},"summary":{"type":"string"}}}}}},"responses":{"201":{"description":"Created (returns the new fact id)."},"403":{"$ref":"#/components/responses/SubscriptionRequired"}}}},"/facts/{factId}":{"get":{"x-tier":"read","summary":"Get one fact + its mentions","parameters":[{"name":"factId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The fact + where it's mentioned."}}},"patch":{"x-tier":"write","summary":"Edit a fact","parameters":[{"name":"factId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string"},"kind":{"type":"string"},"aliases":{"type":"array","items":{"type":"string"}},"summary":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated."},"403":{"$ref":"#/components/responses/SubscriptionRequired"}}}},"/facts/merge":{"post":{"x-tier":"write","summary":"Merge two facts (keepId absorbs dropId)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["keepId","dropId"],"properties":{"keepId":{"type":"string"},"dropId":{"type":"string"}}}}}},"responses":{"200":{"description":"Merged."}}}},"/books/{bookId}/snippets":{"get":{"x-tier":"read","summary":"List marginalia (notes/todos/refs/research)","parameters":[{"name":"bookId","in":"path","required":true,"schema":{"type":"string"}},{"name":"chapterId","in":"query","required":false,"description":"Limit to one chapter.","schema":{"type":"string"}}],"responses":{"200":{"description":"The book's snippets."}}},"post":{"x-tier":"write","summary":"Add a snippet (beside the prose, never in it)","parameters":[{"name":"bookId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind"],"properties":{"kind":{"type":"string","enum":["note","reference","todo","research"]},"chapterId":{"type":"string"},"blockId":{"type":"string","description":"Anchor to a paragraph (optional)."},"text":{"type":"string"},"url":{"type":"string"},"items":{"type":"array","description":"For kind=todo.","items":{"type":"object","properties":{"text":{"type":"string"},"done":{"type":"boolean"}}}}}}}}},"responses":{"201":{"description":"Created."}}}},"/snippets/{snippetId}":{"patch":{"x-tier":"write","summary":"Replace a snippet's content","parameters":[{"name":"snippetId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind"],"properties":{"kind":{"type":"string","enum":["note","reference","todo","research"]},"text":{"type":"string"},"url":{"type":"string"}}}}}},"responses":{"200":{"description":"Updated."}}},"delete":{"x-tier":"write","summary":"Delete a snippet","parameters":[{"name":"snippetId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted."}}}},"/chapters/{chapterId}":{"get":{"x-tier":"read","summary":"Read a chapter (prose as text + metadata)","parameters":[{"name":"chapterId","in":"path","required":true,"schema":{"type":"string"}},{"name":"format","in":"query","required":false,"description":"Set to `doc` to get the raw document with paragraph blockIds (needed to target PATCH …/paragraphs).","schema":{"type":"string"}}],"responses":{"200":{"description":"The chapter."}}}},"/chapters/{chapterId}/prose":{"put":{"x-tier":"write","summary":"Replace a chapter's whole prose (your own hand)","description":"Body: `text/markdown` (default) or JSON `{markdown}` or `{doc}` (native document). BlockIds are reconciled so unchanged paragraphs keep their marginalia. Mention marks are dropped (see `mentionsDropped` in the response). Prefer PATCH …/paragraphs/{blockId} for targeted edits.","parameters":[{"name":"chapterId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"text/markdown":{"schema":{"type":"string"}},"application/json":{"schema":{"type":"object","properties":{"markdown":{"type":"string"},"doc":{"type":"object","description":"A ProseMirror document."}}}}}},"responses":{"200":{"description":"Saved. Returns { wordCount, blocks, mentionsDropped }."},"403":{"$ref":"#/components/responses/SubscriptionRequired"}}}},"/chapters/{chapterId}/paragraphs/{blockId}":{"patch":{"x-tier":"write","summary":"Update ONE paragraph (anchor-safe)","description":"The surgical edit: the paragraph's blockId is kept, so its marginalia survives. Find blockIds via GET /chapters/{id}?format=doc.","parameters":[{"name":"chapterId","in":"path","required":true,"schema":{"type":"string"}},{"name":"blockId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"text/markdown":{"schema":{"type":"string"}},"application/json":{"schema":{"type":"object","properties":{"markdown":{"type":"string"}}}}}},"responses":{"200":{"description":"Saved. Returns { wordCount, mentionsDropped }."},"404":{"description":"No paragraph with that blockId (or chapter not yours)."}}}},"/chapters/{chapterId}/status":{"put":{"x-tier":"write","summary":"Set chapter status","parameters":[{"name":"chapterId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["draft","revising","done"]}}}}}},"responses":{"200":{"description":"Updated."}}}},"/chapters/{chapterId}/synopsis":{"post":{"x-tier":"ai","summary":"Generate a synopsis (SPENDS CREDITS)","description":"Returns the text only — it does NOT save it. Use PUT to save.","parameters":[{"name":"chapterId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"{ synopsis }."},"402":{"$ref":"#/components/responses/InsufficientCredits"}}},"put":{"x-tier":"write","summary":"Save the synopsis field (never prose)","parameters":[{"name":"chapterId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["synopsis"],"properties":{"synopsis":{"type":"string"}}}}}},"responses":{"200":{"description":"Saved."}}}},"/chapters/{chapterId}/detect":{"post":{"x-tier":"ai","summary":"Detect entities (SPENDS CREDITS)","description":"Proposes map additions. Writes nothing.","parameters":[{"name":"chapterId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Detection proposals."},"402":{"$ref":"#/components/responses/InsufficientCredits"}}}},"/chapters/{chapterId}/check":{"post":{"x-tier":"ai","summary":"Consistency check (SPENDS CREDITS)","description":"Surfaces contradictions + name drift. Writes nothing.","parameters":[{"name":"chapterId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Consistency findings."},"402":{"$ref":"#/components/responses/InsufficientCredits"}}}}}}