{
  "openapi": "3.1.0",
  "info": {
    "title": "Litehost Connect API",
    "version": "1.0.0",
    "description": "Public API for deploying and managing Litehost projects. Designed for developers, automation tools, and AI agents.\n\nBase URL: `https://connect.litehost.io` \u00b7 Agent guide: [`/llms.txt`](https://connect.litehost.io/llms.txt) \u00b7 Spec: [`/openapi.json`](https://connect.litehost.io/openapi.json)\n\n## MCP server\n\nAI assistants (Muse, Claude, ChatGPT, Cursor) can connect to `https://connect.litehost.io/mcp` (Streamable HTTP, OAuth 2.1 with PKCE and dynamic client registration; API keys also work). Tools: `publish_site`, `create_upload_link` (a page where the user uploads a file they already have), `list_projects`, `get_project`, `get_qr_code`, `get_link_opens`, `update_project_files`, `update_project_settings`, `delete_project`, `get_account`.\n\n## Pick the path first\n\n1. **You have a key** (`LITEHOST_API_KEY` or a saved credentials file): send it as `Authorization: Bearer lh_live_xxx`. Check it with `GET /v1/auth/session`. Do not sign in again while it works.\n2. **No key, just publish**: `POST /v1/projects/temp` needs no key. The response has `url` (live for 15 minutes) and `claimUrl`, which the user opens in a browser to keep the project.\n3. **No key, manage projects over time**: sign in **once** with the OTP flow and save the key.\n\n## Authentication\n\n### Dashboard keys (permanent)\nCreate them in the **Integrations** section of the [Litehost dashboard](https://litehost.io/dashboard). They never expire and can be revoked at any time.\n\n### OTP sign-in (no dashboard needed)\n1. `POST /v1/auth/otp/request` with the user's email. Calling it again while a code is open sends nothing (`data.codeAlreadySent: true`); pass `\"resend\": true` only if the user says the email never arrived.\n2. Ask the user for the 6-digit code, then `POST /v1/auth/otp/verify`. A wrong code returns `OTP_INVALID`: ask the user to re-check it, **do not request a new code**.\n3. Save the returned key. Sign-in keys **renew automatically while in use** and expire only after 90 days without use.\n\n## Errors\nEvery error has a stable `code`, a human-readable `error`, and a `nextStep` telling an agent what to do. `429` responses include `retryAfterSeconds` and a `Retry-After` header.",
    "contact": {
      "name": "Litehost",
      "url": "https://litehost.io"
    }
  },
  "servers": [
    {
      "url": "https://connect.litehost.io",
      "description": "Production"
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "API Key",
        "description": "A Litehost API key in the format `lh_live_xxx`.\n\n**Dashboard keys** \u2014 created in the Integrations section, never expire, revocable at any time.\n\n**Sign-in keys** \u2014 returned by `POST /v1/auth/otp/verify`. They renew automatically while in use and expire after 90 days without use. Check one with `GET /v1/auth/session`."
      }
    },
    "schemas": {
      "ProjectOpensResponse": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "example": "success" },
          "data": {
            "type": "object",
            "properties": {
              "projectId": { "type": "string", "format": "uuid" },
              "analyticsEnabled": { "type": "boolean", "description": "When `false`, nothing new is recorded for this link." },
              "opens": {
                "type": "object",
                "description": "Opens: a person viewing the link in a browser. Excludes link previews, bots, email scanners, AI fetchers and the owner's own views.",
                "properties": {
                  "total": { "type": "integer" },
                  "last24h": { "type": "integer" },
                  "last7d": { "type": "integer" }
                },
                "required": ["total", "last24h", "last7d"]
              },
              "lastOpenedAt": { "type": "string", "format": "date-time", "nullable": true, "description": "Time of the latest open." },
              "recentOpens": {
                "type": "array",
                "description": "Latest 10 opens, newest first.",
                "items": {
                  "type": "object",
                  "properties": {
                    "at": { "type": "string", "format": "date-time" },
                    "country": { "type": "string", "nullable": true, "description": "ISO 3166-1 alpha-2" },
                    "device": { "type": "string", "nullable": true },
                    "browser": { "type": "string", "nullable": true },
                    "os": { "type": "string", "nullable": true },
                    "referrer": { "type": "string", "nullable": true }
                  }
                }
              },
              "topCountries": {
                "type": "array",
                "description": "Top 5 countries by opens.",
                "items": {
                  "type": "object",
                  "properties": {
                    "country": { "type": "string" },
                    "opens": { "type": "integer" }
                  }
                }
              },
              "visits": {
                "type": "object",
                "description": "Optional. Every non-bot page request, including link previews and scanners the server could not tell apart and the owner's views. Plan visit limits count these.",
                "properties": {
                  "total": { "type": "integer" },
                  "last24h": { "type": "integer" },
                  "last7d": { "type": "integer" }
                }
              },
              "ownerOpens": {
                "type": "object",
                "description": "Optional. The owner's own opens (from the dashboard, or right after publishing). Never counted in `opens`.",
                "properties": {
                  "total": { "type": "integer" }
                }
              }
            },
            "required": ["projectId", "analyticsEnabled", "opens", "lastOpenedAt", "recentOpens", "topCountries"]
          }
        }
      },
      "Project": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid", "example": "3fa85f64-5717-4562-b3fc-2c963f66afa6" },
          "title": { "type": "string", "example": "My Project" },
          "slug": { "type": "string", "example": "my-project" },
          "type": {
            "type": "string",
            "enum": ["html", "zip", "pdf", "image", "files", "presentation", "audio", "video", "code", "document", "markdown", "yaml", "sql", "python", "model3d"]
          },
          "url": { "type": "string", "format": "uri", "example": "https://my-project.litepage.site" },
          "status": { "type": "string", "enum": ["public", "private", "uploading", "expired", "archived"] },
          "access": { "type": "string", "enum": ["public", "password", "private"], "description": "Visibility mode. When `password`, visitors must enter a password to view the project. The password itself is never returned in responses." },
          "domainId": { "type": "string", "format": "uuid", "nullable": true },
          "workspaceId": { "type": "string", "format": "uuid", "nullable": true },
          "noIndex": { "type": "boolean", "example": false },
          "disableDownloads": { "type": "boolean", "example": false },
          "pageTitle": { "type": "string", "nullable": true },
          "metaDescription": { "type": "string", "nullable": true },
          "analyticsEnabled": { "type": "boolean", "example": true },
          "expiresAt": { "type": "string", "format": "date-time", "nullable": true },
          "archivedAt": { "type": "string", "format": "date-time", "nullable": true },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        },
        "required": ["id", "title", "slug", "type", "url", "status", "access", "noIndex", "disableDownloads", "analyticsEnabled", "createdAt", "updatedAt"]
      },
      "Deployment": {
        "type": "object",
        "properties": {
          "deploymentId": { "type": "string", "example": "dep_v3" },
          "version": { "type": "integer", "example": 3 },
          "status": { "type": "string", "enum": ["active", "ready"] },
          "sizeBytes": { "type": "integer", "nullable": true, "example": 204800 },
          "createdAt": { "type": "string", "format": "date-time" }
        },
        "required": ["deploymentId", "version", "status", "createdAt"]
      },
      "Domain": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "domain": { "type": "string", "example": "example.com" },
          "status": {
            "type": "string",
            "enum": ["pending_verification", "propagation_wait", "active", "error_dns", "ssl_pending", "ssl_active"]
          },
          "verifiedAt": { "type": "string", "format": "date-time", "nullable": true },
          "projectCount": { "type": "integer" },
          "createdAt": { "type": "string", "format": "date-time" }
        },
        "required": ["id", "domain", "status", "projectCount", "createdAt"]
      },
      "WorkspaceSummary": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "description": "Workspace ID" },
          "name": { "type": "string", "description": "Workspace display name" },
          "projectCount": { "type": "integer", "description": "Number of projects in this workspace" },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        },
        "required": ["id", "name", "projectCount", "createdAt", "updatedAt"]
      },
      "DomainDetail": {
        "allOf": [
          { "$ref": "#/components/schemas/Domain" },
          {
            "type": "object",
            "properties": {
              "projects": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": { "type": "string", "format": "uuid" },
                    "title": { "type": "string" },
                    "slug": { "type": "string" },
                    "status": { "type": "string" }
                  }
                }
              }
            }
          }
        ]
      },
      "ErrorResponse": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": ["error"] },
          "code": { "type": "string", "description": "Stable machine-readable code, e.g. `API_KEY_EXPIRED`, `OTP_INVALID`, `FREE_TIER_RESTRICTED`, `RATE_LIMITED`." },
          "error": { "type": "string" },
          "nextStep": { "type": "string", "description": "What an agent should do next. Follow it instead of guessing (for example, never request a new sign-in code after `OTP_INVALID`)." },
          "retryAfterSeconds": { "type": "integer", "description": "Present on `429` responses." }
        },
        "required": ["status", "error"]
      },
      "FreeTierRestricted": {
        "type": "object",
        "description": "Returned when a free-tier user attempts to access an endpoint that requires a paid plan.",
        "properties": {
          "status": { "type": "string", "enum": ["error"] },
          "error": { "type": "string", "example": "This endpoint is not available on the free plan. Upgrade to a paid plan (starter or higher) to use it." },
          "code": { "type": "string", "enum": ["FREE_TIER_RESTRICTED"] },
          "requiredTier": { "type": "string", "enum": ["starter"], "example": "starter" }
        },
        "required": ["status", "error", "code", "requiredTier"]
      },
      "SuccessResponse": {
        "type": "object",
        "properties": {
          "status": { "type": "string", "enum": ["success"] }
        },
        "required": ["status"]
      }
    }
  },
  "paths": {
    "/health": {
      "get": {
        "summary": "Health check",
        "operationId": "health",
        "tags": ["System"],
        "responses": {
          "200": {
            "description": "Service is healthy",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "example": "ok" },
                    "service": { "type": "string", "example": "connect" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1": {
      "get": {
        "summary": "API info",
        "operationId": "apiInfo",
        "tags": ["System"],
        "responses": {
          "200": {
            "description": "API version info",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "example": "ok" },
                    "version": { "type": "string", "example": "v1" },
                    "docs": { "type": "string", "example": "https://api-docs.litehost.io" }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/auth/otp/request": {
      "post": {
        "summary": "Request OTP code",
        "operationId": "requestOtp",
        "tags": ["Authentication"],
        "description": "Emails a 6-digit code to the address. First step of OTP sign-in \u2014 no existing account or dashboard access is required.\n\n**Safe to call more than once.** While a code is open (valid for 10 minutes) no new code is created and nothing is sent: the response has `data.codeAlreadySent: true`. Keep waiting for the user. Only if the user says the email never arrived (after checking spam), call again with `\"resend\": true`; the **same** code is emailed again, at most once a minute.\n\n**Limits:** 5 emails per address per hour, 10 per IP per hour.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email"],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "The email address to send the verification code to.",
                    "example": "you@example.com"
                  },
                  "resend": {
                    "type": "boolean",
                    "description": "Email the open code again (same digits). Use only when the user says no email arrived.",
                    "default": false
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Code sent. The response is identical whether or not the email corresponds to an existing account — this prevents account enumeration.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["success"] },
                    "message": { "type": "string", "example": "Verification code sent. Check your email." },
                    "nextStep": { "type": "string", "example": "Ask the user for the 6-digit code from the Litehost email (check spam), then call POST /v1/auth/otp/verify. Do not call this endpoint again while waiting for the user." },
                    "data": {
                      "type": "object",
                      "properties": {
                        "sent": { "type": "boolean", "description": "Whether an email was sent by this call." },
                        "resent": { "type": "boolean", "description": "The open code was emailed again." },
                        "codeAlreadySent": { "type": "boolean", "description": "A code is already open; nothing was sent. Ask the user for it." },
                        "expiresAt": { "type": "string", "format": "date-time", "description": "When the open code stops working." },
                        "resendAvailableInSeconds": { "type": "integer", "description": "Seconds until `resend: true` can email the code again." }
                      }
                    }
                  },
                  "required": ["status", "message"]
                }
              }
            }
          },
          "400": {
            "description": "The email address is missing or not a valid format.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          },
          "429": {
            "description": "Too many sign-in emails. Includes `retryAfterSeconds`. If a code was already emailed, `nextStep` says to verify that one instead.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          }
        }
      }
    },
    "/v1/auth/otp/verify": {
      "post": {
        "summary": "Verify OTP and get API key",
        "operationId": "verifyOtp",
        "tags": ["Authentication"],
        "description": "Verifies the 6-digit code sent by `POST /v1/auth/otp/request`.\n\nOn success:\n- If no Litehost account exists for the email, one is created (new accounts get a 7-day Pro trial).\n- An API key (`lh_live_xxx`) is returned **once** \u2014 save it where it survives the session. It renews automatically while in use and expires after 90 days without use.\n\nErrors:\n- `OTP_INVALID` (401): wrong code. Ask the user to re-check the latest email. **Do not request a new code**; `attemptsLeft` says how many tries remain.\n- `OTP_EXPIRED` (401): no open code (expired or used). Request a new one once.\n- `OTP_TOO_MANY_ATTEMPTS` (429): after 5 wrong codes the code is cancelled. Request a new one.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["email", "code"],
                "properties": {
                  "email": {
                    "type": "string",
                    "format": "email",
                    "description": "The same email address used in `POST /v1/auth/otp/request`.",
                    "example": "you@example.com"
                  },
                  "code": {
                    "type": "string",
                    "minLength": 6,
                    "maxLength": 6,
                    "pattern": "^[0-9]{6}$",
                    "description": "The 6-digit verification code from the email.",
                    "example": "482931"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OTP verified. Account ensured. A short-lived API key is returned.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["success"] },
                    "data": {
                      "type": "object",
                      "properties": {
                        "email": {
                          "type": "string",
                          "format": "email",
                          "description": "The email address of the account.",
                          "example": "you@example.com"
                        },
                        "apiKey": {
                          "type": "string",
                          "description": "The API key to use as `Authorization: Bearer <apiKey>` on all subsequent requests. **Shown only once — store it securely.**",
                          "example": "lh_live_a1b2c3d4e5f6..."
                        },
                        "expiresAt": {
                          "type": "string",
                          "format": "date-time",
                          "description": "When the key would expire if unused. Each use pushes it back, so a key in use does not expire.",
                          "example": "2026-04-09T14:23:00.000Z"
                        },
                        "message": {
                          "type": "string",
                          "example": "Signed in. This key is shown only once. It renews automatically while in use and expires after 90 days without use."
                        }
                      },
                      "required": ["email", "apiKey", "expiresAt", "message"]
                    }
                  },
                  "required": ["status", "data"]
                },
                "example": {
                  "status": "success",
                  "data": {
                    "email": "you@example.com",
                    "apiKey": "lh_live_a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2",
                    "expiresAt": "2026-04-09T14:23:00.000Z",
                    "message": "Signed in. This key is shown only once. It renews automatically while in use and expires after 90 days without use."
                  }
                }
              }
            }
          },
          "400": {
            "description": "Email or code is missing or malformed.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          },
          "401": {
            "description": "`OTP_INVALID`: wrong code — ask the user to re-check it, do not request a new one. `OTP_EXPIRED`: no open code — request a new one once.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          },
          "500": {
            "description": "Account provisioning failed. Retrying the full OTP flow is safe.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          }
        }
      }
    },
    "/v1/auth/session": {
      "get": {
        "summary": "Check the current API key",
        "operationId": "getSession",
        "tags": ["Authentication"],
        "description": "Returns who the key belongs to, the plan, and when the key expires. No side effects — call it at the start of a session to decide whether a saved key still works before ever signing in again.",
        "security": [{ "ApiKeyAuth": [] }],
        "responses": {
          "200": {
            "description": "The key works.",
            "content": {
              "application/json": {
                "example": {
                  "status": "success",
                  "data": {
                    "email": "you@example.com",
                    "plan": "pro",
                    "apiKey": { "expiresAt": "2026-12-27T12:00:00.000Z", "renewsWhileInUse": true }
                  }
                }
              }
            }
          },
          "401": {
            "description": "`API_KEY_MISSING`, `API_KEY_INVALID` or `API_KEY_EXPIRED`. Follow `nextStep`.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          }
        }
      }
    },
    "/v1/user": {
      "get": {
        "summary": "Get current user",
        "operationId": "getUser",
        "tags": ["User"],
        "security": [{ "ApiKeyAuth": [] }],
        "description": "Returns the authenticated user's email, plan tier, limits, and current quota usage. **Quota only counts active (non-archived) projects.** Archiving a project frees up both project slots and storage.",
        "responses": {
          "200": {
            "description": "User info with plan and quota",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["success"] },
                    "data": {
                      "type": "object",
                      "properties": {
                        "email": { "type": "string", "format": "email", "example": "user@example.com" },
                        "plan": {
                          "type": "object",
                          "properties": {
                            "tier": {
                              "type": "string",
                              "enum": ["free", "starter", "pro", "agency", "scale"],
                              "example": "pro"
                            },
                            "limits": {
                              "type": "object",
                              "properties": {
                                "maxProjects": { "type": "integer", "description": "-1 means unlimited", "example": 10 },
                                "maxStorageBytes": { "type": "integer", "description": "-1 means unlimited", "example": 2147483648 },
                                "maxFilesPerProject": { "type": "integer", "description": "-1 means unlimited", "example": 200 },
                                "maxVisitorsPerMonth": { "type": "integer", "description": "-1 means unlimited", "example": 100000 }
                              }
                            }
                          }
                        },
                        "quota": {
                          "type": "object",
                          "properties": {
                            "projects": {
                              "type": "object",
                              "properties": {
                                "used": { "type": "integer", "description": "Active (non-archived) project count", "example": 3 },
                                "limit": { "type": "integer", "description": "-1 means unlimited", "example": 10 },
                                "unlimited": { "type": "boolean", "example": false }
                              }
                            },
                            "storage": {
                              "type": "object",
                              "properties": {
                                "usedBytes": { "type": "integer", "description": "Bytes used by active (non-archived) projects only", "example": 52428800 },
                                "limitBytes": { "type": "integer", "description": "-1 means unlimited", "example": 2147483648 },
                                "unlimited": { "type": "boolean", "example": false }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "status": "success",
                  "data": {
                    "email": "user@example.com",
                    "plan": {
                      "tier": "pro",
                      "limits": {
                        "maxProjects": 25,
                        "maxStorageBytes": 5368709120,
                        "maxFilesPerProject": 200,
                        "maxVisitorsPerMonth": 100000
                      }
                    },
                    "quota": {
                      "projects": { "used": 3, "limit": 25, "unlimited": false },
                      "storage": { "usedBytes": 52428800, "limitBytes": 5368709120, "unlimited": false }
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/v1/projects": {
      "get": {
        "summary": "List projects",
        "operationId": "listProjects",
        "tags": ["Projects"],
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          {
            "name": "workspaceId",
            "in": "query",
            "schema": { "type": "string", "format": "uuid" },
            "description": "Filter by workspace ID"
          },
          {
            "name": "archived",
            "in": "query",
            "schema": { "type": "string", "enum": ["true", "false", "all"] },
            "description": "Filter by archive state. `true` = only archived projects, `false` or omitted = only active projects, `all` = both active and archived."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 },
            "description": "Maximum number of results"
          },
          {
            "name": "offset",
            "in": "query",
            "schema": { "type": "integer", "minimum": 0, "default": 0 },
            "description": "Pagination offset"
          }
        ],
        "responses": {
          "200": {
            "description": "List of projects",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["success"] },
                    "data": {
                      "type": "object",
                      "properties": {
                        "projects": { "type": "array", "items": { "$ref": "#/components/schemas/Project" } },
                        "limit": { "type": "integer" },
                        "offset": { "type": "integer" }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      },
      "post": {
        "summary": "Create project",
        "operationId": "createProject",
        "tags": ["Projects"],
        "security": [{ "ApiKeyAuth": [] }],
        "description": "Create a new project and upload its content. Send as `multipart/form-data`.\n\n**Required:** `title` and at least one file via `files`.\n\nWhen uploading a `.zip`, the system automatically determines how to handle it:\n- **No HTML files** → treated as a file bundle\n- **One HTML file** → treated as a static site (used as homepage)\n- **Multiple HTML files** → you must provide `zipIndexHtmlPath` to specify the homepage\n\nIf `zipIndexHtmlPath` is missing or invalid when multiple HTML files are found, the request will fail and return the list of detected HTML files.\n\nSet `asFileBundle: true` to skip detection entirely and always treat the upload as a file bundle.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["title"],
                "properties": {
                  "title": { "type": "string", "description": "Project display name (required)", "example": "My Project" },
                  "slug": { "type": "string", "description": "URL slug. Auto-generated if omitted.", "example": "my-project" },
                  "domainId": { "type": "string", "format": "uuid", "description": "Assign a custom domain at creation time. The slug becomes a subdomain of that domain." },
                  "workspaceId": { "type": "string", "format": "uuid", "description": "Assign to a workspace." },
                  "access": { "type": "string", "enum": ["public", "password", "private"], "default": "public", "description": "Visibility mode. Use `password` together with the `password` field." },
                  "password": { "type": "string", "maxLength": 255, "description": "Required when `access` is `password`. Visitors must enter this to view the project." },
                  "expiresIn": { "type": "string", "enum": ["15m", "1h", "24h", "7d"], "description": "Relative expiry duration from the moment the project is created. After this time the project becomes inaccessible." },
                  "files": {
                    "type": "array",
                    "items": { "type": "string", "format": "binary" },
                    "description": "One or more files to upload (required). Send a single `.zip` for archive-based projects or multiple individual files. Multiple files with at least one HTML file → static site; no HTML → file bundle."
                  },
                  "zipIndexHtmlPath": { "type": "string", "description": "Home page path inside a ZIP archive, e.g. `dist/index.html`. Required only when the ZIP contains **more than one** HTML file — omitting it in that case returns a `ZIP_MULTIPLE_HTML` error with the list of available paths. Has no effect when the ZIP contains zero or exactly one HTML file." },
                  "asFileBundle": { "type": "boolean", "description": "Set to `true` to skip ZIP auto-detection and always treat the upload as a downloadable file collection, even if it contains HTML files." }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Project created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["success"] },
                    "data": {
                      "type": "object",
                      "properties": {
                        "projectId": { "type": "string", "format": "uuid" },
                        "url": { "type": "string", "format": "uri" },
                        "title": { "type": "string" },
                        "type": { "type": "string" },
                        "slug": { "type": "string" },
                        "expiresAt": { "type": "string", "format": "date-time", "nullable": true },
                        "createdAt": { "type": "string", "format": "date-time" },
                        "quota": {
                          "type": "object",
                          "description": "Account-level quota snapshot after creation. Only active (non-archived) projects count.",
                          "properties": {
                            "projects": {
                              "type": "object",
                              "properties": {
                                "used": { "type": "integer", "description": "Active project count after this creation", "example": 4 },
                                "limit": { "type": "integer", "description": "-1 means unlimited", "example": 10 },
                                "unlimited": { "type": "boolean", "example": false }
                              }
                            },
                            "storage": {
                              "type": "object",
                              "properties": {
                                "usedBytes": { "type": "integer", "description": "Total bytes used across active projects after this upload", "example": 10485760 },
                                "limitBytes": { "type": "integer", "description": "-1 means unlimited", "example": 2147483648 },
                                "unlimited": { "type": "boolean", "example": false }
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error or ZIP ambiguity. When `code` is `ZIP_MULTIPLE_HTML`, the `htmlPaths` array lists all HTML files in the ZIP — retry with `zipIndexHtmlPath` set to one of them.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["error"] },
                    "error": { "type": "string" },
                    "code": { "type": "string", "example": "ZIP_MULTIPLE_HTML" },
                    "htmlPaths": {
                      "type": "array",
                      "items": { "type": "string" },
                      "description": "Present only when `code` is `ZIP_MULTIPLE_HTML`",
                      "example": ["index.html", "about.html", "dist/index.html"]
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "403": { "description": "Plan limit exceeded (`PROJECT_LIMIT_REACHED` or `STORAGE_LIMIT_REACHED`)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/v1/projects/temp": {
      "post": {
        "summary": "Create temporary project",
        "operationId": "createTempProject",
        "tags": ["Projects"],
        "description": "Upload a file or set of files and instantly get a publicly accessible URL \u2014 **no API key required**. This is the fastest path for agents: no sign-in, no code.\n\nThe project is temporary: it expires in **15 minutes** and has no owner. To keep it, give the user `claimUrl`: they open it and sign in with Google or email in the browser. If you already hold an API key you can instead call `POST /v1/projects/claim/{claimToken}`.\n\n**Limits (per request):**\n- Up to **5 files**\n- Total upload size \u2264 **2 MB**\n\n**Rate limits (per IP):**\n- Burst: 3 uploads per minute\n- Hourly: 5 uploads per hour\n\n**ZIP handling:** When uploading a `.zip`, the server auto-detects whether it contains HTML (\u2192 static site) or not (\u2192 file bundle). If the ZIP contains more than one HTML file you must include `zipIndexHtmlPath` to specify the home page, otherwise the request fails with `ZIP_MULTIPLE_HTML` and a list of available paths.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "files": {
                    "type": "array",
                    "items": { "type": "string", "format": "binary" },
                    "description": "One or more files to upload (max 5, total ≤ 2 MB). You may also use the field name `files[]` or `file`."
                  },
                  "zipIndexHtmlPath": {
                    "type": "string",
                    "description": "Required only when the ZIP contains more than one HTML file. Specifies which HTML file to use as the homepage, e.g. `dist/index.html`."
                  },
                  "asFileBundle": {
                    "type": "boolean",
                    "description": "Set to `true` to skip ZIP auto-detection and always treat the upload as a downloadable file collection, even if it contains HTML files."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Upload successful. The project is live immediately at `url` and will auto-expire in 15 minutes unless claimed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["success"] },
                    "data": {
                      "type": "object",
                      "properties": {
                        "projectId": {
                          "type": "string",
                          "format": "uuid",
                          "nullable": true,
                          "description": "UUID of the created project. Use this to reference the project in other endpoints after claiming."
                        },
                        "slug": {
                          "type": "string",
                          "description": "URL slug assigned to the project.",
                          "example": "blue-fox-42"
                        },
                        "url": {
                          "type": "string",
                          "format": "uri",
                          "description": "Public URL where the project is immediately accessible.",
                          "example": "https://blue-fox-42.litepage.site"
                        },
                        "claimToken": {
                          "type": "string",
                          "description": "One-time token to permanently claim this project. Pass it to `POST /v1/projects/claim/{claimToken}` while authenticated. Becomes invalid once claimed or after expiry.",
                          "example": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
                        },
                        "claimUrl": {
                          "type": "string",
                          "format": "uri",
                          "description": "Give this to the user: opening it and signing in (Google or email) keeps the project in their account. No API key needed.",
                          "example": "https://litehost.io/claim/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
                        },
                        "expiresAt": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "When the temporary project will be automatically deleted if unclaimed.",
                          "example": "2026-04-03T01:30:00.000Z"
                        }
                      },
                      "required": ["projectId", "slug", "url", "claimToken", "claimUrl", "expiresAt"]
                    }
                  },
                  "required": ["status", "data"]
                },
                "example": {
                  "status": "success",
                  "data": {
                    "projectId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "slug": "blue-fox-42",
                    "url": "https://blue-fox-42.litepage.site",
                    "claimToken": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
                    "claimUrl": "https://litehost.io/claim/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
                    "expiresAt": "2026-04-03T01:30:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Validation error. When `code` is `ZIP_MULTIPLE_HTML`, retry with `zipIndexHtmlPath` set to one of the paths listed in `htmlPaths`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["error"] },
                    "error": { "type": "string" },
                    "code": { "type": "string", "example": "ZIP_MULTIPLE_HTML" },
                    "htmlPaths": {
                      "type": "array",
                      "items": { "type": "string" },
                      "description": "Present only when `code` is `ZIP_MULTIPLE_HTML`",
                      "example": ["index.html", "about.html"]
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded. Check `Retry-After` header for when to retry.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          },
          "500": {
            "description": "Upload failed on the server side.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          }
        }
      }
    },
    "/v1/projects/claim/{claimToken}": {
      "parameters": [
        {
          "name": "claimToken",
          "in": "path",
          "required": true,
          "schema": { "type": "string" },
          "description": "The `claimToken` returned by `POST /v1/projects/temp`. Single-use — becomes invalid after a successful claim or after the project expires."
        }
      ],
      "post": {
        "summary": "Claim anonymous project",
        "operationId": "claimProject",
        "tags": ["Projects"],
        "security": [{ "ApiKeyAuth": [] }],
        "description": "Permanently transfers an anonymous temporary project to the authenticated user's account.\n\nBefore claiming, the endpoint verifies that the user has not exceeded their **active project count** or **storage quota**. If either limit would be exceeded the request is rejected with `403`.\n\n**TTL extension on claim:**\n- **Free plan** — expiry is extended to **7 days** from the moment of claiming.\n- **Paid plans** — expiry is removed; the project becomes permanent.\n\nThe `claimToken` is invalidated after a successful claim. Claiming the same token a second time returns `409`.",
        "responses": {
          "200": {
            "description": "Project claimed successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["success"] },
                    "data": {
                      "type": "object",
                      "properties": {
                        "projectId": {
                          "type": "string",
                          "format": "uuid",
                          "description": "UUID of the claimed project."
                        },
                        "slug": {
                          "type": "string",
                          "description": "URL slug of the project.",
                          "example": "blue-fox-42"
                        },
                        "url": {
                          "type": "string",
                          "format": "uri",
                          "description": "Public URL of the project.",
                          "example": "https://blue-fox-42.litepage.site"
                        },
                        "expiresAt": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true,
                          "description": "New expiry after claiming. `null` for paid plans (permanent). Free plan receives a 7-day window from the time of claiming.",
                          "example": "2026-04-10T01:15:00.000Z"
                        }
                      },
                      "required": ["projectId", "slug", "url", "expiresAt"]
                    }
                  },
                  "required": ["status", "data"]
                }
              }
            }
          },
          "401": {
            "description": "Invalid or missing API key.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          },
          "403": {
            "description": "Plan limit exceeded. `code` is `PROJECT_LIMIT_REACHED` or `STORAGE_LIMIT_REACHED`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["error"] },
                    "error": { "type": "string" },
                    "code": { "type": "string", "enum": ["PROJECT_LIMIT_REACHED", "STORAGE_LIMIT_REACHED"] }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Claim token not found or already expired.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } }
          },
          "409": {
            "description": "Project has already been claimed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["error"] },
                    "error": { "type": "string" },
                    "code": { "type": "string", "enum": ["ALREADY_CLAIMED"] }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/projects/{projectId}": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": { "type": "string", "format": "uuid" },
          "description": "Project UUID"
        }
      ],
      "get": {
        "summary": "Get project",
        "operationId": "getProject",
        "tags": ["Projects"],
        "security": [{ "ApiKeyAuth": [] }],
        "responses": {
          "200": {
            "description": "Project details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["success"] },
                    "data": { "$ref": "#/components/schemas/Project" }
                  }
                }
              }
            }
          },
          "401": { "description": "Invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "404": { "description": "Project not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      },
      "put": {
        "summary": "Push new version",
        "operationId": "pushProjectVersion",
        "tags": ["Projects"],
        "security": [{ "ApiKeyAuth": [] }],
        "description": "> **Paid plan required.** Not available on the free tier — upgrade to starter or higher.\n\nUpload new content to an existing project. Replaces the live files and increments the deployment version. Previous versions are kept in history.\n\nWhen uploading a `.zip`, the system automatically determines how to handle it:\n- **No HTML files** → treated as a file bundle\n- **One HTML file** → treated as a static site (used as homepage)\n- **Multiple HTML files** → you must provide `zipIndexHtmlPath` to specify the homepage\n\nIf `zipIndexHtmlPath` is missing or invalid when multiple HTML files are found, the request will fail and return the list of detected HTML files.\n\nSet `asFileBundle: true` to skip detection entirely and always treat the upload as a file bundle.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "required": ["files"],
                "properties": {
                  "files": {
                    "type": "array",
                    "items": { "type": "string", "format": "binary" },
                    "description": "One or more files to upload (required). Send a single `.zip` for archive-based projects or multiple individual files."
                  },
                  "zipIndexHtmlPath": { "type": "string", "description": "Home page path inside a ZIP archive, e.g. `dist/index.html`. Required only when the ZIP contains **more than one** HTML file. Omitting it in that case returns a `ZIP_MULTIPLE_HTML` error." },
                  "asFileBundle": { "type": "boolean", "description": "Set to `true` to skip ZIP auto-detection and always treat the upload as a downloadable file collection, even if it contains HTML files." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Deployment successful",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["success"] },
                    "data": {
                      "type": "object",
                      "properties": {
                        "deploymentId": { "type": "string", "example": "dep_v2" },
                        "url": { "type": "string", "format": "uri" }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "403": { "description": "Free-tier restriction.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FreeTierRestricted" } } } },
          "404": { "description": "Project not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      },
      "patch": {
        "summary": "Update project settings",
        "operationId": "updateProject",
        "tags": ["Projects"],
        "security": [{ "ApiKeyAuth": [] }],
        "description": "> **Paid plan required.** Not available on the free tier — upgrade to starter or higher.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": { "type": "string", "minLength": 1, "maxLength": 255 },
                  "slug": { "type": "string", "minLength": 1, "maxLength": 255, "pattern": "^[a-z0-9-]+$", "description": "Lowercase alphanumeric with hyphens" },
                  "domainId": { "type": "string", "format": "uuid", "nullable": true, "description": "Assign a custom domain (null to unset)" },
                  "status": { "type": "string", "enum": ["public", "private"] },
                  "access": { "type": "string", "enum": ["public", "password", "private"], "description": "Visibility mode. Use `password` together with the `password` field." },
                  "password": { "type": "string", "maxLength": 255, "nullable": true, "description": "Required when `access` is `password`. Set to `null` to clear. Has no effect when `access` is not `password`." },
                  "noIndex": { "type": "boolean", "description": "When `true`, adds a `noindex` meta tag to the project page so search engines do not index it. Useful for private or draft content." },
                  "disableDownloads": { "type": "boolean", "description": "When `true`, the download button is hidden on file and document viewer pages. The files are still accessible via direct URL." },
                  "pageTitle": { "type": "string", "nullable": true, "description": "Custom browser tab title and `<title>` tag for the hosted page. Falls back to the project title if `null`." },
                  "metaDescription": { "type": "string", "nullable": true, "description": "SEO meta description shown by search engines and link previews. Set to `null` to clear." },
                  "expiresIn": { "type": "string", "enum": ["15m", "1h", "24h", "7d", "none"], "description": "Update the project expiry. Relative shorthands are calculated from the moment the request is processed. Use `none` to remove an existing expiry." },
                  "analyticsEnabled": { "type": "boolean", "description": "When `true`, page views and visitor data are collected for this project. Set to `false` to stop tracking." },
                  "workspaceId": { "type": "string", "format": "uuid", "nullable": true, "description": "Move the project to a different workspace. Set to `null` to remove it from any workspace (personal scope)." }
                },
                "additionalProperties": false
              },
              "example": {
                "title": "Updated Title",
                "status": "public",
                "noIndex": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Settings updated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["success"] },
                    "data": {
                      "type": "object",
                      "properties": {
                        "updated": { "type": "boolean", "example": true }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "403": { "description": "Free-tier restriction.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FreeTierRestricted" } } } },
          "404": { "description": "Project not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "409": { "description": "Slug already taken", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "422": { "description": "Validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      },
      "delete": {
        "summary": "Delete project",
        "operationId": "deleteProject",
        "tags": ["Projects"],
        "security": [{ "ApiKeyAuth": [] }],
        "description": "Permanently delete a project and all its associated data.",
        "responses": {
          "200": {
            "description": "Project deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["success"] },
                    "data": {
                      "type": "object",
                      "properties": {
                        "deleted": { "type": "boolean", "example": true }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "404": { "description": "Project not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/v1/projects/{projectId}/status": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": { "type": "string", "format": "uuid" }
        }
      ],
      "get": {
        "summary": "Get deployment history",
        "operationId": "getProjectStatus",
        "tags": ["Projects"],
        "security": [{ "ApiKeyAuth": [] }],
        "description": "> **Paid plan required.** Not available on the free tier — upgrade to starter or higher.\n\nReturns all deployment versions for a project, newest first.",
        "responses": {
          "200": {
            "description": "Deployment history",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["success"] },
                    "data": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/Deployment" }
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "403": { "description": "Free-tier restriction.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FreeTierRestricted" } } } },
          "404": { "description": "Project not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/v1/projects/{projectId}/analytics": {
      "get": {
        "summary": "Link opens",
        "operationId": "getProjectAnalytics",
        "tags": ["Projects"],
        "description": "Whether and when the link was opened: totals, last opened time, the 10 latest opens (country, device, browser, OS, referrer) and top countries. Requires a paid plan (Starter or higher); Free returns `FREE_TIER_RESTRICTED`.\n\nAn **open** is a person viewing the link in a browser: the page was on screen and the visitor interacted with it or kept it visible for a few seconds. Link previews (WhatsApp, Slack, iMessage…), bots, email link scanners, AI assistants fetching the link and the owner's own views (opened from the dashboard, or right after publishing) are not opens. `opens`, `lastOpenedAt`, `recentOpens` and `topCountries` only count opens.\n\nOptional fields (added later, may be missing on older deployments): `visits` counts every non-bot page request (link previews and scanners the server could not tell apart included) and is what plan visit limits use; `ownerOpens` counts the owner's own opens.",
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [{ "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }],
        "responses": {
          "200": {
            "description": "Opens for the project.",
            "content": {
              "application/json": {
                "example": {
                  "status": "success",
                  "data": {
                    "projectId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
                    "analyticsEnabled": true,
                    "opens": { "total": 2, "last24h": 1, "last7d": 2 },
                    "lastOpenedAt": "2026-09-28T16:20:00.000Z",
                    "recentOpens": [{ "at": "2026-09-28T16:20:00.000Z", "country": "PE", "device": "Desktop", "browser": "Chrome", "os": "macOS", "referrer": null }],
                    "topCountries": [{ "country": "PE", "opens": 2 }],
                    "visits": { "total": 5, "last24h": 2, "last7d": 5 },
                    "ownerOpens": { "total": 1 }
                  }
                },
                "schema": { "$ref": "#/components/schemas/ProjectOpensResponse" }
              }
            }
          },
          "401": { "description": "Invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "403": { "description": "Needs a paid plan (`FREE_TIER_RESTRICTED` or `FEATURE_LOCKED`).", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "404": { "description": "Project not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/qr/{projectId}.{format}": {
      "get": {
        "summary": "QR code for a project link",
        "operationId": "getProjectQrCode",
        "tags": ["Projects"],
        "description": "A QR code image that opens the project's public link, to print on a menu, flyer or business card. It keeps working when the project gets a new version. No API key: the project id is only known to its owner. Requires the owner to be on a paid plan (Starter or higher). Responses are cached for 5 minutes.",
        "security": [],
        "parameters": [
          { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
          { "name": "format", "in": "path", "required": true, "schema": { "type": "string", "enum": ["png", "svg"] }, "description": "`png` for screens and most printing, `svg` for print shops (scales without blur)." }
        ],
        "responses": {
          "200": {
            "description": "The QR code image.",
            "content": {
              "image/png": { "schema": { "type": "string", "format": "binary" } },
              "image/svg+xml": { "schema": { "type": "string" } }
            }
          },
          "403": { "description": "The owner's plan does not include QR codes (plain text: `QR codes need a paid plan.`)." },
          "404": { "description": "Unknown, archived or expired project." }
        }
      }
    },
    "/v1/projects/{projectId}/archive": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": { "type": "string", "format": "uuid" }
        }
      ],
      "post": {
        "summary": "Archive project",
        "operationId": "archiveProject",
        "tags": ["Projects"],
        "security": [{ "ApiKeyAuth": [] }],
        "description": "> **Paid plan required.** Not available on the free tier — upgrade to starter or higher.\n\nArchives the project. Archived projects are excluded from the active quota (project count and storage). This endpoint is **idempotent** — if the project is already archived a success response is returned immediately with no changes.",
        "responses": {
          "200": {
            "description": "Project archived (or was already archived)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["success"] },
                    "data": {
                      "type": "object",
                      "properties": {
                        "archived": { "type": "boolean", "example": true }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "403": { "description": "Free-tier restriction.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FreeTierRestricted" } } } },
          "404": { "description": "Project not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/v1/projects/{projectId}/unarchive": {
      "parameters": [
        {
          "name": "projectId",
          "in": "path",
          "required": true,
          "schema": { "type": "string", "format": "uuid" }
        }
      ],
      "post": {
        "summary": "Unarchive project",
        "operationId": "unarchiveProject",
        "tags": ["Projects"],
        "security": [{ "ApiKeyAuth": [] }],
        "description": "> **Paid plan required.** Not available on the free tier — upgrade to starter or higher.\n\nRestores an archived project to active status. Before restoring, the plan's **active project count** and **storage limits** are checked — if either would be exceeded the request is rejected with `403`. This endpoint is **idempotent** — if the project is already active a success response is returned immediately.",
        "responses": {
          "200": {
            "description": "Project unarchived (or was already active)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["success"] },
                    "data": {
                      "type": "object",
                      "properties": {
                        "archived": { "type": "boolean", "example": false }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "403": {
            "description": "Free-tier restriction (`FREE_TIER_RESTRICTED`), or plan limit exceeded (`PROJECT_LIMIT_REACHED` / `STORAGE_LIMIT_REACHED`).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["error"] },
                    "error": { "type": "string" },
                    "code": { "type": "string", "enum": ["FREE_TIER_RESTRICTED", "PROJECT_LIMIT_REACHED", "STORAGE_LIMIT_REACHED"] }
                  }
                }
              }
            }
          },
          "404": { "description": "Project not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/v1/domains": {
      "get": {
        "summary": "List domains",
        "operationId": "listDomains",
        "tags": ["Domains"],
        "security": [{ "ApiKeyAuth": [] }],
        "description": "> **Paid plan required.** Not available on the free tier — upgrade to starter or higher.\n\nReturns all custom domains associated with the authenticated user.",
        "responses": {
          "200": {
            "description": "List of domains",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["success"] },
                    "data": {
                      "type": "object",
                      "properties": {
                        "domains": {
                          "type": "array",
                          "items": { "$ref": "#/components/schemas/Domain" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "403": { "description": "Free-tier restriction.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FreeTierRestricted" } } } }
        }
      }
    },
    "/v1/workspaces": {
      "get": {
        "summary": "List workspaces",
        "operationId": "listWorkspaces",
        "tags": ["Workspaces"],
        "security": [{ "ApiKeyAuth": [] }],
        "description": "> **Paid plan required.** Not available on the free tier — upgrade to starter or higher.",
        "responses": {
          "200": {
            "description": "List of workspaces owned by the authenticated user",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "example": "success" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "workspaces": {
                          "type": "array",
                          "items": { "$ref": "#/components/schemas/WorkspaceSummary" }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "403": { "description": "Free-tier restriction.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FreeTierRestricted" } } } }
        }
      },
      "post": {
        "summary": "Create workspace",
        "operationId": "createWorkspace",
        "tags": ["Workspaces"],
        "security": [{ "ApiKeyAuth": [] }],
        "description": "> **Paid plan required.** Not available on the free tier — upgrade to starter or higher.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255,
                    "description": "Display name for the workspace",
                    "example": "My Team"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Workspace created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "example": "success" },
                    "data": { "$ref": "#/components/schemas/WorkspaceSummary" }
                  }
                }
              }
            }
          },
          "401": { "description": "Invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "403": { "description": "Free-tier restriction.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FreeTierRestricted" } } } },
          "422": { "description": "Validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/v1/workspaces/{workspaceId}": {
      "patch": {
        "summary": "Rename workspace",
        "operationId": "renameWorkspace",
        "tags": ["Workspaces"],
        "security": [{ "ApiKeyAuth": [] }],
        "description": "> **Paid plan required.** Not available on the free tier — upgrade to starter or higher.",
        "parameters": [
          {
            "name": "workspaceId",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "ID of the workspace to rename"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255,
                    "description": "New name for the workspace"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Workspace renamed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "example": "success" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "id": { "type": "string" },
                        "name": { "type": "string" },
                        "updatedAt": { "type": "string", "format": "date-time" }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "403": { "description": "Free-tier restriction.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FreeTierRestricted" } } } },
          "404": { "description": "Workspace not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "422": { "description": "Validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      },
      "delete": {
        "summary": "Delete workspace",
        "operationId": "deleteWorkspace",
        "tags": ["Workspaces"],
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          {
            "name": "workspaceId",
            "in": "path",
            "required": true,
            "schema": { "type": "string" },
            "description": "ID of the workspace to delete"
          }
        ],
        "description": "> **Paid plan required.** Not available on the free tier — upgrade to starter or higher.\n\nDeletes the workspace. Projects inside the workspace are not deleted — their `workspaceId` is set to `null` and they become part of the personal space.",
        "responses": {
          "200": {
            "description": "Workspace deleted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "example": "success" },
                    "data": {
                      "type": "object",
                      "properties": {
                        "deleted": { "type": "boolean", "example": true }
                      }
                    }
                  }
                }
              }
            }
          },
          "401": { "description": "Invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "403": { "description": "Free-tier restriction.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FreeTierRestricted" } } } },
          "404": { "description": "Workspace not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    },
    "/v1/domains/{domainId}": {
      "parameters": [
        {
          "name": "domainId",
          "in": "path",
          "required": true,
          "schema": { "type": "string", "format": "uuid" }
        }
      ],
      "get": {
        "summary": "Get domain",
        "operationId": "getDomain",
        "tags": ["Domains"],
        "security": [{ "ApiKeyAuth": [] }],
        "description": "> **Paid plan required.** Not available on the free tier — upgrade to starter or higher.\n\nReturns domain details including all projects assigned to it.",
        "responses": {
          "200": {
            "description": "Domain details",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["success"] },
                    "data": { "$ref": "#/components/schemas/DomainDetail" }
                  }
                }
              }
            }
          },
          "401": { "description": "Invalid or missing API key", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } },
          "403": { "description": "Free-tier restriction.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/FreeTierRestricted" } } } },
          "404": { "description": "Domain not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }
        }
      }
    }
  },
  "tags": [
    { "name": "System", "description": "Health and metadata endpoints" },
    { "name": "Authentication", "description": "OTP-based sign-in flow for desktop apps and AI agents. No dashboard or browser required — request a code by email, verify it, and receive a short-lived API key." },
    { "name": "User", "description": "Authenticated user info, plan, and quota" },
    { "name": "Projects", "description": "Create, deploy, and manage projects" },
    { "name": "Domains", "description": "View custom domains and their associated projects" },
    { "name": "Workspaces", "description": "Create and manage workspaces to organise projects" }
  ]
}
