{"openapi":"3.1.0","info":{"title":"Sunflare CMS API","version":"0.1.0","description":"Public REST API for managing a Sunflare photographer CMS. Authenticate with an API key created in Settings → Developer → API Keys. Errors follow RFC 7807 problem+json."},"servers":[{"url":"https://sc-api.sunflare.app"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Bearer sk_live_{keyId}_{secret}"}},"schemas":{"Problem":{"type":"object","required":["type","title","status"],"properties":{"type":{"type":"string","format":"uri"},"title":{"type":"string"},"status":{"type":"integer"},"detail":{"type":"string"},"instance":{"type":"string"},"requestId":{"type":"string"}}},"PageSummary":{"type":"object","properties":{"id":{"type":["string","null"]},"path":{"type":["string","null"]},"name":{"type":["string","null"]},"title":{"type":["string","null"]},"seo":{"type":["object","null"],"additionalProperties":true},"blockCount":{"type":"integer"}}},"Page":{"type":"object","additionalProperties":true,"description":"Full page payload including rows and blocks. Shape follows the CMS block schema."},"TranslatableContent":{"type":"object","required":["path","title","seo","blocks"],"properties":{"path":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"seo":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string"},"keywords":{"type":"string"}}},"blocks":{"type":"object","additionalProperties":{"type":"object","required":["type","fields"],"properties":{"type":{"type":"string","description":"Block type (e.g. title-text, quote, text-image)"},"fields":{"type":"object","additionalProperties":true,"description":"Translatable field values"}}}}}},"VersionSummary":{"type":"object","properties":{"id":{"type":"string"},"versionNumber":{"type":"integer"},"createdAt":{"type":"integer","description":"Unix ms"},"createdBy":{"type":"string"},"label":{"type":["string","null"]},"triggerType":{"type":["string","null"]},"role":{"type":"string","enum":["draft","snapshot","published"]},"publishedAt":{"type":["integer","null"]},"changesSummary":{"type":["string","null"]}}},"Version":{"allOf":[{"$ref":"#/components/schemas/VersionSummary"},{"type":"object","properties":{"theme":{"type":"object","additionalProperties":true},"settings":{"type":"object","additionalProperties":true},"pages":{"type":"array","items":{"$ref":"#/components/schemas/Page"}}}}]},"ImageSummary":{"type":"object","properties":{"id":{"type":"string"},"filename":{"type":"string"},"galleryId":{"type":"string"},"alt":{"type":["string","null"]},"altGeneratedAt":{"type":["integer","null"]},"width":{"type":["number","null"]},"height":{"type":["number","null"]},"urlPath":{"type":"string"},"uploadedAt":{"type":"integer"},"order":{"type":"integer"}}},"Image":{"allOf":[{"$ref":"#/components/schemas/ImageSummary"},{"type":"object","properties":{"mimeType":{"type":"string"},"size":{"type":"integer"},"metadata":{"type":"object","additionalProperties":true}}}]},"WhoAmI":{"type":"object","properties":{"accountId":{"type":"string"},"keyId":{"type":"string"},"scopes":{"type":"array","items":{"enum":["cms:read","cms:write","cms:write:blocks","cms:write:structure","cms:publish","test:otp"]}}}},"Position":{"description":"Target slot for an insert or move. Exactly one of after/before/appendTo/prependTo.","oneOf":[{"type":"object","required":["after"],"properties":{"after":{"type":"string","description":"Block id to insert/move after"}}},{"type":"object","required":["before"],"properties":{"before":{"type":"string","description":"Block id to insert/move before"}}},{"type":"object","required":["appendTo"],"properties":{"appendTo":{"type":"string","description":"Row id; append the block as the last block in that row."}}},{"type":"object","required":["prependTo"],"properties":{"prependTo":{"type":"string","description":"Row id; insert the block as the first block in that row."}}}]},"RowPosition":{"description":"Target slot for a row-level `insert-row` op. Exactly one of afterRow/beforeRow/atStart/atEnd. `afterRow`/`beforeRow` take an existing row id; `atStart`/`atEnd` take literal true.","oneOf":[{"type":"object","required":["afterRow"],"properties":{"afterRow":{"type":"string","description":"Row id to insert after."}}},{"type":"object","required":["beforeRow"],"properties":{"beforeRow":{"type":"string","description":"Row id to insert before."}}},{"type":"object","required":["atStart"],"properties":{"atStart":{"type":"boolean","enum":[true]}}},{"type":"object","required":["atEnd"],"properties":{"atEnd":{"type":"boolean","enum":[true]}}}]},"NewRow":{"type":"object","required":["columns"],"description":"Config for a freshly created row. `columns` is bounded to [1, 6]. `style` and `gap` are optional and stored as-is.","properties":{"columns":{"type":"integer","minimum":1,"maximum":6},"style":{"type":"string","description":"e.g. container, full-width, sidebar"},"gap":{"type":"integer","minimum":0,"maximum":16}}},"NewBlock":{"type":"object","required":["type","settings"],"properties":{"type":{"type":"string","description":"Block type, e.g. title-text, quote, text-image"},"settings":{"type":"object","additionalProperties":true},"position":{"type":"object","description":"Optional explicit stored position {column, row}. When omitted, the server auto-calculates column from the insert slot and rejects with `row-full` if no column is free. Pass this when you need exact column control (e.g. multi-column rows).","required":["column"],"properties":{"column":{"type":"integer","minimum":0},"row":{"type":"integer","minimum":0}}}}},"Operation":{"description":"One structural operation against the page tree. Discriminated by `op`.","oneOf":[{"type":"object","required":["op","blockId","fields"],"properties":{"op":{"type":"string","enum":["set"]},"blockId":{"type":"string"},"fields":{"type":"object","additionalProperties":true,"description":"Partial field map to merge into the block settings."}}},{"type":"object","required":["op","block","position"],"properties":{"op":{"type":"string","enum":["insert"]},"block":{"$ref":"#/components/schemas/NewBlock"},"position":{"$ref":"#/components/schemas/Position"}}},{"type":"object","required":["op","blockId","position"],"properties":{"op":{"type":"string","enum":["move"]},"blockId":{"type":"string"},"position":{"$ref":"#/components/schemas/Position"}}},{"type":"object","required":["op","blockId"],"properties":{"op":{"type":"string","enum":["delete"]},"blockId":{"type":"string"}}},{"type":"object","required":["op","blockId","with"],"properties":{"op":{"type":"string","enum":["replace"]},"blockId":{"type":"string"},"with":{"$ref":"#/components/schemas/NewBlock"}}},{"type":"object","required":["op","row","position"],"description":"Create a brand-new row in the page block tree. Optionally seed it with starting blocks. Each seed block gets `position = { column: <its array index>, row: 0 }`. Use this when stacking content vertically (the renderer is 1-block-per-column, so each stacked element needs its own row).","properties":{"op":{"type":"string","enum":["insert-row"]},"row":{"$ref":"#/components/schemas/NewRow"},"position":{"$ref":"#/components/schemas/RowPosition"},"blocks":{"type":"array","description":"Optional starting blocks for the new row. `blocks.length` must be ≤ `row.columns`; violations return `seed-overflow`.","items":{"$ref":"#/components/schemas/NewBlock"}}}},{"type":"object","required":["op","rowId"],"description":"Delete an entire row from the page block tree. Every block nested under the row is removed with it; their ids show up in the diff as `removedBlockIds` and trigger the same translation-overlay cleanup as a block-level `delete`.","properties":{"op":{"type":"string","enum":["delete-row"]},"rowId":{"type":"string"}}}],"discriminator":{"propertyName":"op"}},"DiffEntry":{"description":"One applied (or proposed) change in the response diff.","oneOf":[{"type":"object","required":["op","blockId","changedFields"],"properties":{"op":{"type":"string","enum":["set"]},"blockId":{"type":"string"},"changedFields":{"type":"array","items":{"type":"string"}}}},{"type":"object","required":["op","newBlockId","position"],"properties":{"op":{"type":"string","enum":["insert"]},"newBlockId":{"type":"string"},"position":{"$ref":"#/components/schemas/Position"}}},{"type":"object","required":["op","blockId","position"],"properties":{"op":{"type":"string","enum":["move"]},"blockId":{"type":"string"},"position":{"$ref":"#/components/schemas/Position"}}},{"type":"object","required":["op","blockId"],"properties":{"op":{"type":"string","enum":["delete"]},"blockId":{"type":"string"}}},{"type":"object","required":["op","oldBlockId","newBlockId"],"properties":{"op":{"type":"string","enum":["replace"]},"oldBlockId":{"type":"string"},"newBlockId":{"type":"string"}}},{"type":"object","required":["op","newRowId","position","newBlockIds"],"properties":{"op":{"type":"string","enum":["insert-row"]},"newRowId":{"type":"string"},"position":{"$ref":"#/components/schemas/RowPosition"},"newBlockIds":{"type":"array","description":"Ids of seeded blocks in `NewRow.blocks` order; empty when no seed.","items":{"type":"string"}}}},{"type":"object","required":["op","rowId","removedBlockIds"],"properties":{"op":{"type":"string","enum":["delete-row"]},"rowId":{"type":"string"},"removedBlockIds":{"type":"array","description":"Ids of blocks that were nested under the deleted row and removed with it.","items":{"type":"string"}}}}],"discriminator":{"propertyName":"op"}},"ApplyError":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"},"opIndex":{"type":"integer"},"blockId":{"type":"string"},"field":{"type":"string"}}},"ValidationError":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string"},"message":{"type":"string"},"blockId":{"type":"string"},"field":{"type":"string"}}},"MutateResponse":{"type":"object","required":["dryRun","snapshotVersion","diff","staleTranslationKeys"],"properties":{"dryRun":{"type":"boolean"},"snapshotVersion":{"type":["integer","null"],"description":"New page snapshot version after apply. Null on dryRun."},"diff":{"type":"array","items":{"$ref":"#/components/schemas/DiffEntry"}},"staleTranslationKeys":{"type":"array","items":{"type":"string"},"description":"Translation overlay keys (blockId or blockId__itemId) invalidated by this batch."}}}},"parameters":{"Cursor":{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque pagination cursor from a previous response"},"Limit":{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}},"responses":{"Unauthorized":{"description":"Missing or invalid API key","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"Forbidden":{"description":"Key lacks the required scope","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"NotFound":{"description":"Resource does not exist","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"paths":{"/v1/whoami":{"get":{"summary":"Return the account and scopes bound to the current API key","tags":["Meta"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhoAmI"}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/pages":{"get":{"summary":"List pages in the current draft (falls back to published)","tags":["Pages"],"parameters":[{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/PageSummary"}},"nextCursor":{"type":["string","null"]},"total":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}},"post":{"summary":"Create a new page in the draft","description":"Creates a page at the given path. Fails with 409 if the path already exists. Auto-snapshots the prior draft state when a new editing window begins.","tags":["Pages"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["path","title"],"properties":{"path":{"type":"string","description":"Page path, leading slash optional"},"title":{"type":"string"},"description":{"type":"string"},"layout":{"type":"string","enum":["default","full-width","sidebar"]},"order":{"type":"integer"},"settings":{"type":"object","additionalProperties":true},"rows":{"type":"array","items":{"type":"object","additionalProperties":true}},"ogImagePhotoId":{"type":"string"},"ogImageAlt":{"type":"string"}}}}}},"responses":{"201":{"description":"Created","headers":{"X-Sunflare-Snapshot-Version":{"schema":{"type":"integer"},"description":"Set when an auto-snapshot was created"}},"content":{"application/json":{"schema":{"type":"object","properties":{"page":{"$ref":"#/components/schemas/Page"},"snapshotVersion":{"type":["integer","null"]}}}}}},"400":{"$ref":"#/components/responses/NotFound"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"409":{"description":"Page path already exists","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/pages/{pageId}":{"get":{"summary":"Get the full content of a single page","tags":["Pages"],"parameters":[{"name":"pageId","in":"path","required":true,"schema":{"type":"string"},"description":"Page path (URL-encoded, e.g. %2F for the root page)"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Page"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"summary":"Update non-SEO page metadata (path/title/description/layout/order/settings)","description":"Body keys are optional — send only what you want to change. `path` in the body renames the page. SEO-owned setting keys (seoTitle, seoDescription, keywords, robots, ogImage, sitemapMode) are ignored — use the /seo endpoint. `rows` (block content) is REJECTED with 400 `rows-not-allowed`; use POST /v1/pages/{pageId}/mutate to edit block trees.","tags":["Pages"],"parameters":[{"name":"pageId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"path":{"type":"string"},"title":{"type":"string"},"description":{"type":"string"},"layout":{"type":"string"},"order":{"type":"integer"},"settings":{"type":"object","additionalProperties":true},"ogImagePhotoId":{"type":"string"},"ogImageAlt":{"type":"string"}}}}}},"responses":{"200":{"description":"OK","headers":{"X-Sunflare-Snapshot-Version":{"schema":{"type":"integer"},"description":"Version number of the auto-snapshot, if one was created"}},"content":{"application/json":{"schema":{"type":"object","properties":{"page":{"$ref":"#/components/schemas/Page"},"snapshotVersion":{"type":["integer","null"]}}}}}},"400":{"description":"Invalid body — includes `rows-not-allowed` when the caller sends a `rows` key","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"Target path already exists","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}},"delete":{"summary":"Delete a page from the draft","description":"Hard delete. An auto-snapshot is taken before mutation — restore via POST /v1/versions/{n}/restore using the returned snapshotVersion.","tags":["Pages"],"parameters":[{"name":"pageId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted","headers":{"X-Sunflare-Snapshot-Version":{"schema":{"type":"integer"},"description":"Version number of the auto-snapshot, if one was created"}},"content":{"application/json":{"schema":{"type":"object","properties":{"deletedPath":{"type":"string"},"snapshotVersion":{"type":["integer","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/pages/{pageId}/mutate":{"post":{"summary":"Apply a batch of structural operations to a page","description":"The only way to edit block content (or the block tree) via the API. Operations apply atomically: either every op succeeds or none do. Run with `dryRun: true` first to preview the diff and any validation errors before committing. Concurrency control via `ifMatch` — pass the snapshot version you last saw; mismatch returns 409 `stale-version`. Required header `Idempotency-Key` — same key+body within 24h returns the cached response; same key+different body returns 422 `idempotency-mismatch`. Required scope is `cms:write:blocks` when the batch is `set`-only, or `cms:write:structure` when any op is `insert`/`move`/`delete`/`replace`.","tags":["Pages"],"parameters":[{"name":"pageId","in":"path","required":true,"schema":{"type":"string"},"description":"Page id or URL-encoded path"},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string"},"description":"Client-chosen UUID-ish token for safe retries"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["ifMatch","operations"],"properties":{"ifMatch":{"type":"integer","description":"Last snapshot version the client saw. Server returns 409 stale-version on mismatch."},"dryRun":{"type":"boolean","default":false,"description":"When true, validate and return the would-be diff without writing or bumping snapshotVersion."},"operations":{"type":"array","minItems":1,"items":{"$ref":"#/components/schemas/Operation"}}}}}}},"responses":{"200":{"description":"Batch applied (or, on dryRun, would have applied). The diff describes every change in order.","headers":{"X-Sunflare-Snapshot-Version":{"schema":{"type":"integer"},"description":"New page snapshot version after apply. Omitted (or null in body) on dryRun."},"X-Idempotent-Replay":{"schema":{"type":"string","enum":["true"]},"description":"Present when this response is a cached replay."}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/MutateResponse"}}}},"400":{"description":"Invalid body (missing ifMatch, missing operations, missing Idempotency-Key, invalid JSON)","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Key lacks the required scope (`cms:write:blocks` for set-only batches, `cms:write:structure` otherwise). Body includes `required_scope`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"$ref":"#/components/responses/NotFound"},"409":{"description":"`stale-version` — `ifMatch` did not match the current snapshot version. Body includes `currentVersion`.","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"422":{"description":"Operation validation failed. `code` is one of `invalid-operations-shape`, `invalid-operations`, `invalid-block-tree`, `invalid-reference`, or `idempotency-mismatch`. `errors` lists structured per-op (ApplyError) or per-block (ValidationError) failures. ApplyError `code` values include `unknown-block-id`, `unknown-position-target`, `unknown-field`, `unknown-block-type`, `duplicate-result-id`, `invalid-op`, `row-full` (insert/move into a row whose `columns` slot is taken), `position-out-of-range` (explicit `NewBlock.position.column` outside `[0, row.columns)`), `unknown-row-id` (`insert-row` afterRow/beforeRow target missing), `invalid-row-columns` (`insert-row` row.columns outside [1, 6]), and `seed-overflow` (`insert-row` blocks.length > row.columns).","content":{"application/problem+json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Problem"},{"type":"object","properties":{"errors":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/ApplyError"},{"$ref":"#/components/schemas/ValidationError"}]}}}}]}}}}}}},"/v1/pages/bulk-seo":{"post":{"summary":"Apply many SEO patches atomically","description":"Accepts an array of page SEO patches. All-or-nothing: if any page is not found, the whole batch aborts. Supports the optional Idempotency-Key header — same key + same body within 24h returns the cached response with X-Idempotent-Replay: true; same key + different body returns 422.","tags":["Pages"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"},"description":"Client-chosen UUID-ish token for safe retries"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"type":"object","required":["path"],"properties":{"path":{"type":"string"},"seoTitle":{"type":"string"},"seoDescription":{"type":"string"},"keywords":{"type":"string"},"robots":{"type":"string"}}}}}}}}},"responses":{"200":{"description":"OK","headers":{"X-Sunflare-Snapshot-Version":{"schema":{"type":"integer"},"description":"Set when an auto-snapshot was created"},"X-Idempotent-Replay":{"schema":{"type":"string","enum":["true"]},"description":"Present when this response is a cached replay"}},"content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"snapshotVersion":{"type":["integer","null"]},"updated":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"seo":{"type":"object","properties":{"title":{"type":["string","null"]},"description":{"type":["string","null"]},"keywords":{"type":["string","null"]},"robots":{"type":["string","null"]}}}}}}}}}}},"400":{"description":"Invalid body","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"Idempotency-Key reused with a different body","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/pages/{pageId}/translatable":{"get":{"operationId":"getPageTranslatable","summary":"Get translatable content from a page","description":"Returns only the translatable text fields from a page's blocks, structured for easy translation. Response blocks use composite keys (blockId__itemId) for array items like FAQs and testimonials. Strip the 'type' field when writing back as a PageTranslationOverlay.","tags":["Pages"],"parameters":[{"name":"pageId","in":"path","required":true,"schema":{"type":"string"},"description":"URL-encoded page path (e.g. %2F for root, %2Fembaras for /embaras)"}],"responses":{"200":{"description":"Translatable content extracted from the page","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TranslatableContent"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/pages/{pageId}/seo":{"patch":{"summary":"Update SEO fields on a single page","description":"All fields are optional — send only the ones you want to change. Writes target the draft; auto-creates a snapshot once per 15-minute editing window and returns the version number in X-Sunflare-Snapshot-Version when it does.","tags":["Pages"],"parameters":[{"name":"pageId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"seoTitle":{"type":"string"},"seoDescription":{"type":"string"},"keywords":{"type":"string"},"robots":{"type":"string"}}}}}},"responses":{"200":{"description":"OK","headers":{"X-Sunflare-Snapshot-Version":{"schema":{"type":"integer"},"description":"Version number of the auto-snapshot, if one was created"}},"content":{"application/json":{"schema":{"type":"object","properties":{"path":{"type":"string"},"seo":{"type":"object","properties":{"title":{"type":["string","null"]},"description":{"type":["string","null"]},"keywords":{"type":["string","null"]},"robots":{"type":["string","null"]}}},"snapshotVersion":{"type":["integer","null"]}}}}}},"400":{"description":"Invalid JSON body","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/versions":{"get":{"summary":"List versions (newest first)","tags":["Versions"],"parameters":[{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Limit"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/VersionSummary"}},"nextCursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/images":{"get":{"summary":"List photos for the account","tags":["Images"],"parameters":[{"$ref":"#/components/parameters/Cursor"},{"$ref":"#/components/parameters/Limit"},{"name":"missingAlt","in":"query","schema":{"type":"boolean"},"description":"If true, only return images without alt text"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/ImageSummary"}},"nextCursor":{"type":["string","null"]}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/images/{imageId}":{"get":{"summary":"Get a single photo","tags":["Images"],"parameters":[{"name":"imageId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Image"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/images/{imageId}/alt":{"patch":{"summary":"Set alt text on a photo","description":"Writes alt text and stamps altGeneratedAt. Photos are not version-tracked, so no snapshot is created.","tags":["Images"],"parameters":[{"name":"imageId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["alt"],"properties":{"alt":{"type":"string"}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"alt":{"type":"string"}}}}}},"400":{"description":"Invalid body","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/images/bulk-alt":{"post":{"summary":"Apply many alt-text updates atomically","description":"All-or-nothing: if any image is not found the whole batch aborts. Supports Idempotency-Key.","tags":["Images"],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","items":{"type":"object","required":["imageId","alt"],"properties":{"imageId":{"type":"string"},"alt":{"type":"string"}}}}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"count":{"type":"integer"},"updated":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"alt":{"type":"string"}}}}}}}}},"400":{"description":"Invalid body","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"description":"Idempotency-Key reused with a different body","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/locales":{"get":{"summary":"Return configured primary + enabled languages for the account","tags":["i18n"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"primary":{"type":"string"},"enabled":{"type":"array","items":{"type":"string"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/blocks/schema":{"get":{"summary":"CMS block registry: schema + translatable subset per block type (live).","description":"Returns the authoritative authoring contract for every CMS block type. Consumed by the sunflare-cms agent skill and any third-party client. Each block exposes its description, translatable field list, and a JSON Schema (Draft 2020-12) generated from the underlying Zod spec. Use settingsSchema.properties to discover writable fields before calling POST /v1/pages/{pageId}/mutate.","tags":["cms"],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"blocks":{"type":"object","additionalProperties":{"type":"object","properties":{"description":{"type":["string","null"]},"translatable":{"type":"array","items":{"type":"string"}},"settingsSchema":{"type":"object"}},"required":["description","translatable","settingsSchema"]}}},"required":["blocks"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/translations":{"get":{"summary":"List page-translation overlays for one language at the latest version","tags":["i18n"],"parameters":[{"name":"language","in":"query","required":true,"schema":{"type":"string"},"description":"Language code, e.g. es, ca, en"}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"versionNumber":{"type":["integer","null"]},"language":{"type":"string"},"updatedAt":{"type":"integer"},"pages":{"type":"array","items":{"type":"object","properties":{"path":{"type":"string"},"overlay":{"type":"object","additionalProperties":true},"staleness":{"type":["object","null"],"additionalProperties":true}}}}}}}}},"400":{"description":"Missing language parameter","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/translations/{language}/{pagePath}":{"get":{"summary":"Get a single page translation overlay","tags":["i18n"],"parameters":[{"name":"language","in":"path","required":true,"schema":{"type":"string"}},{"name":"pagePath","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"versionNumber":{"type":"integer"},"language":{"type":"string"},"path":{"type":"string"},"overlay":{"type":"object","additionalProperties":true},"staleness":{"type":["object","null"],"additionalProperties":true},"updatedAt":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}},"patch":{"summary":"Upsert a page translation overlay","description":"Writes against the latest version. Existing overlays for the same language+path are replaced; other pages are untouched. Optional staleness payload mirrors the CMS block-level staleness map.","tags":["i18n"],"parameters":[{"name":"language","in":"path","required":true,"schema":{"type":"string"}},{"name":"pagePath","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["overlay"],"properties":{"overlay":{"type":"object","additionalProperties":true},"staleness":{"type":"object","additionalProperties":true}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"versionNumber":{"type":"integer"},"language":{"type":"string"},"path":{"type":"string"}}}}}},"400":{"description":"Invalid body","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/v1/versions/snapshot":{"post":{"summary":"Manually snapshot current state as a new version","description":"Creates a labeled version row from the current draft (or published if no draft exists). Use before risky bulk edits; complements the automatic 15-min debounced snapshots.","tags":["Versions"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"label":{"type":"string"},"changesSummary":{"type":"string"}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"versionNumber":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"500":{"description":"No site available to snapshot","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}}}}},"/v1/versions/publish":{"post":{"summary":"Publish draft to live (requires cms:publish)","description":"Marks a version as published and unpublishes the previous one; translations are promoted alongside. A fresh draft is seeded from the just-published state so subsequent edits land on a new row. Omit versionNumber to publish the latest draft.","tags":["Versions"],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"versionNumber":{"type":"integer"}}}}}},"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"publishedVersion":{"type":"integer"},"newDraftVersion":{"type":"integer"},"publishedAt":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Key lacks cms:publish scope","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/versions/{versionNumber}/restore":{"post":{"summary":"Restore a previous version into a fresh draft (requires cms:publish)","description":"Creates a new draft version containing the theme, settings, pages, and translations of the target version. Does not publish; call /v1/versions/publish afterwards to go live.","tags":["Versions"],"parameters":[{"name":"versionNumber","in":"path","required":true,"schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"versionNumber":{"type":"integer"},"restoredFrom":{"type":"integer"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"description":"Key lacks cms:publish scope","content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/Problem"}}}},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/versions/{versionNumber}":{"get":{"summary":"Get a version by its version number","tags":["Versions"],"parameters":[{"name":"versionNumber","in":"path","required":true,"schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Version"}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"$ref":"#/components/responses/NotFound"}}}}},"tags":[{"name":"Meta","description":"Identity and capability discovery"},{"name":"Pages","description":"CMS page read and SEO operations"},{"name":"Versions","description":"Version history access"},{"name":"Images","description":"Photo library and SEO (alt text)"},{"name":"i18n","description":"Locales and translation overlays"}]}