{
  "openapi": "3.1.0",
  "info": {
    "title": "Shotly Screenshot API",
    "version": "2.1.0",
    "description": "Public HTTP API for screenshots, Evidence Bundles, visual QA release gates, raw HTML or Markdown rendering, cache, opaque render links, batches, and localization matrices."
  },
  "servers": [
    {
      "url": "https://shotly.cloud"
    }
  ],
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key"
      }
    },
    "schemas": {
      "CreateScreenshotRequest": {
        "type": "object",
        "description": "Exactly one source must be supplied: url, html, or markdown.",
        "oneOf": [
          { "required": ["url"] },
          { "required": ["html"] },
          { "required": ["markdown"] }
        ],
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "example": "https://stripe.com"
          },
          "format": {
            "type": "string",
            "enum": ["png", "jpeg", "jpg", "webp", "pdf"],
            "default": "png",
            "description": "Output format. PDF is Chromium-only and uses fixed A4 portrait pages, screen media, background graphics, zero margins, no headers or footers, all pages, and ignores CSS @page size.",
            "example": "png"
          },
          "imageQuality": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100,
            "example": 85,
            "description": "Optional JPEG or WebP encoding quality. Integer from 1 through 100. Omission preserves automatic Chromium quality without a public numeric default. Rejected for PNG and PDF. Chromium only."
          },
          "fullPage": {
            "type": "boolean",
            "default": true,
            "description": "Capture the entire page when true, or the viewport when false. Must be false or omitted when selector is present. PDF requests must omit this field or set it to false.",
            "example": true
          },
          "selector": {
            "type": "string",
            "maxLength": 256,
            "example": ".pricing-card",
            "description": "Optional CSS selector of the element to capture. Shotly captures the first visible matching element at its actual bounds. Leading and trailing whitespace is trimmed. When present, fullPage must be false or omitted; an omitted fullPage resolves to false. Not supported with PDF. Chromium only."
          },
          "hideSelectors": {
            "type": "array",
            "maxItems": 10,
            "items": {
              "type": "string",
              "maxLength": 256
            },
            "example": [".cookie-banner", "#chat-widget"],
            "description": "Optional ordered list of ordinary CSS selectors. Leading and trailing whitespace is trimmed and all matching elements are hidden with display: none !important after readiness and delay, before image, element, or PDF capture. Missing matches are ignored. Maximum combined selector text is 1024 Unicode characters. Invalid CSS causes the safe task error INVALID_HIDE_SELECTOR. Chromium only."
          },
          "browser": {
            "type": "string",
            "enum": ["chromium"],
            "default": "chromium",
            "example": "chromium"
          },
          "viewportWidth": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4096,
            "default": 1280,
            "example": 390,
            "description": "Viewport width in CSS pixels. Width and height must be supplied together."
          },
          "viewportHeight": {
            "type": "integer",
            "minimum": 1,
            "maximum": 4096,
            "default": 720,
            "example": 844,
            "description": "Viewport height in CSS pixels. Width and height must be supplied together."
          },
          "deviceScaleFactor": {
            "type": "integer",
            "enum": [1, 2],
            "default": 1,
            "example": 2,
            "description": "Device pixel ratio. Output dimensions are multiplied by this factor. PDF accepts only omitted or 1."
          },
          "waitUntil": {
            "type": "string",
            "enum": ["load", "domcontentloaded", "networkidle"],
            "example": "networkidle",
            "description": "Optional exact Chromium navigation readiness event. Omitting the field preserves the legacy-compatible automatic mode: navigate to DOMContentLoaded, then try to reach network idle for up to 5 seconds and continue if settling expires. Explicit values do not add that automatic settle step. Network idle may time out on continuously connected pages."
          },
          "delayMs": {
            "type": "integer",
            "minimum": 0,
            "maximum": 15000,
            "default": 0,
            "example": 2000,
            "description": "Additional delay after navigation and selector waiting, immediately before capture. Increases processing time and is limited to 15000 ms."
          },
          "waitForSelector": {
            "type": "string",
            "maxLength": 256,
            "example": ".dashboard-ready",
            "description": "Optional CSS selector. Waits until at least one match is attached to the DOM; visibility is not required."
          },
          "webhook": {
            "type": "boolean",
            "default": false,
            "description": "Opt in to delivery of screenshot.done or screenshot.error to the enabled account webhook endpoint configured in the dashboard."
          },
          "externalId": {
            "type": "string",
            "maxLength": 128,
            "description": "Optional caller correlation id. Requires webhook=true and is echoed in the webhook payload and task response."
          },
          "captureProfileId": {
            "type": ["string", "null"],
            "format": "uuid",
            "description": "PAID-only capture profile for authenticated pages."
          },
          "networkProfileId": {
            "type": ["string", "null"],
            "format": "uuid",
            "description": "Tenant-owned localization profile that snapshots locale, timezone, and optional geolocation into the capture."
          },
          "bundle": {
            "oneOf": [
              { "$ref": "#/components/schemas/EvidenceBundleRequest" },
              { "type": "null" }
            ],
            "description": "Optional sanitized evidence artifacts generated with the screenshot."
          },
          "actions": {
            "type": ["array", "null"],
            "maxItems": 10,
            "items": { "$ref": "#/components/schemas/BrowserAction" },
            "description": "PAID-only ordered browser actions. Arbitrary JavaScript is not supported."
          },
          "destinationId": {
            "type": ["string", "null"],
            "format": "uuid",
            "description": "PAID-only S3-compatible destination for the completed result."
          },
          "html": {
            "type": ["string", "null"],
            "maxLength": 1048576,
            "description": "Raw HTML source. Mutually exclusive with url and markdown. Scripts are disabled unless allowScripts is true."
          },
          "markdown": {
            "type": ["string", "null"],
            "maxLength": 1048576,
            "description": "CommonMark source. Mutually exclusive with url and html; raw HTML in Markdown is disabled."
          },
          "allowScripts": {
            "type": "boolean",
            "default": false
          },
          "cache": {
            "type": "boolean",
            "default": false,
            "description": "Reuse a tenant-scoped completed render when possible. Cache hits do not consume screenshot quota."
          },
          "cacheTtlSeconds": {
            "type": "integer",
            "minimum": 60,
            "maximum": 2592000,
            "default": 3600,
            "description": "Cache lifetime. FREE is capped at 86400 seconds; PAID at 2592000 seconds."
          },
          "cacheKey": {
            "type": ["string", "null"],
            "maxLength": 256,
            "description": "Optional tenant-scoped logical cache key."
          },
          "forceRefresh": {
            "type": "boolean",
            "default": false,
            "description": "Bypass a reusable cache entry and create a fresh render."
          },
          "blockAds": { "type": "boolean", "default": false },
          "blockTrackers": { "type": "boolean", "default": false },
          "blockChatWidgets": { "type": "boolean", "default": false },
          "blockCookieBanners": { "type": "boolean", "default": false },
          "customCss": {
            "type": ["string", "null"],
            "description": "CSS injected before capture."
          },
          "colorScheme": {
            "type": ["string", "null"],
            "enum": ["light", "dark", "no-preference", null]
          },
          "reducedMotion": { "type": "boolean", "default": false },
          "locale": { "type": ["string", "null"], "example": "ru-RU" },
          "timezone": { "type": ["string", "null"], "example": "Europe/Moscow" },
          "userAgent": { "type": ["string", "null"] },
          "mediaType": {
            "type": ["string", "null"],
            "enum": ["screen", "print", null]
          },
          "blockedResourceTypes": {
            "type": ["array", "null"],
            "items": {
              "type": "string",
              "enum": ["document", "stylesheet", "image", "media", "font", "script", "xhr", "fetch", "websocket", "other"]
            }
          },
          "maskSelectors": {
            "type": ["array", "null"],
            "items": { "type": "string" },
            "description": "Selectors masked consistently for visual comparison."
          }
        }
      },
      "CreateScreenshotResponse": {
        "type": "object",
        "required": ["taskId"],
        "properties": {
          "taskId": {
            "type": "string",
            "format": "uuid"
          },
          "cacheStatus": {
            "type": ["string", "null"],
            "enum": ["HIT", "MISS", "BYPASS", null],
            "description": "Cache decision made while accepting the request."
          }
        }
      },
      "EvidenceBundleRequest": {
        "type": "object",
        "required": ["artifacts"],
        "properties": {
          "artifacts": {
            "type": "array",
            "minItems": 1,
            "maxItems": 3,
            "uniqueItems": true,
            "items": { "type": "string", "enum": ["html", "markdown", "metadata"] }
          }
        }
      },
      "EvidenceBundleResponse": {
        "type": "object",
        "required": ["id", "taskId", "status", "requestedArtifacts", "totalSize", "createdAt", "artifacts"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "taskId": { "type": "string", "format": "uuid" },
          "status": { "type": "string", "enum": ["PENDING", "READY", "FAILED"] },
          "requestedArtifacts": { "type": "array", "items": { "type": "string", "enum": ["html", "markdown", "metadata"] } },
          "totalSize": { "type": "integer", "format": "int64" },
          "createdAt": { "type": "string", "format": "date-time" },
          "completedAt": { "type": ["string", "null"], "format": "date-time" },
          "artifacts": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["kind", "contentType", "size", "sha256", "downloadUrl"],
              "properties": {
                "kind": { "type": "string", "enum": ["html", "markdown", "metadata"] },
                "contentType": { "type": "string" },
                "size": { "type": "integer" },
                "sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
                "downloadUrl": { "type": "string", "format": "uri" }
              }
            }
          }
        }
      },
      "BrowserAction": {
        "type": "object",
        "required": ["type"],
        "properties": {
          "type": {
            "type": "string",
            "enum": ["click", "type", "scroll", "waitForSelector"]
          },
          "selector": {
            "type": ["string", "null"],
            "description": "CSS selector used by click, type, or waitForSelector."
          },
          "value": {
            "type": ["string", "null"],
            "description": "Plain value for type. Do not combine with secretRef."
          },
          "secretRef": {
            "type": ["string", "null"],
            "description": "Name of a secret stored in the selected capture profile."
          },
          "amount": {
            "type": ["integer", "null"],
            "description": "Scroll amount in CSS pixels."
          }
        }
      },
      "BatchItemRequest": {
        "allOf": [
          { "$ref": "#/components/schemas/CreateScreenshotRequest" },
          {
            "type": "object",
            "properties": {
              "actions": {
                "type": ["array", "null"],
                "maxItems": 10,
                "items": { "$ref": "#/components/schemas/BrowserAction" },
                "description": "Ordered browser actions. Arbitrary JavaScript is not supported."
              }
            }
          }
        ]
      },
      "CreateBatchRequest": {
        "type": "object",
        "required": ["items"],
        "properties": {
          "items": {
            "type": "array",
            "minItems": 1,
            "maxItems": 100,
            "items": { "$ref": "#/components/schemas/BatchItemRequest" },
            "description": "FREE accepts up to 5 items; PAID accepts up to 100."
          },
          "captureProfileId": {
            "type": ["string", "null"],
            "format": "uuid",
            "description": "PAID-only capture profile applied to every item."
          },
          "destinationId": {
            "type": ["string", "null"],
            "format": "uuid",
            "description": "PAID-only S3-compatible destination applied to every item."
          }
        }
      },
      "Delivery": {
        "type": "object",
        "required": ["destinationId", "status"],
        "properties": {
          "destinationId": { "type": "string", "format": "uuid" },
          "status": {
            "type": "string",
            "enum": ["PENDING", "DELIVERED", "FAILED"]
          },
          "errorMessage": {
            "type": ["string", "null"],
            "description": "Safe delivery failure classification when available."
          }
        }
      },
      "BatchItemResponse": {
        "type": "object",
        "required": ["index", "taskId", "status"],
        "properties": {
          "index": { "type": "integer", "minimum": 0 },
          "taskId": { "type": "string", "format": "uuid" },
          "status": {
            "type": "string",
            "enum": ["PENDING", "IN_PROGRESS", "DONE", "ERROR"]
          },
          "errorMessage": { "type": ["string", "null"] },
          "resultUrl": { "type": ["string", "null"], "format": "uri" },
          "deliveries": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Delivery" }
          }
        }
      },
      "BatchResponse": {
        "type": "object",
        "required": ["id", "status", "itemCount", "createdAt", "items"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "status": {
            "type": "string",
            "enum": ["PENDING", "RUNNING", "DONE", "PARTIAL_ERROR", "ERROR"]
          },
          "itemCount": { "type": "integer", "minimum": 1 },
          "createdAt": { "type": "string", "format": "date-time" },
          "items": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/BatchItemResponse" }
          }
        }
      },
      "ScreenshotResponse": {
        "type": "object",
        "required": ["taskId", "status", "waitUntil", "delayMs", "waitForSelector", "selector", "hideSelectors", "imageQuality", "createdAt", "updatedAt"],
        "properties": {
          "taskId": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": ["PENDING", "IN_PROGRESS", "DONE", "ERROR"]
          },
          "resultUrl": {
            "type": ["string", "null"],
            "format": "uri"
          },
          "errorMessage": {
            "type": ["string", "null"],
            "description": "Safe task failure classification when available, including selector, navigation, readiness, and render failures."
          },
          "payloadSize": {
            "type": ["integer", "null"],
            "format": "int32"
          },
          "waitUntil": {
            "type": ["string", "null"],
            "enum": ["load", "domcontentloaded", "networkidle", null],
            "description": "Selected navigation readiness event, or null for the legacy-compatible automatic mode, including historical tasks."
          },
          "delayMs": {
            "type": "integer",
            "minimum": 0,
            "maximum": 15000,
            "description": "Resolved delay in milliseconds. Historical tasks resolve to 0."
          },
          "waitForSelector": {
            "type": ["string", "null"],
            "maxLength": 256,
            "description": "Resolved selector, or null when selector waiting is disabled."
          },
          "selector": {
            "type": ["string", "null"],
            "maxLength": 256,
            "description": "Resolved element capture selector, or null for full-page and viewport captures, including historical tasks."
          },
          "hideSelectors": {
            "type": ["array", "null"],
            "maxItems": 10,
            "items": {
              "type": "string",
              "maxLength": 256
            },
            "description": "Normalized ordered selectors hidden before capture, or null for historical tasks and requests without element hiding."
          },
          "imageQuality": {
            "type": ["integer", "null"],
            "minimum": 1,
            "maximum": 100,
            "description": "Resolved JPEG or WebP encoding quality, or null when automatic quality applies, including historical tasks."
          },
          "externalId": {
            "type": ["string", "null"],
            "maxLength": 128,
            "description": "Caller correlation id supplied when webhook delivery was requested."
          },
          "deliveries": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/Delivery" },
            "description": "External S3 delivery states. Empty when no destination was requested."
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "updatedAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": ["message"],
        "properties": {
          "message": {
            "type": "string"
          }
        }
      },
      "CodedErrorResponse": {
        "type": "object",
        "required": ["code", "message"],
        "properties": {
          "code": {
            "type": "string",
            "examples": [
              "INVALID_REQUEST",
              "VIEWPORT_DIMENSIONS_REQUIRED",
              "VIEWPORT_DIMENSIONS_INVALID",
              "VIEWPORT_TOO_LARGE",
              "DEVICE_SCALE_FACTOR_UNSUPPORTED",
              "INVALID_WAIT_UNTIL",
              "INVALID_DELAY",
              "DELAY_TOO_LARGE",
              "WAIT_SELECTOR_TOO_LONG",
              "INVALID_WAIT_SELECTOR",
              "INVALID_CAPTURE_SELECTOR",
              "CAPTURE_SELECTOR_TOO_LONG",
              "SELECTOR_FULL_PAGE_CONFLICT",
              "INVALID_HIDE_SELECTORS",
              "TOO_MANY_HIDE_SELECTORS",
              "HIDE_SELECTOR_TOO_LONG",
              "HIDE_SELECTORS_TOO_LARGE",
              "INVALID_HIDE_SELECTOR",
              "INVALID_IMAGE_QUALITY",
              "IMAGE_QUALITY_FORMAT_CONFLICT",
              "WEBP_BROWSER_UNSUPPORTED",
              "PDF_SELECTOR_CONFLICT",
              "PDF_FULL_PAGE_CONFLICT",
              "PDF_DEVICE_SCALE_CONFLICT",
              "PDF_DOCUMENT_TOO_LARGE",
              "PDF_OUTPUT_TOO_LARGE",
              "PDF_RENDER_FAILED"
            ]
          },
          "message": {
            "type": "string"
          }
        }
      },
      "QuotaErrorResponse": {
        "type": "object",
        "required": ["code", "message", "plan", "limit", "used", "periodStart"],
        "properties": {
          "code": {
            "type": "string",
            "example": "SCREENSHOT_QUOTA_EXCEEDED"
          },
          "message": {
            "type": "string"
          },
          "plan": {
            "type": "string",
            "example": "FREE"
          },
          "limit": {
            "type": "integer",
            "example": 100
          },
          "used": {
            "type": "integer",
            "example": 100
          },
          "periodStart": {
            "type": "string",
            "format": "date"
          }
        }
      },
      "PlanRequiredErrorResponse": {
        "type": "object",
        "required": ["code", "message", "requiredPlan"],
        "properties": {
          "code": {
            "type": "string",
            "example": "PAID_PLAN_REQUIRED"
          },
          "message": {
            "type": "string"
          },
          "requiredPlan": {
            "type": "string",
            "example": "PAID"
          }
        }
      },
      "DevicePreset": {
        "type": "object",
        "required": ["id", "name", "category", "viewportWidth", "viewportHeight", "deviceScaleFactor", "mobile", "version"],
        "properties": {
          "id": { "type": "string", "example": "iphone-15" },
          "name": { "type": "string", "example": "iPhone 15" },
          "category": { "type": "string", "enum": ["mobile", "tablet", "desktop"] },
          "viewportWidth": { "type": "integer" },
          "viewportHeight": { "type": "integer" },
          "deviceScaleFactor": { "type": "integer" },
          "mobile": { "type": "boolean" },
          "version": { "type": "string", "description": "Version of the immutable preset definition." }
        }
      },
      "CreateMatrixRequest": {
        "type": "object",
        "required": ["url", "deviceIds"],
        "properties": {
          "url": { "type": "string", "format": "uri" },
          "deviceIds": {
            "type": "array",
            "minItems": 1,
            "maxItems": 10,
            "uniqueItems": true,
            "items": { "type": "string" }
          },
          "networkProfileIds": {
            "type": "array",
            "maxItems": 30,
            "uniqueItems": true,
            "items": { "type": "string", "format": "uuid" },
            "description": "Localization profiles crossed with every selected device. Omit for the default environment."
          },
          "format": { "type": "string", "enum": ["png", "jpeg", "jpg", "webp"], "default": "png" },
          "fullPage": { "type": "boolean", "default": true },
          "delayMs": { "type": "integer", "minimum": 0, "maximum": 15000 },
          "waitUntil": { "type": "string", "enum": ["load", "domcontentloaded", "networkidle"] }
        }
      },
      "MatrixResponse": {
        "type": "object",
        "required": ["id", "url", "status", "createdAt", "items"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "url": { "type": "string", "format": "uri" },
          "status": { "type": "string", "enum": ["PENDING", "IN_PROGRESS", "DONE", "PARTIAL", "ERROR"] },
          "createdAt": { "type": "string", "format": "date-time" },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "required": ["deviceId", "deviceName", "capture"],
              "properties": {
                "deviceId": { "type": "string" },
                "deviceName": { "type": "string" },
                "networkProfileId": { "type": ["string", "null"], "format": "uuid" },
                "networkProfileName": { "type": "string" },
                "capture": { "$ref": "#/components/schemas/BatchItemResponse" }
              }
            }
          },
          "archiveUrl": { "type": ["string", "null"], "description": "ZIP download path after all captures finish." }
        }
      },
      "RenderLinkRequest": {
        "type": "object",
        "required": ["name", "screenshot"],
        "properties": {
          "name": { "type": "string", "maxLength": 120 },
          "screenshot": { "$ref": "#/components/schemas/CreateScreenshotRequest" },
          "cacheTtlSeconds": { "type": "integer", "minimum": 60, "maximum": 2592000, "default": 3600 }
        }
      },
      "RenderLinkResponse": {
        "type": "object",
        "required": ["id", "name", "status", "cacheTtlSeconds", "enabled", "renderUrl", "createdAt", "updatedAt"],
        "properties": {
          "id": { "type": "string", "format": "uuid" },
          "name": { "type": "string" },
          "taskId": { "type": ["string", "null"], "format": "uuid" },
          "status": { "type": "string" },
          "cacheTtlSeconds": { "type": "integer" },
          "enabled": { "type": "boolean" },
          "renderUrl": { "type": "string", "example": "/r/opaque-token" },
          "createdAt": { "type": "string", "format": "date-time" },
          "updatedAt": { "type": "string", "format": "date-time" }
        }
      }
    }
  },
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "paths": {
    "/v1/devices": {
      "get": {
        "summary": "List responsive device presets",
        "operationId": "listDevicePresets",
        "responses": {
          "200": {
            "description": "Versioned device catalog",
            "content": {
              "application/json": {
                "schema": { "type": "array", "items": { "$ref": "#/components/schemas/DevicePreset" } }
              }
            }
          },
          "401": { "description": "Missing or invalid API key" }
        }
      }
    },
    "/v1/matrices": {
      "post": {
        "summary": "Create responsive capture matrix",
        "description": "Creates one capture per selected preset. FREE accepts up to 3 devices; PAID accepts up to 10.",
        "operationId": "createCaptureMatrix",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": { "type": "string", "minLength": 8, "maxLength": 128 }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": { "schema": { "$ref": "#/components/schemas/CreateMatrixRequest" } }
          }
        },
        "responses": {
          "202": {
            "description": "Matrix accepted",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MatrixResponse" } } }
          },
          "400": { "description": "Unknown, duplicate, or invalid device selection" },
          "401": { "description": "Missing or invalid API key" },
          "402": { "description": "Monthly quota exceeded" }
        }
      },
      "get": {
        "summary": "List capture matrices",
        "operationId": "listCaptureMatrices",
        "responses": {
          "200": {
            "description": "Matrices owned by the API-key tenant",
            "content": {
              "application/json": {
                "schema": { "type": "array", "items": { "$ref": "#/components/schemas/MatrixResponse" } }
              }
            }
          },
          "401": { "description": "Missing or invalid API key" }
        }
      }
    },
    "/v1/matrices/{id}": {
      "get": {
        "summary": "Get capture matrix",
        "operationId": "getCaptureMatrix",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "responses": {
          "200": {
            "description": "Matrix and capture statuses",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MatrixResponse" } } }
          },
          "401": { "description": "Missing or invalid API key" },
          "404": { "description": "Matrix not found" }
        }
      }
    },
    "/v1/matrices/{id}/retry-failed": {
      "post": {
        "summary": "Retry failed matrix captures",
        "operationId": "retryFailedMatrixCaptures",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "responses": {
          "200": {
            "description": "Updated matrix",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/MatrixResponse" } } }
          },
          "401": { "description": "Missing or invalid API key" },
          "404": { "description": "Matrix not found" }
        }
      }
    },
    "/v1/matrices/{id}/archive": {
      "get": {
        "summary": "Download completed matrix as ZIP",
        "operationId": "downloadMatrixArchive",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "responses": {
          "200": { "description": "ZIP archive", "content": { "application/zip": {} } },
          "409": { "description": "Matrix is not complete" },
          "413": { "description": "Archive exceeds 100 MiB" }
        }
      }
    },
    "/v1/render-links": {
      "post": {
        "summary": "Create opaque render link",
        "description": "Creates a stable unguessable URL backed by a cached URL render. Raw HTML and Markdown are not accepted. FREE allows 3 active links; PAID allows 100.",
        "operationId": "createRenderLink",
        "requestBody": {
          "required": true,
          "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RenderLinkRequest" } } }
        },
        "responses": {
          "201": {
            "description": "Render link created",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RenderLinkResponse" } } }
          },
          "401": { "description": "Missing or invalid API key" },
          "409": { "description": "Active link limit reached" }
        }
      },
      "get": {
        "summary": "List render links",
        "operationId": "listRenderLinks",
        "responses": {
          "200": {
            "description": "Tenant render links",
            "content": {
              "application/json": {
                "schema": { "type": "array", "items": { "$ref": "#/components/schemas/RenderLinkResponse" } }
              }
            }
          }
        }
      }
    },
    "/v1/render-links/{id}": {
      "get": {
        "summary": "Get render link",
        "operationId": "getRenderLink",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "responses": {
          "200": {
            "description": "Render link",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RenderLinkResponse" } } }
          },
          "404": { "description": "Render link not found" }
        }
      },
      "delete": {
        "summary": "Revoke render link",
        "operationId": "revokeRenderLink",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "responses": {
          "204": { "description": "Render link revoked" },
          "404": { "description": "Render link not found" }
        }
      }
    },
    "/v1/render-links/{id}/refresh": {
      "post": {
        "summary": "Refresh render link now",
        "operationId": "refreshRenderLink",
        "parameters": [
          { "name": "id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "responses": {
          "200": {
            "description": "Updated render link",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RenderLinkResponse" } } }
          },
          "404": { "description": "Render link not found" }
        }
      }
    },
    "/r/{token}": {
      "get": {
        "security": [],
        "summary": "Resolve opaque render link",
        "description": "Returns the current render. A stale successful artifact may be returned while refresh proceeds in the background.",
        "operationId": "resolveRenderLink",
        "parameters": [
          { "name": "token", "in": "path", "required": true, "schema": { "type": "string" } }
        ],
        "responses": {
          "200": {
            "description": "Rendered image or PDF. X-Shotly-Cache is HIT or STALE.",
            "headers": {
              "X-Shotly-Cache": { "schema": { "type": "string", "enum": ["HIT", "STALE"] } },
              "X-Shotly-Task-Id": { "schema": { "type": "string", "format": "uuid" } }
            }
          },
          "404": { "description": "Unknown or revoked token" }
        }
      }
    },
    "/v1/screenshots": {
      "post": {
        "summary": "Create screenshot task",
        "description": "Creates an asynchronous Chromium render from exactly one URL, raw HTML payload, or Markdown payload. Supports PNG, JPEG, WebP, and PDF, opt-in clean-capture controls, browser-environment overrides, and tenant-scoped cache reuse. PAID requests may reference a capture profile, execute up to 10 browser actions, and deliver the completed result to an S3-compatible destination. A cache hit does not consume another monthly screenshot unit; a cache miss reserves normal quota. Existing URL behavior is unchanged when new fields are omitted. Optional imageQuality is available for JPEG and WebP. PDF is fixed A4 portrait and cannot be combined with selector, fullPage=true, deviceScaleFactor=2, or imageQuality. Chromium only.",
        "operationId": "createScreenshot",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateScreenshotRequest"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Task accepted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CreateScreenshotResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid request. Image-quality validation uses INVALID_IMAGE_QUALITY, IMAGE_QUALITY_FORMAT_CONFLICT, or WEBP_BROWSER_UNSUPPORTED. Hide-selector validation uses INVALID_HIDE_SELECTORS, TOO_MANY_HIDE_SELECTORS, HIDE_SELECTOR_TOO_LONG, HIDE_SELECTORS_TOO_LARGE, or INVALID_HIDE_SELECTOR. Element validation uses INVALID_CAPTURE_SELECTOR, CAPTURE_SELECTOR_TOO_LONG, or SELECTOR_FULL_PAGE_CONFLICT. PDF compatibility validation uses PDF_SELECTOR_CONFLICT, PDF_FULL_PAGE_CONFLICT, or PDF_DEVICE_SCALE_CONFLICT. Readiness validation uses INVALID_WAIT_UNTIL, INVALID_DELAY, DELAY_TOO_LARGE, WAIT_SELECTOR_TOO_LONG, or INVALID_WAIT_SELECTOR. Viewport errors use VIEWPORT_DIMENSIONS_REQUIRED, VIEWPORT_DIMENSIONS_INVALID, VIEWPORT_TOO_LARGE, or DEVICE_SCALE_FACTOR_UNSUPPORTED.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {"$ref": "#/components/schemas/ErrorResponse"},
                    {"$ref": "#/components/schemas/CodedErrorResponse"}
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key"
          },
          "402": {
            "description": "Monthly quota exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QuotaErrorResponse"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded"
          },
          "503": {
            "description": "Rate limiter unavailable"
          }
        }
      }
    },
    "/v1/screenshots/{taskId}": {
      "get": {
        "summary": "Get screenshot task status",
        "description": "Returns current task status. PAID users can pass timeoutSeconds to wait for DONE or ERROR.",
        "operationId": "getScreenshot",
        "parameters": [
          {
            "name": "taskId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "timeoutSeconds",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 60,
              "default": 0
            },
            "description": "PAID-only sync wait timeout. Defaults to async status read."
          }
        ],
        "responses": {
          "200": {
            "description": "Task status",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScreenshotResponse"
                }
              }
            }
          },
          "400": {
            "description": "Invalid timeoutSeconds"
          },
          "401": {
            "description": "Missing or invalid API key"
          },
          "402": {
            "description": "PAID plan required for sync wait",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlanRequiredErrorResponse"
                }
              }
            }
          },
          "404": {
            "description": "Task not found"
          },
          "429": {
            "description": "Status rate limit exceeded or too many concurrent sync waits"
          }
        }
      }
    },
    "/v1/screenshots/{taskId}/bundle": {
      "get": {
        "summary": "Get Capture Evidence Bundle",
        "operationId": "getEvidenceBundle",
        "parameters": [
          { "name": "taskId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "responses": {
          "200": {
            "description": "Bundle status, checksums, sizes, and temporary artifact URLs",
            "content": { "application/json": { "schema": { "$ref": "#/components/schemas/EvidenceBundleResponse" } } }
          },
          "401": { "description": "Missing or invalid API key" },
          "404": { "description": "Bundle not found" }
        }
      }
    },
    "/v1/screenshots/{taskId}/bundle/artifacts/{kind}": {
      "get": {
        "summary": "Download one evidence artifact",
        "operationId": "downloadEvidenceArtifact",
        "parameters": [
          { "name": "taskId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
          { "name": "kind", "in": "path", "required": true, "schema": { "type": "string", "enum": ["html", "markdown", "metadata"] } }
        ],
        "responses": {
          "200": { "description": "Evidence artifact" },
          "401": { "description": "Missing or invalid API key" },
          "404": { "description": "Artifact not found" }
        }
      }
    },
    "/v1/screenshots/{taskId}/bundle/archive": {
      "get": {
        "summary": "Download evidence ZIP with manifest",
        "operationId": "downloadEvidenceArchive",
        "parameters": [
          { "name": "taskId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "responses": {
          "200": { "description": "ZIP archive", "content": { "application/zip": {} } },
          "401": { "description": "Missing or invalid API key" },
          "404": { "description": "Bundle not found" },
          "409": { "description": "Bundle is not ready" }
        }
      }
    },
    "/v1/projects/{projectId}/visual-pipelines/{pipelineId}/runs": {
      "post": {
        "summary": "Start an immutable Visual QA release-gate run",
        "operationId": "startVisualQaRun",
        "parameters": [
          { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
          { "name": "pipelineId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
          { "name": "Idempotency-Key", "in": "header", "required": true, "schema": { "type": "string", "minLength": 8, "maxLength": 128 } }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "branch": { "type": ["string", "null"], "maxLength": 255 },
                  "commitSha": { "type": ["string", "null"], "pattern": "^[0-9a-fA-F]{7,64}$" }
                }
              }
            }
          }
        },
        "responses": {
          "202": { "description": "Run accepted with a snapshotted pipeline policy" },
          "401": { "description": "Missing or invalid API key" },
          "404": { "description": "Project or pipeline not found" },
          "409": { "description": "Conflicting idempotency replay or pipeline state" }
        }
      }
    },
    "/v1/projects/{projectId}/visual-pipelines/{pipelineId}/runs/{runId}": {
      "get": {
        "summary": "Get Visual QA verdict and item outcomes",
        "operationId": "getVisualQaRun",
        "parameters": [
          { "name": "projectId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
          { "name": "pipelineId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } },
          { "name": "runId", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid" } }
        ],
        "responses": {
          "200": { "description": "Immutable release-gate verdict and per-device results" },
          "401": { "description": "Missing or invalid API key" },
          "404": { "description": "Run not found" }
        }
      }
    },
    "/v1/batches": {
      "post": {
        "summary": "Create screenshot batch",
        "description": "Atomically validates and creates a batch. Each accepted item consumes one monthly screenshot unit. FREE accepts up to 5 items; PAID accepts up to 100. Capture profiles, browser actions, and S3 destinations require PAID.",
        "operationId": "createScreenshotBatch",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": { "type": "string", "minLength": 8, "maxLength": 128 }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateBatchRequest" }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Batch accepted",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/BatchResponse" }
              }
            }
          },
          "400": { "description": "Invalid batch or browser action" },
          "401": { "description": "Missing or invalid API key" },
          "402": { "description": "Monthly quota exceeded or PAID plan required" },
          "429": { "description": "Rate limit exceeded" }
        }
      }
    },
    "/v1/batches/{id}": {
      "get": {
        "summary": "Get screenshot batch",
        "operationId": "getScreenshotBatch",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": { "type": "string", "format": "uuid" }
          }
        ],
        "responses": {
          "200": {
            "description": "Batch and item statuses",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/BatchResponse" }
              }
            }
          },
          "401": { "description": "Missing or invalid API key" },
          "404": { "description": "Batch not found" }
        }
      }
    }
  }
}
