{
  "openapi": "3.1.0",
  "info": {
    "title": "KodExport public tools API",
    "version": "2026-10-25",
    "description": "Public, unauthenticated scan, source fetch, and single-page ZIP export operations. This description covers free tools and health only; checkout, paid exports, hosting, deployment, and internal operations are outside its scope. Source requests do not execute JavaScript.",
    "contact": { "url": "https://kodexport.com/contact/" }
  },
  "servers": [{ "url": "https://api.kodexport.com" }],
  "externalDocs": { "url": "https://kodexport.com/api/" },
  "security": [],
  "paths": {
    "/health": {
      "get": {
        "operationId": "getHealth",
        "summary": "Check whether the API is responding",
        "responses": {
          "200": {
            "description": "API is responding. This does not verify downstream Stripe, storage, or browser services.",
            "content": { "application/json": { "schema": {
              "type": "object",
              "required": ["status", "service"],
              "properties": {
                "status": { "type": "string", "const": "ok" },
                "service": { "type": "string", "const": "kodexport-api" }
              }
            } } }
          }
        }
      }
    },
    "/scan": {
      "get": {
        "operationId": "scanSite",
        "summary": "Detect a supported platform, assets, and integrations",
        "description": "Supports Framer, Bubble, WordPress, Wix, and Webflow public pages. Limits per 24-hour window: 60 requests per IP and 20 per IP/domain pair. Results are static HTML signals, not a full runtime audit.",
        "parameters": [{ "$ref": "#/components/parameters/SourceUrl" }],
        "responses": {
          "200": {
            "description": "Platform and migration signals detected in the fetched page.",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScanResult" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "422": { "$ref": "#/components/responses/UnsupportedPage" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": { "$ref": "#/components/responses/UpstreamError" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      }
    },
    "/fetch": {
      "get": {
        "operationId": "fetchSource",
        "summary": "Download source HTML or text from a public URL",
        "description": "Accepts public text resources from any platform, including HTML, XML, JSON, plain text, and SVG. The response is always served as a text/html attachment; X-Source-Content-Type identifies the upstream media type. Limit: 1000 requests per IP per 24-hour window; source body limit: 8 MiB. JavaScript is not executed.",
        "parameters": [{ "$ref": "#/components/parameters/SourceUrl" }],
        "responses": {
          "200": {
            "description": "Decoded source text served as a download.",
            "headers": {
              "Content-Disposition": { "schema": { "type": "string" }, "description": "Attachment filename derived from the source hostname." },
              "X-Source-Content-Type": { "schema": { "type": "string" }, "description": "Original upstream content type." },
              "X-Source-Bytes": { "schema": { "type": "integer" }, "description": "UTF-8 byte count of the returned text." },
              "X-Thin-Shell": { "schema": { "type": "string", "const": "true" }, "description": "Present for a small response that may be a client-rendered shell." }
            },
            "content": { "text/html": { "schema": { "type": "string" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "413": { "description": "Source exceeds the 8 MiB body limit.", "content": { "text/plain": { "schema": { "type": "string" } } } },
          "415": { "description": "Source is a binary or unsupported media type.", "content": { "text/plain": { "schema": { "type": "string" } } } },
          "422": { "description": "Source returned no content.", "content": { "text/plain": { "schema": { "type": "string" } } } },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": { "$ref": "#/components/responses/UpstreamError" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      }
    },
    "/export": {
      "get": {
        "operationId": "exportPage",
        "summary": "Download one supported public page and its assets as a ZIP",
        "description": "Supports Framer, Bubble, WordPress, Wix, and Webflow. Limits per 24-hour window: 25 requests per IP and 10 per IP/domain pair. Downloads up to 500 assets within a 24 MiB asset budget; individual assets over 10 MiB are skipped. Source HTML is limited to 8 MiB. Only downloaded assets are rewritten to local paths; skipped assets keep remote URLs. Does not export backend logic or execute JavaScript.",
        "parameters": [{ "$ref": "#/components/parameters/SourceUrl" }],
        "responses": {
          "200": {
            "description": "ZIP containing index.html and downloaded files under _assets/.",
            "headers": {
              "Content-Disposition": { "schema": { "type": "string" }, "description": "ZIP attachment filename." },
              "X-Asset-Count": { "schema": { "type": "integer" }, "description": "Number of downloaded assets." },
              "X-Skipped-Asset-Count": { "schema": { "type": "integer" }, "description": "Number of skipped assets." }
            },
            "content": { "application/zip": { "schema": { "type": "string", "format": "binary" } } }
          },
          "400": { "$ref": "#/components/responses/BadRequest" },
          "422": { "$ref": "#/components/responses/UnsupportedPage" },
          "429": { "$ref": "#/components/responses/RateLimited" },
          "502": { "$ref": "#/components/responses/UpstreamError" },
          "503": { "$ref": "#/components/responses/ServiceUnavailable" }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "SourceUrl": {
        "name": "url",
        "in": "query",
        "required": true,
        "description": "Public HTTP(S) URL, URL-encoded as a query parameter. No credentials, private/loopback hosts, or ports other than 80/443. Redirect targets are also validated.",
        "schema": { "type": "string", "format": "uri" },
        "example": "https://example.framer.website/"
      }
    },
    "schemas": {
      "ScanResult": {
        "type": "object",
        "required": ["status", "platform", "url", "domain", "title", "ready", "stats", "integrations", "warnings"],
        "properties": {
          "status": { "type": "string", "const": "ok" },
          "platform": { "type": "string", "enum": ["framer", "bubble", "wordpress", "wix", "webflow"] },
          "url": { "type": "string", "format": "uri" },
          "domain": { "type": "string" },
          "title": { "type": ["string", "null"] },
          "ready": { "type": "boolean", "description": "True when this scan produced no warnings." },
          "stats": {
            "type": "object",
            "required": ["htmlBytes", "assetCount", "internalLinkCount", "externalScriptCount"],
            "properties": {
              "htmlBytes": { "type": "integer", "minimum": 0 },
              "assetCount": { "type": "integer", "minimum": 0 },
              "internalLinkCount": { "type": "integer", "minimum": 0 },
              "externalScriptCount": { "type": "integer", "minimum": 0, "description": "Number of distinct external script hostnames." }
            }
          },
          "integrations": { "type": "array", "items": { "type": "string" } },
          "warnings": { "type": "array", "items": { "type": "string" } }
        }
      }
    },
    "responses": {
      "BadRequest": { "description": "Missing or unsafe source URL, or an unexpected source type.", "content": { "text/plain": { "schema": { "type": "string" } } } },
      "UnsupportedPage": { "description": "Unsupported platform or page content unsuitable for this operation.", "content": { "text/plain": { "schema": { "type": "string" } } } },
      "RateLimited": {
        "description": "IP or IP/domain quota exceeded. Wait before retrying.",
        "headers": { "Retry-After": { "schema": { "type": "integer", "const": 86400 } } },
        "content": { "text/plain": { "schema": { "type": "string" } } }
      },
      "UpstreamError": { "description": "Source retrieval failed, timed out, or exceeded a source limit.", "content": { "text/plain": { "schema": { "type": "string" } } } },
      "ServiceUnavailable": { "description": "Unexpected service failure.", "content": { "application/json": { "schema": { "type": "object", "required": ["error", "requestId"], "properties": { "error": { "type": "string" }, "requestId": { "type": "string" } } } } } }
    }
  }
}
