{
  "openapi": "3.0.3",
  "info": {
    "title": "PitchStation Websites API",
    "version": "2026-07-15",
    "description": "Programmatic website creation for API tenants: generate a site from a content model, run the customer/compliance approval loop, publish to a VPS with its own domain + TLS, read aggregate visitor metrics, and tear down on churn. Authentication: Personal Access Token (pst_…) as `Authorization: Bearer`. Correlate objects with your own `external_ref` (unique per account, replay-safe) and `metadata` (≤20 scalar keys, ≤2KB, echoed in webhooks, never rendered). Webhook events: website.generated, website.submitted, website.approved, website.changes_requested, website.published, website.unpublished — HMAC-signed like invoice events."
  },
  "servers": [{ "url": "https://www.pitchstation.ai" }],
  "components": {
    "securitySchemes": { "pat": { "type": "http", "scheme": "bearer", "description": "Personal Access Token (pst_…)" } },
    "schemas": {
      "SiteCreate": {
        "type": "object",
        "required": ["content", "assets"],
        "properties": {
          "content": { "type": "object", "description": "The site content model. Required: meta.company_name. Notable meta fields: lang (zh-Hant|zh-Hans|en — localizes the site chrome), template (visual template id; server-enforced when your account has a template lock), hero_layout (diamond|full), metrics (false disables the visitor beacon). Sections: hero, about, facts, services, team, positioning, advantages, values, vision, mission, roadmap, insights, faq, contact." },
          "assets": { "type": "object", "description": "logo (required, data URI or base64), hero, materials[] ({image, caption, section}) — photos are recompressed server-side (≤1600px) before the 6MB embed cap." },
          "deckId": { "type": "string", "description": "Regenerate an existing site in place (same URL, same analytics)." },
          "external_ref": { "type": "string", "maxLength": 128, "description": "Your key for this site (e.g. agent-12345). Unique per account; charset A-Za-z0-9._:/-. Replaying a create with a known ref returns the existing site with replayed:true — sites are mutable, use update to change them." },
          "metadata": { "type": "object", "description": "≤20 scalar keys, ≤2KB serialized. Echoed in every webhook; never rendered into the site." }
        }
      },
      "Error": { "type": "object", "properties": { "success": { "type": "boolean" }, "code": { "type": "string" }, "error": { "type": "string" } } }
    }
  },
  "security": [{ "pat": [] }],
  "paths": {
    "/api/website/generate": {
      "post": {
        "summary": "Create a site (or regenerate in place)",
        "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SiteCreate" } } } },
        "responses": {
          "200": { "description": "success — {deckId, previewUrl, bundleUrl}; replayed:true when a known external_ref short-circuited; templateLocked:true when your account's template lock rewrote meta.template" },
          "400": { "description": "BAD_EXTERNAL_REF | BAD_METADATA | missing company_name/logo" },
          "429": { "description": "SITE_QUOTA — plan site limit reached ({limit, current} in body)" }
        }
      }
    },
    "/api/website/list": {
      "get": {
        "summary": "List your sites",
        "parameters": [{ "name": "external_ref", "in": "query", "schema": { "type": "string" }, "description": "Exact-match filter by your ref" }],
        "responses": { "200": { "description": "{websites: [{deckId, name, external_ref, metadata, reviewStatus, round, shared?}]}" } }
      }
    },
    "/api/website/templates": {
      "get": { "summary": "Visual template catalog", "responses": { "200": { "description": "{templates:[{id,label,desc}], templateLock?} — partner templates appear only for accounts locked to them" } } }
    },
    "/api/website/{deckId}/content": {
      "get": {
        "summary": "Read the saved content model",
        "parameters": [
          { "name": "deckId", "in": "path", "required": true, "schema": { "type": "string" } },
          { "name": "assets", "in": "query", "schema": { "type": "string", "enum": ["1"] }, "description": "Include saved assets (data URIs)" }
        ],
        "responses": { "200": { "description": "{content, hasAssets, external_ref, metadata, isOwner}" } }
      }
    },
    "/api/website/{deckId}/update": {
      "post": {
        "summary": "Save edited content and regenerate in place",
        "parameters": [{ "name": "deckId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "responses": { "200": { "description": "Regenerated; transitioned:'revising' when the edit put a customer approval on hold" } }
      }
    },
    "/api/website/{deckId}/domain": {
      "get": { "summary": "Read the reserved site address", "parameters": [{ "name": "deckId", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "{subdomain, suggested}" } } },
      "put": {
        "summary": "Reserve the site address",
        "parameters": [{ "name": "deckId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "subdomain": { "type": "string", "description": "Bare label (→ label.<target base domain>, created at deploy) or a full domain you control (verified at deploy). Reserved platform-wide." } } } } } },
        "responses": { "200": { "description": "reserved" }, "409": { "description": "SUBDOMAIN_TAKEN" } }
      }
    },
    "/api/website/{deckId}/submit-review": {
      "post": {
        "summary": "Mint/reuse the approval link (compliance loop)",
        "parameters": [{ "name": "deckId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "properties": { "approverEmail": { "type": "string" }, "password": {}, "expiresIn": { "type": "string" } } } } } },
        "responses": { "201": { "description": "{reviewUrl, round, …} — decisions arrive via website.approved / website.changes_requested webhooks; a site cannot publish unapproved" } }
      }
    },
    "/api/website/{deckId}/publish": {
      "post": {
        "summary": "Deploy to a VPS (dry-run plan by default)",
        "parameters": [{ "name": "deckId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "required": ["targetId"], "properties": { "targetId": { "type": "integer" }, "dryRun": { "type": "boolean", "default": true }, "releaseTs": { "type": "string" } } } } } },
        "responses": {
          "200": { "description": "Plan (dry-run) or live result {ok, hostname, liveUrl, queued, log}. Publishes to one target serialize FIFO." },
          "409": { "description": "NOT_APPROVED — the approval gate" },
          "429": { "description": "PUBLISH_QUOTA — daily live-publish plan limit" }
        }
      }
    },
    "/api/website/{deckId}/unpublish": {
      "post": {
        "summary": "Stop serving a published site (churn teardown)",
        "description": "Removes the vhost (and the platform-created DNS record when a provider is configured); the site's releases stay on the server so re-publishing is instant. Emits website.unpublished.",
        "parameters": [{ "name": "deckId", "in": "path", "required": true, "schema": { "type": "string" } }],
        "requestBody": { "content": { "application/json": { "schema": { "type": "object", "required": ["targetId"], "properties": { "targetId": { "type": "integer" } } } } } },
        "responses": { "200": { "description": "{ok, hostname, dnsRemoved}" } }
      }
    },
    "/api/website/{deckId}/rollback": {
      "post": { "summary": "Roll back to the previous release", "parameters": [{ "name": "deckId", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "rolled back" } } }
    },
    "/api/website/{deckId}/metrics": {
      "get": {
        "summary": "Aggregate visitor metrics for the published site",
        "parameters": [{ "name": "deckId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "days", "in": "query", "schema": { "type": "integer", "default": 30 } }],
        "responses": { "200": { "description": "{totalVisits, visitsByDay, topSections, topReferrers} — cookie-less, DNT-honoring, aggregate only" } }
      }
    },
    "/api/website/{deckId}/collaborators": {
      "get": { "summary": "Roster (owner + co-managers)", "parameters": [{ "name": "deckId", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "{owner, collaborators, isOwner}" } } }
    }
  }
}
