{
  "openapi": "3.1.0",
  "info": {
    "title": "VibeScan Free URL Preview API",
    "version": "1.0.0",
    "description": "Run one first-pass public-surface scan of a deployed app the user owns or has explicit permission to test. This published contract covers the existing anonymous free URL preview only. It does not buy credits, run a paid scan, or certify security.",
    "termsOfService": "https://vibe-scan.app/terms",
    "contact": { "email": "support@vibe-scan.app", "url": "https://vibe-scan.app/support" }
  },
  "servers": [{ "url": "https://vibe-scan.app" }],
  "externalDocs": { "url": "https://vibe-scan.app/developers" },
  "paths": {
    "/api/scan": {
      "post": {
        "operationId": "runAuthorizedFreeUrlPreview",
        "summary": "Run one authorized free URL security preview",
        "description": "Confirm target ownership or permission before calling. Do not submit credentials, private URLs or customer records. Honor quotas and Retry-After. HTTP 200 is a transport result: inspect data.status; blocked, timeout and failed do not mean a clean scan. Free findings redact detailed fix prompts. A scan can make outbound HTTP requests and save a report.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": { "application/json": {
            "schema": {
              "type": "object", "required": ["type", "payload"], "additionalProperties": false,
              "properties": {
                "type": { "type": "string", "const": "url" },
                "payload": { "type": "string", "format": "uri", "minLength": 1, "maxLength": 2048, "description": "An authorized public HTTP or HTTPS URL." }
              }
            },
            "example": { "type": "url", "payload": "https://your-app.example" }
          } }
        },
        "responses": {
          "200": {
            "description": "Scan response. Always inspect data.status and the findings; an empty list is not a security certification.",
            "headers": {
              "X-RateLimit-Limit": { "schema": { "type": "integer" } },
              "X-RateLimit-Remaining": { "schema": { "type": "integer" } },
              "X-RateLimit-Reset": { "schema": { "type": "integer" } }
            },
            "content": { "application/json": { "schema": {
              "type": "object", "required": ["success", "data", "scan_id"],
              "properties": {
                "success": { "type": "boolean", "const": true },
                "scan_id": { "type": ["string", "null"], "description": "Saved report identifier, or null if persistence was unavailable. Keep private." },
                "data": {
                  "type": "object", "required": ["id", "url", "platform", "status", "summary", "findings"],
                  "properties": {
                    "id": { "type": "string" },
                    "url": { "type": "string" },
                    "platform": { "type": "string" },
                    "status": { "type": "string", "enum": ["complete", "failed", "blocked", "timeout"] },
                    "error": { "type": "string" },
                    "summary": {
                      "type": "object", "required": ["critical", "high", "medium", "low", "info", "passed", "inconclusive", "skipped"],
                      "properties": {
                        "critical": { "type": "integer", "minimum": 0 },
                        "high": { "type": "integer", "minimum": 0 },
                        "medium": { "type": "integer", "minimum": 0 },
                        "low": { "type": "integer", "minimum": 0 },
                        "info": { "type": "integer", "minimum": 0 },
                        "passed": { "type": "integer", "minimum": 0 },
                        "inconclusive": { "type": "integer", "minimum": 0 },
                        "skipped": { "type": "integer", "minimum": 0 }
                      }
                    },
                    "findings": { "type": "array", "items": {
                      "type": "object", "required": ["id", "severity", "category", "check_name", "title", "description", "fix_prompt"],
                      "properties": {
                        "id": { "type": "string" },
                        "severity": { "type": "string", "enum": ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO", "PASS", "INCONCLUSIVE", "SKIPPED"] },
                        "category": { "type": "string" },
                        "check_name": { "type": "string" },
                        "title": { "type": "string" },
                        "description": { "type": "string" },
                        "affected_resource": { "type": "string" },
                        "fix_prompt": { "type": "string", "description": "A paid-unlock message in the free preview, not the detailed remediation prompt." }
                      }
                    } }
                  }
                }
              }
            } } }
          },
          "400": { "$ref": "#/components/responses/Error" },
          "413": { "$ref": "#/components/responses/Error" },
          "429": {
            "description": "Free quota exceeded. Wait for Retry-After; do not evade the quota.",
            "headers": { "Retry-After": { "schema": { "type": "integer", "minimum": 0 }, "description": "Seconds to wait." } },
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
          },
          "500": { "$ref": "#/components/responses/Error" }
        }
      }
    }
  },
  "components": {
    "schemas": { "Error": {
      "type": "object", "required": ["error"],
      "properties": { "error": { "type": "string" }, "code": { "type": "string" }, "retryAfterSeconds": { "type": "number" } }
    } },
    "responses": { "Error": {
      "description": "The request was invalid, too large, or could not be completed. No security conclusion can be drawn from this error.",
      "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error" } } }
    } }
  }
}
