{
  "openapi": "3.1.0",
  "info": {
    "title": "Flatpage API",
    "version": "1.0.0",
    "description": "Publish, update, list and delete Flatpage sites from scripts, CI and AI assistants. Every plan, Free included.",
    "contact": {
      "url": "https://flatpage.app/docs"
    }
  },
  "servers": [
    {
      "url": "https://flatpage.app/v1"
    }
  ],
  "security": [
    {
      "apiKey": []
    }
  ],
  "tags": [
    {
      "name": "Account",
      "description": "Your plan and usage."
    },
    {
      "name": "Sites",
      "description": "Publish, update, rename and delete sites, and read their views."
    },
    {
      "name": "Link names",
      "description": "The part before `.flatpage.page`."
    },
    {
      "name": "Uploads",
      "description": "Poll a publish, and the two-step upload for files over 25 MB."
    }
  ],
  "paths": {
    "/me": {
      "get": {
        "operationId": "getAccount",
        "tags": [
          "Account"
        ],
        "summary": "Get your account",
        "description": "Your email, plan, and what you've used of it today. Handy as a first call to check a key works.",
        "responses": {
          "200": {
            "description": "Your account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccountInfo"
                }
              }
            }
          },
          "401": {
            "description": "Missing, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email not verified, or over your plan's site limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit: 200 calls per key per day, or your plan's uploads per day. `Retry-After` says when to try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/sites": {
      "get": {
        "operationId": "listSites",
        "tags": [
          "Sites"
        ],
        "summary": "List sites",
        "description": "All your sites, most recently updated first, with links and all-time views.",
        "responses": {
          "200": {
            "description": "Your sites.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/Site"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email not verified, or over your plan's site limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit: 200 calls per key per day, or your plan's uploads per day. `Retry-After` says when to try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "publishSite",
        "tags": [
          "Sites"
        ],
        "summary": "Publish a new site",
        "description": "Publish an HTML page, a zipped static site (with `index.html` at the top level) or a PDF as a new site. The call waits up to about 20 seconds for scanning and publishing, so it usually answers `201` with the live URL.",
        "parameters": [
          {
            "name": "linkName",
            "in": "query",
            "required": false,
            "description": "Link name for the new site: 3 to 63 lowercase letters, digits and hyphens. Leave it out for a random name.",
            "schema": {
              "type": "string"
            },
            "example": "spring-menu"
          },
          {
            "name": "filename",
            "in": "query",
            "required": false,
            "description": "Only needed when the Content-Type doesn't say what the file is (for example `application/octet-stream`): the extension (.html, .zip, .pdf) decides.",
            "schema": {
              "type": "string"
            },
            "example": "site.zip"
          }
        ],
        "requestBody": {
          "required": true,
          "description": "The page or file. Send the raw file with its Content-Type, or JSON with an `html` field (an AI chat reply is fine: a ```html fence and text around it are stripped). Up to 25 MB; bigger files use the two-step upload.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublishJson"
              },
              "example": {
                "html": "<!doctype html><title>Spring menu</title><h1>Spring menu</h1>",
                "linkName": "spring-menu"
              }
            },
            "text/html": {
              "schema": {
                "type": "string"
              },
              "example": "<!doctype html><title>Spring menu</title><h1>Spring menu</h1>"
            },
            "application/zip": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "application/pdf": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Published. `site.url` is live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublishResult"
                }
              }
            }
          },
          "202": {
            "description": "Still processing after about 20 seconds. Poll `GET /uploads/{uploadId}` every couple of seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublishResult"
                }
              }
            }
          },
          "400": {
            "description": "Bad input, such as an invalid link name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email not verified, or over your plan's site limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "That link name is taken.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The file is bigger than your plan allows, or over 25 MB for a direct publish.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "The body isn't HTML, a zip, a PDF or JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The upload was rejected, for example a zip without index.html or a file the virus scanner flagged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit: 200 calls per key per day, or your plan's uploads per day. `Retry-After` says when to try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/sites/{site}": {
      "parameters": [
        {
          "name": "site",
          "in": "path",
          "required": true,
          "description": "The site's id, or its link name (the part before `.flatpage.page`).",
          "schema": {
            "type": "string"
          },
          "example": "spring-menu"
        }
      ],
      "get": {
        "operationId": "getSite",
        "tags": [
          "Sites"
        ],
        "summary": "Get a site",
        "responses": {
          "200": {
            "description": "The site.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Site"
                }
              }
            }
          },
          "401": {
            "description": "Missing, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email not verified, or over your plan's site limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such site or upload on your account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit: 200 calls per key per day, or your plan's uploads per day. `Retry-After` says when to try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "operationId": "updateSite",
        "tags": [
          "Sites"
        ],
        "summary": "Update a site",
        "description": "Replace a site's content with a new version. The link stays the same, so anything already shared (a QR code, an email) shows the update. Same body as publishing; `linkName` isn't allowed here (rename with `PUT /sites/{site}/link-name`).",
        "parameters": [
          {
            "name": "filename",
            "in": "query",
            "required": false,
            "description": "Only needed when the Content-Type doesn't say what the file is (for example `application/octet-stream`): the extension (.html, .zip, .pdf) decides.",
            "schema": {
              "type": "string"
            },
            "example": "site.zip"
          }
        ],
        "requestBody": {
          "required": true,
          "description": "Same as publishing: the raw file with its Content-Type, or JSON with an `html` field.",
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublishJson"
              },
              "example": {
                "html": "<!doctype html><title>Spring menu</title><h1>Spring menu, updated</h1>"
              }
            },
            "text/html": {
              "schema": {
                "type": "string"
              },
              "example": "<!doctype html><title>Spring menu</title><h1>Spring menu</h1>"
            },
            "application/zip": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "application/pdf": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The new version is live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublishResult"
                }
              }
            }
          },
          "202": {
            "description": "Still processing after about 20 seconds. Poll `GET /uploads/{uploadId}` every couple of seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublishResult"
                }
              }
            }
          },
          "400": {
            "description": "Bad input, such as an invalid link name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email not verified, or over your plan's site limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such site or upload on your account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The file is bigger than your plan allows, or over 25 MB for a direct publish.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "415": {
            "description": "The body isn't HTML, a zip, a PDF or JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The upload was rejected, for example a zip without index.html or a file the virus scanner flagged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit: 200 calls per key per day, or your plan's uploads per day. `Retry-After` says when to try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteSite",
        "tags": [
          "Sites"
        ],
        "summary": "Delete a site",
        "description": "Take a site offline and delete it. The link stops working at once.",
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "401": {
            "description": "Missing, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email not verified, or over your plan's site limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such site or upload on your account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit: 200 calls per key per day, or your plan's uploads per day. `Retry-After` says when to try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/sites/{site}/link-name": {
      "parameters": [
        {
          "name": "site",
          "in": "path",
          "required": true,
          "description": "The site's id, or its link name (the part before `.flatpage.page`).",
          "schema": {
            "type": "string"
          },
          "example": "spring-menu"
        }
      ],
      "put": {
        "operationId": "renameSite",
        "tags": [
          "Sites"
        ],
        "summary": "Rename a site",
        "description": "Change a site's link name. The old link redirects to the new one for 30 days. On the Free plan a site can be renamed once every 30 days.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RenameSiteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Renamed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RenameSiteResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad input, such as an invalid link name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email not verified, or over your plan's site limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such site or upload on your account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "That link name is taken.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit: 200 calls per key per day, or your plan's uploads per day. `Retry-After` says when to try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/sites/{site}/stats": {
      "parameters": [
        {
          "name": "site",
          "in": "path",
          "required": true,
          "description": "The site's id, or its link name (the part before `.flatpage.page`).",
          "schema": {
            "type": "string"
          },
          "example": "spring-menu"
        }
      ],
      "get": {
        "operationId": "getSiteStats",
        "tags": [
          "Sites"
        ],
        "summary": "Get site views",
        "description": "Views per day for the last 30 days (UTC, oldest first), this month and all time. Counts lag real time by up to a minute.",
        "responses": {
          "200": {
            "description": "The site's views.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SiteStats"
                }
              }
            }
          },
          "401": {
            "description": "Missing, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email not verified, or over your plan's site limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such site or upload on your account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit: 200 calls per key per day, or your plan's uploads per day. `Retry-After` says when to try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/link-names/{name}": {
      "get": {
        "operationId": "checkLinkName",
        "tags": [
          "Link names"
        ],
        "summary": "Check a link name",
        "description": "Whether a link name is valid and free, and the URL it would get.",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "example": "spring-menu"
          }
        ],
        "responses": {
          "200": {
            "description": "The check.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LinkNameCheck"
                }
              }
            }
          },
          "401": {
            "description": "Missing, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email not verified, or over your plan's site limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit: 200 calls per key per day, or your plan's uploads per day. `Retry-After` says when to try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/uploads": {
      "post": {
        "operationId": "createUpload",
        "tags": [
          "Uploads"
        ],
        "summary": "Start a big upload",
        "description": "Step 1 of the two-step upload for files over 25 MB (PDFs on Plus go up to 500 MB). Answers a signed `putUrl`: PUT the file there with exactly the `contentType` given (step 2), then call `POST /uploads/{uploadId}/complete` (step 3). The URL expires after 15 minutes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateUploadRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Upload created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateUploadResponse"
                }
              }
            }
          },
          "400": {
            "description": "Bad input, such as an invalid link name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email not verified, or over your plan's site limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such site or upload on your account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "That link name is taken.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "The file is bigger than your plan allows, or over 25 MB for a direct publish.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit: 200 calls per key per day, or your plan's uploads per day. `Retry-After` says when to try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/uploads/{uploadId}": {
      "parameters": [
        {
          "name": "uploadId",
          "in": "path",
          "required": true,
          "description": "The `uploadId` from a publish or from `POST /uploads`.",
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "operationId": "getUpload",
        "tags": [
          "Uploads"
        ],
        "summary": "Get an upload",
        "description": "Where a publish is now. Poll this after a `202`; `site` is set once it's live.",
        "responses": {
          "200": {
            "description": "The upload.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublishResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email not verified, or over your plan's site limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such site or upload on your account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The upload was rejected, for example a zip without index.html or a file the virus scanner flagged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit: 200 calls per key per day, or your plan's uploads per day. `Retry-After` says when to try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/uploads/{uploadId}/complete": {
      "parameters": [
        {
          "name": "uploadId",
          "in": "path",
          "required": true,
          "description": "The `uploadId` from a publish or from `POST /uploads`.",
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "operationId": "completeUpload",
        "tags": [
          "Uploads"
        ],
        "summary": "Finish a big upload",
        "description": "Step 3: tell us the file is in place. Waits for publishing like `POST /sites`.",
        "responses": {
          "200": {
            "description": "Published. `site.url` is live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublishResult"
                }
              }
            }
          },
          "202": {
            "description": "Still processing after about 20 seconds. Poll `GET /uploads/{uploadId}` every couple of seconds.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublishResult"
                }
              }
            }
          },
          "400": {
            "description": "Bad input, such as an invalid link name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, revoked or expired API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Email not verified, or over your plan's site limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such site or upload on your account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The upload was rejected, for example a zip without index.html or a file the virus scanner flagged.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit: 200 calls per key per day, or your plan's uploads per day. `Retry-After` says when to try again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "fpk_...",
        "description": "An API key from Settings in the dashboard (`fpk_...`), or an OAuth access token from the MCP sign-in."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "What went wrong, in words you can show to a person.",
            "example": "site not found"
          }
        }
      },
      "PublishJson": {
        "type": "object",
        "required": [
          "html"
        ],
        "properties": {
          "html": {
            "type": "string",
            "description": "The page. A reply pasted from an AI chat is fine: a ```html fence and text around it are stripped."
          },
          "linkName": {
            "type": [
              "string",
              "null"
            ],
            "description": "New sites only: the link name. Null or missing picks a random one."
          }
        }
      },
      "AccountInfo": {
        "type": "object",
        "required": [
          "email",
          "plan",
          "sites",
          "maxSites",
          "maxSiteBytes",
          "maxPdfBytes",
          "uploadsToday",
          "uploadsPerDay"
        ],
        "properties": {
          "email": {
            "type": "string",
            "format": "email"
          },
          "plan": {
            "type": "string",
            "description": "`free`, `starter` or `plus`.",
            "example": "starter"
          },
          "sites": {
            "type": "integer",
            "description": "Sites you have now."
          },
          "maxSites": {
            "type": "integer",
            "description": "Sites your plan allows."
          },
          "maxSiteBytes": {
            "type": "integer",
            "format": "int64",
            "description": "Largest HTML page or zip your plan allows, in bytes."
          },
          "maxPdfBytes": {
            "type": "integer",
            "format": "int64",
            "description": "Largest PDF your plan allows, in bytes."
          },
          "uploadsToday": {
            "type": "integer",
            "description": "Uploads since midnight UTC."
          },
          "uploadsPerDay": {
            "type": "integer",
            "description": "Uploads your plan allows per day."
          }
        }
      },
      "Site": {
        "type": "object",
        "required": [
          "id",
          "slug",
          "title",
          "kind",
          "status",
          "url",
          "version",
          "bytes",
          "updatedAt"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "example": "3f5e2a90-6b1d-4c1e-9a43-0d6c2f8e7b21"
          },
          "slug": {
            "type": "string",
            "description": "The link name.",
            "example": "spring-menu"
          },
          "title": {
            "type": "string",
            "description": "From the page's `<title>`, the PDF's title or the file name.",
            "example": "Spring menu"
          },
          "kind": {
            "type": "string",
            "enum": [
              "html",
              "zip",
              "pdf"
            ]
          },
          "status": {
            "type": "string",
            "description": "`active` (live), `held_for_review`, `paused` or `suspended`.",
            "example": "active"
          },
          "url": {
            "type": "string",
            "format": "uri",
            "example": "https://spring-menu.flatpage.page"
          },
          "version": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Current version number; null before the first version is live.",
            "example": 1
          },
          "bytes": {
            "type": [
              "integer",
              "null"
            ],
            "format": "int64",
            "description": "Size of the current version.",
            "example": 48213
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time",
            "example": "2026-11-02T15:40:00Z"
          },
          "views": {
            "type": "integer",
            "format": "int64",
            "description": "All-time views. Left out when 0.",
            "example": 128
          },
          "passwordProtected": {
            "type": "boolean",
            "description": "Left out when false.",
            "example": false
          },
          "noindex": {
            "type": "boolean",
            "description": "Hidden from search engines. Left out when false.",
            "example": false
          },
          "share": {
            "$ref": "#/components/schemas/ShareSettings"
          },
          "notifyUnlock": {
            "type": "boolean",
            "description": "Email the owner the first unlock of each version while password protected. Left out when true."
          },
          "spaMode": {
            "type": "string",
            "enum": [
              "auto",
              "on",
              "off"
            ],
            "description": "Single-page-app routing. Left out when `auto`."
          },
          "pdfDownload": {
            "type": "boolean",
            "description": "PDFs: visitors can download and print. Left out when true."
          },
          "emailGate": {
            "$ref": "#/components/schemas/EmailGateSettings"
          },
          "feedbackMode": {
            "type": "boolean",
            "description": "Visitors can leave comments on the page. Left out when false."
          },
          "formsEnabled": {
            "type": "boolean",
            "description": "Contact forms on the page email you. Left out when true."
          },
          "routing": {
            "$ref": "#/components/schemas/RoutingInfo"
          }
        }
      },
      "ShareSettings": {
        "type": "object",
        "description": "Link preview settings.",
        "required": [
          "title",
          "description",
          "image",
          "imagePending",
          "faviconPending",
          "preview"
        ],
        "properties": {
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "image": {
            "type": [
              "string",
              "null"
            ],
            "description": "`uploaded`, `generated` or null."
          },
          "imagePending": {
            "type": "boolean"
          },
          "faviconPending": {
            "type": "boolean"
          },
          "preview": {
            "$ref": "#/components/schemas/SharePreviewTags"
          }
        }
      },
      "SharePreviewTags": {
        "type": "object",
        "description": "What a pasted link shows; null fields come from the page itself.",
        "required": [
          "title",
          "description",
          "imageUrl",
          "faviconUrl"
        ],
        "properties": {
          "title": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "imageUrl": {
            "type": [
              "string",
              "null"
            ]
          },
          "faviconUrl": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "EmailGateSettings": {
        "type": "object",
        "description": "Ask visitors for their email before showing the page.",
        "properties": {
          "enabled": {
            "type": "boolean"
          },
          "headline": {
            "type": [
              "string",
              "null"
            ]
          },
          "consent": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "RoutingInfo": {
        "type": "object",
        "description": "What the current version does for missing paths. Not set for PDFs.",
        "properties": {
          "spaDetected": {
            "type": "boolean"
          },
          "has404": {
            "type": "boolean"
          },
          "redirects": {
            "type": "integer",
            "description": "`_redirects` rules in use."
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "PublishResult": {
        "type": "object",
        "required": [
          "uploadId",
          "status"
        ],
        "properties": {
          "uploadId": {
            "type": "string",
            "format": "uuid",
            "example": "8d0c6f1e-2b7a-4f0e-b1c4-5e9a7d3c2f10"
          },
          "status": {
            "type": "string",
            "description": "`published`, or `awaiting`, `received` or `processing` while it's still going.",
            "example": "published"
          },
          "site": {
            "$ref": "#/components/schemas/Site",
            "description": "Set once published."
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Published, but worth a look: `source_not_build` means the zip was source code, not a build."
          }
        }
      },
      "SiteStats": {
        "type": "object",
        "required": [
          "site",
          "totalViews",
          "last30Days"
        ],
        "properties": {
          "site": {
            "$ref": "#/components/schemas/Site"
          },
          "totalViews": {
            "type": "integer",
            "format": "int64",
            "description": "All-time views."
          },
          "last30Days": {
            "$ref": "#/components/schemas/ViewsReport"
          }
        }
      },
      "ViewsReport": {
        "type": "object",
        "required": [
          "days",
          "total",
          "thisMonth"
        ],
        "properties": {
          "days": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DailyViews"
            },
            "description": "30 entries, oldest first, ending today (UTC)."
          },
          "total": {
            "type": "integer",
            "format": "int64",
            "description": "Sum of `days`."
          },
          "thisMonth": {
            "type": "integer",
            "format": "int64",
            "description": "Views since the 1st of this month (UTC)."
          },
          "sites": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SiteMonthViews"
            },
            "description": "Not used by this endpoint."
          }
        }
      },
      "DailyViews": {
        "type": "object",
        "required": [
          "day",
          "views"
        ],
        "properties": {
          "day": {
            "type": "string",
            "format": "date"
          },
          "views": {
            "type": "integer",
            "format": "int64"
          }
        }
      },
      "SiteMonthViews": {
        "type": "object",
        "required": [
          "id",
          "thisMonth"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "thisMonth": {
            "type": "integer",
            "format": "int64"
          }
        }
      },
      "LinkNameCheck": {
        "type": "object",
        "required": [
          "name",
          "available"
        ],
        "properties": {
          "name": {
            "type": "string",
            "description": "The name as it would be saved (lower-cased, trimmed)."
          },
          "available": {
            "type": "boolean"
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Why it can't be used. Left out when available."
          },
          "url": {
            "type": [
              "string",
              "null"
            ],
            "description": "The URL it would get."
          }
        }
      },
      "RenameSiteRequest": {
        "type": "object",
        "required": [
          "linkName"
        ],
        "properties": {
          "linkName": {
            "type": "string",
            "example": "summer-menu"
          }
        }
      },
      "RenameSiteResponse": {
        "type": "object",
        "required": [
          "slug",
          "url",
          "oldSlug",
          "redirectUntil"
        ],
        "properties": {
          "slug": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "oldSlug": {
            "type": "string"
          },
          "redirectUntil": {
            "type": "string",
            "format": "date-time",
            "description": "The old link redirects to the new one until then."
          }
        }
      },
      "CreateUploadRequest": {
        "type": "object",
        "required": [
          "fileName",
          "bytes"
        ],
        "properties": {
          "fileName": {
            "type": "string",
            "description": "Ends in .html, .zip or .pdf.",
            "example": "brochure.pdf"
          },
          "contentType": {
            "type": [
              "string",
              "null"
            ],
            "description": "Optional; the extension decides."
          },
          "bytes": {
            "type": "integer",
            "format": "int64",
            "description": "Exact file size."
          },
          "siteId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Replace this site (id or link name) instead of publishing a new one."
          },
          "linkName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Link name for a new site. Not allowed with `siteId`."
          }
        }
      },
      "CreateUploadResponse": {
        "type": "object",
        "required": [
          "uploadId",
          "kind",
          "putUrl",
          "contentType",
          "expiresAt"
        ],
        "properties": {
          "uploadId": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string",
            "enum": [
              "html",
              "zip",
              "pdf"
            ]
          },
          "putUrl": {
            "type": "string",
            "format": "uri",
            "description": "PUT the file here."
          },
          "contentType": {
            "type": "string",
            "description": "The PUT must send exactly this Content-Type; it's part of the signature."
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    }
  }
}
