{
  "openapi": "3.1.0",
  "info": {
    "title": "Nanopage API",
    "version": "1.0.0",
    "summary": "Publish and manage AI-generated static websites hosted on nanopage.site.",
    "description": "Nanopage (https://nanopage.site) hosts small static websites. This API is the REST twin of the Nanopage MCP server (https://api.nanopage.site/mcp): you generate the HTML/CSS/JS bundle yourself, and Nanopage validates it against the authoring spec, stores it, and serves it at https://{slug}.nanopage.site immediately. The authoring spec (file layout, hard rules, size limits) is documented at https://nanopage.site/developers/ and served as the MCP resource nanopage://spec/authoring-guide.md. Authenticate every request with a personal access token minted at https://nanopage.site/mcp.",
    "contact": {
      "name": "Nanopage support",
      "email": "hello@nanopage.site",
      "url": "https://nanopage.site/contact/"
    },
    "termsOfService": "https://nanopage.site/terms/"
  },
  "servers": [
    {
      "url": "https://api.nanopage.site",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Nanopage developer resources",
    "url": "https://nanopage.site/developers/"
  },
  "security": [
    {
      "personalAccessToken": []
    }
  ],
  "tags": [
    {
      "name": "websites",
      "description": "Create websites and inspect the ones the token owner can access."
    },
    {
      "name": "versions",
      "description": "Publish and list versions of an existing website."
    },
    {
      "name": "jobs",
      "description": "Publish-job status lookups."
    },
    {
      "name": "slugs",
      "description": "Subdomain slug validation and suggestions."
    },
    {
      "name": "meta",
      "description": "API discovery endpoints."
    }
  ],
  "paths": {
    "/v1": {
      "get": {
        "operationId": "getApiIndex",
        "tags": [
          "meta"
        ],
        "summary": "API index",
        "description": "Unauthenticated discovery document: links to the OpenAPI spec, the MCP endpoint, token minting, and the developer docs. Useful as a stable machine-readable entry point.",
        "security": [],
        "responses": {
          "200": {
            "description": "Links to every machine-readable Nanopage API resource.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiIndex"
                }
              }
            }
          }
        }
      }
    },
    "/v1/websites": {
      "get": {
        "operationId": "listWebsites",
        "tags": [
          "websites"
        ],
        "summary": "List websites",
        "description": "List every website the token owner owns or collaborates on (up to 50, most recently updated first), including slug, live URL, status, and current version.",
        "responses": {
          "200": {
            "description": "Websites visible to the authenticated user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WebsiteList"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid personal access token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createWebsite",
        "tags": [
          "websites"
        ],
        "summary": "Create and publish a website",
        "description": "Create a new website from a bundle of files you generated yourself. Nanopage validates the bundle against the authoring spec, stores it, and serves it at https://{slug}.nanopage.site immediately. index.html is required; external scripts/styles, iframes, and service workers are rejected. If slug is omitted, one is derived from the site name.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublishWebsiteRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The website was created and is live.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublishWebsiteResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid personal access token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The requested slug is already taken.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The bundle violates the authoring spec (code PUBLISH_INVALID, violations listed) or the category is invalid.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Publish rate limit exceeded — retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/websites/{websiteId}": {
      "get": {
        "operationId": "getWebsite",
        "tags": [
          "websites"
        ],
        "summary": "Get a website",
        "description": "Fetch full details for one website: status, live URL, current version, every past version, and per-version file metadata.",
        "parameters": [
          {
            "name": "websiteId",
            "in": "path",
            "required": true,
            "description": "Website id, e.g. \"web_ab12cd34\". Returned by createWebsite and listWebsites.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The website with its versions.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Website"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid personal access token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No website with that id is visible to the authenticated user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/websites/{websiteId}/versions": {
      "get": {
        "operationId": "listWebsiteVersions",
        "tags": [
          "versions"
        ],
        "summary": "List versions",
        "description": "List every version of a website, newest first, with file metadata for each.",
        "parameters": [
          {
            "name": "websiteId",
            "in": "path",
            "required": true,
            "description": "Website id, e.g. \"web_ab12cd34\". Returned by createWebsite and listWebsites.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "All versions of the website.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VersionList"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid personal access token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No website with that id is visible to the authenticated user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createWebsiteVersion",
        "tags": [
          "versions"
        ],
        "summary": "Publish a new version",
        "description": "Publish a new version of an existing website. The bundle replaces the live version at https://{slug}.nanopage.site unless promote is false, in which case the previous version stays live and the new one is only inspectable via listWebsiteVersions. Same validation rules as createWebsite.",
        "parameters": [
          {
            "name": "websiteId",
            "in": "path",
            "required": true,
            "description": "Website id, e.g. \"web_ab12cd34\". Returned by createWebsite and listWebsites.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PublishVersionRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "The version was published.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PublishVersionResult"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid personal access token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No website with that id is visible to the authenticated user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The bundle violates the authoring spec (code PUBLISH_INVALID, violations listed).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Publish rate limit exceeded — retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/jobs/{jobId}": {
      "get": {
        "operationId": "getJob",
        "tags": [
          "jobs"
        ],
        "summary": "Get job status",
        "description": "Look up a publish job by id. Jobs created through this API are marked ready synchronously, so this is mostly useful for auditing and for polling jobs started from the Nanopage app.",
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "description": "Job id, e.g. \"job_ab12cd34\". Returned by createWebsite and createWebsiteVersion.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The job with its current status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid personal access token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No job with that id is visible to the authenticated user.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/slugs/suggest": {
      "get": {
        "operationId": "suggestSlug",
        "tags": [
          "slugs"
        ],
        "summary": "Validate or suggest a slug",
        "description": "Given a site name or draft slug, report whether it is a valid and available *.nanopage.site subdomain, and return an available fallback suggestion either way.",
        "parameters": [
          {
            "name": "value",
            "in": "query",
            "required": true,
            "description": "The name or draft slug to check, e.g. \"Acme Bakery\" or \"acme-bakery\".",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 120
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Validity, availability, and a suggested slug.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SlugSuggestion"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid personal access token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "personalAccessToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "np_… personal access token",
        "description": "Personal access token minted at https://nanopage.site/mcp, sent as `Authorization: Bearer np_…`. Every token carries the single scope `mcp`, which grants exactly the operations in this specification plus the MCP endpoint (https://api.nanopage.site/mcp) — and nothing else. Dashboard, billing, custom-domain, and admin operations are not reachable with a token. The scope declaration is also published machine-readably as RFC 9728 protected-resource metadata at https://api.nanopage.site/.well-known/oauth-protected-resource."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every non-2xx response uses this envelope.",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "description": "Stable machine-readable error code, e.g. \"AUTH_REQUIRED\", \"NOT_FOUND\", \"PUBLISH_INVALID\"."
              },
              "message": {
                "type": "string",
                "description": "Human-readable explanation."
              },
              "fields": {
                "type": "object",
                "additionalProperties": {
                  "type": "string"
                },
                "description": "Optional per-field validation messages."
              },
              "violations": {
                "type": "array",
                "description": "Present on PUBLISH_INVALID: every authoring-spec rule the bundle broke.",
                "items": {
                  "$ref": "#/components/schemas/PublishViolation"
                }
              }
            }
          }
        }
      },
      "PublishViolation": {
        "type": "object",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Rule identifier, e.g. \"EXTERNAL_URL\", \"MISSING_INDEX\"."
          },
          "message": {
            "type": "string"
          },
          "path": {
            "type": "string",
            "description": "The offending file, when the rule is file-specific."
          },
          "detail": {
            "type": "string"
          }
        }
      },
      "PublishFile": {
        "type": "object",
        "required": [
          "path",
          "content"
        ],
        "properties": {
          "path": {
            "type": "string",
            "description": "Relative path, e.g. \"index.html\", \"assets/site.css\", \"images/hero.webp\". index.html is required in every bundle."
          },
          "content": {
            "type": "string",
            "description": "File contents. UTF-8 text by default; base64 when contentEncoding is \"base64\"."
          },
          "contentEncoding": {
            "type": "string",
            "enum": [
              "utf8",
              "base64"
            ],
            "default": "utf8",
            "description": "Use \"base64\" for images and other binary files."
          },
          "contentType": {
            "type": "string",
            "description": "Optional MIME type override; inferred from the extension if omitted."
          }
        }
      },
      "PublishWebsiteRequest": {
        "type": "object",
        "required": [
          "category",
          "files"
        ],
        "properties": {
          "name": {
            "type": "string",
            "maxLength": 120,
            "description": "Display name shown in the Nanopage dashboard."
          },
          "category": {
            "type": "string",
            "enum": [
              "real_estate",
              "restaurant",
              "service",
              "portfolio",
              "event",
              "resume",
              "other"
            ],
            "description": "What kind of site this is."
          },
          "description": {
            "type": "string",
            "maxLength": 5000,
            "description": "Short human description of the site."
          },
          "slug": {
            "type": "string",
            "minLength": 3,
            "maxLength": 50,
            "pattern": "^[a-z][a-z0-9-]*$",
            "description": "Preferred subdomain. Derived from the name when omitted; suffixed when taken."
          },
          "files": {
            "type": "array",
            "minItems": 1,
            "maxItems": 40,
            "items": {
              "$ref": "#/components/schemas/PublishFile"
            },
            "description": "The complete site bundle. Max 40 files, 2 MB per file, 8 MB total, 15 HTML pages."
          }
        }
      },
      "PublishWebsiteResult": {
        "type": "object",
        "required": [
          "websiteId",
          "jobId",
          "version",
          "slug",
          "pageUrl",
          "bytesWritten"
        ],
        "properties": {
          "websiteId": {
            "type": "string"
          },
          "jobId": {
            "type": "string"
          },
          "version": {
            "type": "integer",
            "description": "Always 1 for a new website."
          },
          "slug": {
            "type": "string"
          },
          "pageUrl": {
            "type": "string",
            "format": "uri",
            "description": "The live URL, e.g. \"https://acme.nanopage.site\". Serving starts immediately."
          },
          "bytesWritten": {
            "type": "integer",
            "description": "Total bytes stored for the bundle."
          }
        }
      },
      "PublishVersionRequest": {
        "type": "object",
        "required": [
          "files"
        ],
        "properties": {
          "files": {
            "type": "array",
            "minItems": 1,
            "maxItems": 40,
            "items": {
              "$ref": "#/components/schemas/PublishFile"
            },
            "description": "The complete replacement bundle (not a diff). Same limits as createWebsite."
          },
          "message": {
            "type": "string",
            "maxLength": 2000,
            "description": "Short changelog entry for this version."
          },
          "promote": {
            "type": "boolean",
            "default": true,
            "description": "When false, the previous version stays live and this one is only stored."
          }
        }
      },
      "PublishVersionResult": {
        "type": "object",
        "required": [
          "websiteId",
          "jobId",
          "version",
          "isCurrent",
          "pageUrl",
          "bytesWritten"
        ],
        "properties": {
          "websiteId": {
            "type": "string"
          },
          "jobId": {
            "type": "string",
            "description": "Also the id of the created version."
          },
          "version": {
            "type": "integer"
          },
          "isCurrent": {
            "type": "boolean",
            "description": "Whether this version is now the one being served."
          },
          "pageUrl": {
            "type": "string",
            "format": "uri"
          },
          "bytesWritten": {
            "type": "integer"
          }
        }
      },
      "WebsiteList": {
        "type": "object",
        "required": [
          "items"
        ],
        "properties": {
          "items": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebsiteSummary"
            }
          },
          "websites": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/WebsiteSummary"
            },
            "description": "Deprecated alias of items.",
            "deprecated": true
          },
          "websiteLimits": {
            "type": "object",
            "description": "How many websites the account has and whether it may create another.",
            "properties": {
              "count": {
                "type": "integer"
              },
              "max": {
                "type": "integer"
              },
              "canCreate": {
                "type": "boolean"
              }
            },
            "additionalProperties": true
          }
        }
      },
      "WebsiteSummary": {
        "type": "object",
        "required": [
          "id",
          "name",
          "status",
          "role"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": [
              "string",
              "null"
            ]
          },
          "pageUrl": {
            "type": "string",
            "format": "uri",
            "description": "Live URL. Absent while the site has no ready version."
          },
          "status": {
            "type": "string",
            "description": "Website status: \"active\", \"draft\" (no ready version yet), or \"deleted\"."
          },
          "currentVersionId": {
            "type": "string"
          },
          "latestJobStatus": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "pending",
              "claimed",
              "generating",
              "ready",
              "retry",
              "dead",
              null
            ]
          },
          "paid": {
            "type": "boolean"
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "editor"
            ],
            "description": "The authenticated user's relationship to the site."
          },
          "ownerName": {
            "type": [
              "string",
              "null"
            ],
            "description": "Present when the user is a collaborator rather than the owner."
          },
          "subscription": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "status": {
                "type": "string"
              },
              "renewsAt": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "endsAt": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "updatedAt": {
            "type": "string"
          }
        }
      },
      "Website": {
        "type": "object",
        "required": [
          "id",
          "name",
          "status",
          "role",
          "category",
          "versions"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "description": "Website status: \"active\", \"draft\" (no ready version yet), or \"deleted\"."
          },
          "role": {
            "type": "string",
            "enum": [
              "owner",
              "editor"
            ]
          },
          "category": {
            "type": "string",
            "enum": [
              "real_estate",
              "restaurant",
              "service",
              "portfolio",
              "event",
              "resume",
              "other"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "pageUrl": {
            "type": "string",
            "format": "uri",
            "description": "Live URL. Absent while the site has no ready version."
          },
          "currentVersionId": {
            "type": "string"
          },
          "currentVersion": {
            "type": [
              "object",
              "null"
            ],
            "description": "Raw record of the currently served version; superset of the Version fields.",
            "additionalProperties": true
          },
          "freeChangesUsed": {
            "type": "integer"
          },
          "freeChangesLimit": {
            "type": "integer"
          },
          "passwordProtected": {
            "type": "boolean"
          },
          "versions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Version"
            }
          },
          "createdAt": {
            "type": "string"
          },
          "updatedAt": {
            "type": "string"
          }
        }
      },
      "VersionList": {
        "type": "object",
        "required": [
          "versions"
        ],
        "properties": {
          "versions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Version"
            },
            "description": "Newest first. isCurrent is not computed by this operation — use getWebsite to see which version is live."
          }
        }
      },
      "Version": {
        "type": "object",
        "required": [
          "id",
          "version",
          "kind",
          "status",
          "files",
          "isCurrent"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "jobId": {
            "type": "string",
            "description": "Same value as id."
          },
          "version": {
            "type": "integer"
          },
          "kind": {
            "type": "string",
            "enum": [
              "initial",
              "refinement"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "claimed",
              "generating",
              "ready",
              "retry",
              "dead"
            ]
          },
          "instruction": {
            "type": "string",
            "description": "The changelog message or refinement instruction that produced this version."
          },
          "files": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VersionFile"
            }
          },
          "isCurrent": {
            "type": "boolean"
          },
          "createdAt": {
            "type": "string"
          }
        }
      },
      "VersionFile": {
        "type": "object",
        "required": [
          "originalName",
          "size"
        ],
        "properties": {
          "originalName": {
            "type": "string",
            "description": "Path within the bundle, e.g. \"index.html\"."
          },
          "contentType": {
            "type": [
              "string",
              "null"
            ]
          },
          "size": {
            "type": "integer",
            "description": "Size in bytes."
          }
        }
      },
      "Job": {
        "type": "object",
        "required": [
          "jobId",
          "websiteId",
          "status"
        ],
        "properties": {
          "jobId": {
            "type": "string"
          },
          "websiteId": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "claimed",
              "generating",
              "ready",
              "retry",
              "dead"
            ],
            "description": "Publishes via this API are \"ready\" synchronously."
          },
          "currentPhase": {
            "type": [
              "string",
              "null"
            ]
          },
          "progressMessage": {
            "type": [
              "string",
              "null"
            ],
            "description": "Human-readable progress description."
          },
          "pageUrl": {
            "type": "string",
            "format": "uri",
            "description": "Present once the job is ready."
          },
          "attempt": {
            "type": "integer"
          },
          "statusUrl": {
            "type": "string",
            "description": "App-relative status page path."
          }
        }
      },
      "SlugSuggestion": {
        "type": "object",
        "required": [
          "slug",
          "valid",
          "available",
          "suggestion",
          "pageUrl"
        ],
        "properties": {
          "slug": {
            "type": "string",
            "description": "The normalized input value."
          },
          "valid": {
            "type": "boolean",
            "description": "Whether the input itself is a well-formed slug."
          },
          "available": {
            "type": "boolean",
            "description": "Whether the input slug is free to claim (false when invalid)."
          },
          "suggestion": {
            "type": "string",
            "description": "An available slug derived from the input — use this when available is false."
          },
          "message": {
            "type": "string",
            "description": "Why the input is invalid or taken; absent when it is usable."
          },
          "pageUrl": {
            "type": "string",
            "format": "uri",
            "description": "The URL the suggestion would be served at."
          }
        }
      },
      "ApiIndex": {
        "type": "object",
        "required": [
          "name",
          "openapi",
          "mcp",
          "documentation"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "version": {
            "type": "string"
          },
          "openapi": {
            "type": "string",
            "format": "uri",
            "description": "Where this API's OpenAPI 3.1 document is served."
          },
          "mcp": {
            "type": "string",
            "format": "uri",
            "description": "The Nanopage MCP endpoint (Streamable HTTP, JSON-RPC 2.0 over POST)."
          },
          "protectedResourceMetadata": {
            "type": "string",
            "format": "uri",
            "description": "RFC 9728 metadata declaring supported token scopes."
          },
          "tokens": {
            "type": "string",
            "format": "uri",
            "description": "Where humans mint personal access tokens."
          },
          "documentation": {
            "type": "string",
            "format": "uri"
          },
          "llms": {
            "type": "string",
            "format": "uri",
            "description": "llms.txt overview of nanopage.site."
          }
        }
      }
    }
  }
}
