{
  "openapi": "3.1.0",
  "info": {
    "title": "Quicky.Page",
    "version": "1.0.0",
    "summary": "Instant publishing for AI-generated web content.",
    "description": "Quicky.Page turns AI-generated content into one public, shareable web page in a single HTTP call. Send markdown or qp.v1 blocks; receive a public `url`, an `editUrl` for the page owner, and a secret `editKey` for later updates or deletion. Anonymous and rate-limited.",
    "contact": {
      "name": "Quicky.Page",
      "url": "https://quicky.page/docs"
    },
    "license": {
      "name": "MIT",
      "identifier": "MIT"
    }
  },
  "servers": [
    {
      "url": "https://quicky.page",
      "description": "Production"
    }
  ],
  "paths": {
    "/api/v1/publish": {
      "post": {
        "operationId": "publishPage",
        "summary": "Publish a public web page (or update an existing one).",
        "description": "Create or update one public, shareable web page. To create, omit id and editKey. To update, pass the page id and editKey from a previous publish. Prefer markdown in content; title becomes the leading heading when the body does not already start with one. For one-shot agent calls you can also POST a raw markdown body directly with Content-Type: text/markdown.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/PublishRequest" },
              "examples": {
                "markdownCreate": {
                  "summary": "Create a new page from a markdown body (recommended).",
                  "value": {
                    "title": "Launch notes for v0.4",
                    "content": "# What's new\n\n- Public API\n- MCP server\n- Browser extension\n\nA short paragraph with a [link](https://quicky.page).",
                    "theme": "auto-default"
                  }
                },
                "blocksCreate": {
                  "summary": "Create a new page from explicit qp.v1 blocks.",
                  "value": {
                    "title": "My AI Summary",
                    "blocks": [
                      { "type": "richtext", "html": "<h2>Subhead</h2><p>Paragraph with <strong>emphasis</strong>.</p>" },
                      { "type": "image", "url": "https://example.com/diagram.png", "alt": "Architecture diagram" }
                    ]
                  }
                },
                "update": {
                  "summary": "Update a previously-published page (id + editKey required).",
                  "value": {
                    "id": "abc123",
                    "editKey": "REPLACE_WITH_EDIT_KEY_FROM_PREVIOUS_PUBLISH",
                    "content": "# Updated\n\nReplaced body."
                  }
                }
              }
            },
            "text/markdown": {
              "schema": {
                "type": "string",
                "description": "Raw markdown body. Equivalent to POSTing { \"content\": <body> } as JSON. Useful for one-shot curl / agent calls that don't want to JSON-encode their content.",
                "example": "# Hello\n\nA page in one call."
              }
            },
            "text/plain": {
              "schema": {
                "type": "string",
                "description": "Raw text body, parsed as markdown. Same handling as text/markdown.",
                "example": "# Hello\n\nA page in one call."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Page successfully created or updated.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PublishResponse" }
              }
            }
          },
          "400": {
            "description": "Input could not be parsed or no usable content was provided.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "403": {
            "description": "editKey missing or wrong on an update.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "404": {
            "description": "Unknown id on an update.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (30 publishes / 5 minutes / IP). Response includes Retry-After and X-RateLimit-* headers.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "500": {
            "description": "Server error. Retry with backoff.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/v1/pages/{id}": {
      "get": {
        "operationId": "getPage",
        "summary": "Read the content of a published page.",
        "description": "Returns the stored content of a page as JSON. Use this to verify a just-published page, to summarize what is currently live at a URL, or to fetch the blocks before editing them. Read-only; no editKey required.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The page id from the public URL.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Page found.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PageReadResponse" }
              }
            }
          },
          "400": {
            "description": "Missing id parameter.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          },
          "404": {
            "description": "No page exists at this id.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "setPagePublished",
        "summary": "Toggle a page between published and unpublished.",
        "description": "Hide a page from the public render route without losing its content (or re-expose a previously-hidden one). The page record stays intact and editable; unpublished pages 404 at https://quicky.page/{id} but remain readable via this API for the editKey holder.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The page id from the public URL.",
            "schema": { "type": "string" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SetPublishedRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Publish state updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["id", "published"],
                  "properties": {
                    "id": { "type": "string" },
                    "published": { "type": "boolean" }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid body.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "editKey missing or wrong.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Unknown id.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      },
      "delete": {
        "operationId": "deletePage",
        "summary": "Permanently delete a page.",
        "description": "Permanently deletes a page when the caller provides its editKey. This is irreversible: later API reads and public visits return 404. To hide without deleting, use setPagePublished with published false.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The page id from the public URL.",
            "schema": { "type": "string" }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/DeleteRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Page deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["id", "deleted"],
                  "properties": {
                    "id": { "type": "string" },
                    "deleted": { "type": "boolean", "const": true }
                  }
                }
              }
            }
          },
          "400": { "description": "Invalid body.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "editKey missing or wrong.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Unknown id.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    },
    "/api/v1/pages/{id}/blocks": {
      "post": {
        "operationId": "appendPageBlocks",
        "summary": "Append content blocks to an existing page.",
        "description": "Append markdown or qp.v1 blocks to a page using its editKey. Unlike publish update, this preserves the page's existing visibility state.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "Page id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/AppendBlocksRequest" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Blocks appended.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/AppendBlocksResponse" }
              }
            }
          },
          "400": { "description": "Invalid body.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "403": { "description": "editKey missing or wrong.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } },
          "404": { "description": "Unknown id.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } } }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "PublishRequest": {
        "type": "object",
        "description": "Publish (or update) request body. Provide exactly one of `content` or `blocks` for the body. To update an existing page, additionally include both `id` and `editKey` from a previous response.",
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 500,
            "description": "Optional title. When set, becomes the leading <h1> on the page — but only if the body doesn't already open with one (no stacked headings)."
          },
          "content": {
            "type": "string",
            "description": "Markdown body. Recommended for AI agents. Supports headings, paragraphs, lists, bold, italic, inline code, links, standalone images, fenced code blocks, dividers, blockquotes, and GitHub-style callouts. Tables, footnotes, and inline HTML fall through as plain text."
          },
          "blocks": {
            "type": "array",
            "description": "Explicit qp.v1 block array. Use only when you need structural control beyond what markdown gives you (custom HTML, embeds, etc). Each block is sanitized server-side; unsafe HTML, scripts, and bad URLs are dropped before storage.",
            "items": { "$ref": "#/components/schemas/Block" }
          },
          "theme": {
            "$ref": "#/components/schemas/ThemeId",
            "description": "Optional page theme. Defaults to `auto-default` when omitted."
          },
          "meta": {
            "$ref": "#/components/schemas/PageMeta",
            "description": "Optional share metadata (title + description) used for social previews and OG cards. Pass `null` to clear existing meta on an update."
          },
          "id": {
            "type": "string",
            "description": "Page id to UPDATE. Omit to create a new page. Must be paired with `editKey`."
          },
          "editKey": {
            "type": "string",
            "description": "Secret returned by a previous publish call. Required for updates."
          },
          "published": {
            "type": "boolean",
            "description": "Optional initial visibility on create. Defaults to true. Pass false to create an unpublished page that opens in the editor for review before it is public."
          }
        }
      },
      "PublishResponse": {
        "type": "object",
        "required": ["id", "url", "editUrl", "editKey"],
        "properties": {
          "id": {
            "type": "string",
            "description": "Page id. The trailing segment of the public URL when no slug is set."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Public, read-only URL. Anyone with this link can view the page. Safe to share."
          },
          "editUrl": {
            "type": "string",
            "format": "uri",
            "description": "Private edit URL in the exact format `<base>/?id=<id>#edit=<editKey>` — id in the querystring, secret editKey in the URL fragment (the fragment is never sent to the server, so it never appears in access logs or in the Referer header on outbound clicks). Opens the editor in one click and authorizes the holder to edit the page. Treat as a secret: anyone with this URL can edit the page. Surface this to the user when they need to edit later; do not reconstruct it from `editKey` yourself."
          },
          "slug": {
            "type": ["string", "null"],
            "description": "The page's premium custom URL segment, if any. When set, `url` uses /<slug>; otherwise it uses /<id>."
          },
          "editKey": {
            "type": "string",
            "description": "Secret edit key in raw form. Required for programmatic updates (POST /api/v1/publish with id+editKey) and for deletes/renames (PATCH/DELETE /api/v1/pages/{id}). The `editUrl` above contains this same value already; only store `editKey` if you'll call the API directly. There is no recovery if it's lost."
          },
          "published": {
            "type": "boolean",
            "description": "Whether the page is currently live at `url`. Unpublished pages still return `url`, but public reads 404 until the page is published from the editor or pages API."
          }
        }
      },
      "AppendBlocksRequest": {
        "type": "object",
        "required": ["editKey"],
        "properties": {
          "editKey": {
            "type": "string",
            "description": "Secret edit key for the target page."
          },
          "content": {
            "type": "string",
            "description": "Markdown content to append."
          },
          "blocks": {
            "type": "array",
            "description": "Explicit qp.v1 blocks to append.",
            "items": { "$ref": "#/components/schemas/Block" }
          }
        }
      },
      "AppendBlocksResponse": {
        "type": "object",
        "required": ["id", "appended", "published", "updatedAt"],
        "properties": {
          "id": { "type": "string" },
          "appended": { "type": "integer" },
          "published": {
            "type": "boolean",
            "description": "The target page's visibility after append."
          },
          "updatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "PageReadResponse": {
        "type": "object",
        "required": ["id", "content", "published", "theme", "createdAt", "updatedAt"],
        "properties": {
          "id": { "type": "string" },
          "content": {
            "type": "array",
            "description": "qp.v1 blocks currently stored on the page.",
            "items": { "$ref": "#/components/schemas/Block" }
          },
          "published": {
            "type": "boolean",
            "description": "Whether the page renders at its public URL. Unpublished pages 404 publicly but remain readable via this API."
          },
          "theme": { "$ref": "#/components/schemas/ThemeId" },
          "meta": {
            "oneOf": [
              { "$ref": "#/components/schemas/PageMeta" },
              { "type": "null" }
            ]
          },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        }
      },
      "SetPublishedRequest": {
        "type": "object",
        "required": ["editKey", "published"],
        "properties": {
          "editKey": { "type": "string", "description": "Secret edit key from the original publish call." },
          "published": { "type": "boolean", "description": "true to publish (default), false to hide from the public route." }
        }
      },
      "DeleteRequest": {
        "type": "object",
        "required": ["editKey"],
        "properties": {
          "editKey": { "type": "string", "description": "Secret edit key from the original publish call." }
        }
      },
      "PageMeta": {
        "type": "object",
        "description": "Share metadata for social previews. Both fields optional; if neither is provided, the server auto-derives them from the leading heading + first paragraph.",
        "properties": {
          "title": { "type": "string", "maxLength": 200 },
          "description": { "type": "string", "maxLength": 500 }
        }
      },
      "ThemeId": {
        "type": "string",
        "description": "Composite theme id of the form `{mode}-{name}`. `mode` is one of `light`, `dark`, `auto` (auto follows the viewer's system preference). `name` is one of `default` (neutral), `editorial` (warm), `vivid` (saturated).",
        "enum": [
          "auto-default",
          "auto-editorial",
          "auto-vivid",
          "light-default",
          "light-editorial",
          "light-vivid",
          "dark-default",
          "dark-editorial",
          "dark-vivid"
        ],
        "default": "auto-default"
      },
      "Block": {
        "type": "object",
        "description": "One qp.v1 block. Server sanitizes all block content before storage. See https://quicky.page/docs/api for the full schema.",
        "required": ["type"],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "richtext",
              "image",
              "embed",
              "html",
              "callout",
              "quote",
              "divider",
              "timeline",
              "comparison",
              "code"
            ]
          },
          "html": { "type": "string", "description": "Inline HTML for richtext / html / callout / quote blocks." },
          "url": { "type": "string", "format": "uri", "description": "Source URL for image / embed blocks." },
          "alt": { "type": "string", "description": "Alt text for image blocks." },
          "code": { "type": "string", "description": "Raw source text for code blocks. Preserve indentation exactly; the renderer escapes." },
          "language": { "type": "string", "description": "Language token for code blocks (e.g. ts, py, sh, json). Defaults to plaintext when omitted." },
          "filename": { "type": "string", "description": "Optional filename / label rendered above a code block." }
        },
        "additionalProperties": true
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": { "type": "string" }
        }
      }
    }
  }
}
