{
  "openapi": "3.1.0",
  "info": {
    "title": "4F API",
    "version": "1.0.0",
    "summary": "Broken link checker and website monitoring API",
    "description": "Create projects, start website scans and retrieve broken links, missing assets, redirect chains, SSL errors and other issues. Available on all plans, including Free.\n\nAuthentication: create an API key in the dashboard (Account > API keys) and send it as `Authorization: Bearer 4f_...`. Keys can be limited to specific projects.\n\nRate limit: 50 requests per minute per user. Scans consume pages from the monthly quota.\n\nDocumentation: https://www.4f.at/docs - CLI: https://www.4f.at/docs/cli",
    "contact": {
      "name": "4F",
      "url": "https://www.4f.at"
    },
    "termsOfService": "https://www.4f.at/legal/terms"
  },
  "externalDocs": {
    "description": "API documentation",
    "url": "https://www.4f.at/docs"
  },
  "servers": [
    {
      "url": "https://www.4f.at"
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Account",
      "description": "Plan and page quota of the key's user"
    },
    {
      "name": "Projects",
      "description": "Websites to scan, with their crawl settings and schedule"
    },
    {
      "name": "Scans",
      "description": "Start, poll, cancel and delete scans"
    },
    {
      "name": "Results",
      "description": "Checked URLs and the issues found, as JSON or CSV"
    }
  ],
  "paths": {
    "/api/v1/account": {
      "get": {
        "operationId": "getAccount",
        "tags": [
          "Account"
        ],
        "summary": "Plan and page quota",
        "description": "Not available for project-scoped API keys.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Account"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key or user has no access to this resource (e.g. a project-scoped key used outside its projects)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (50 requests per minute per user), monthly page quota exhausted, or the domain is scanned too frequently (more than 10 scan starts per 5 minutes)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/projects": {
      "get": {
        "operationId": "listProjects",
        "tags": [
          "Projects"
        ],
        "summary": "List projects",
        "description": "All personal and team projects the key has access to.",
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "personal",
                "team"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ProjectListItem"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (50 requests per minute per user), monthly page quota exhausted, or the domain is scanned too frequently (more than 10 scan starts per 5 minutes)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createProject",
        "tags": [
          "Projects"
        ],
        "summary": "Create a project",
        "description": "Counts towards the plan's project limit (Free 2, Starter 10, Agency 50). Not available for project-scoped keys.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectCreate"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Project created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "name": {
                      "type": "string"
                    },
                    "rootUrl": {
                      "type": "string",
                      "format": "uri"
                    },
                    "scanFrequency": {
                      "type": "string",
                      "enum": [
                        "manual",
                        "weekly",
                        "daily"
                      ]
                    },
                    "crawlConfig": {
                      "$ref": "#/components/schemas/CrawlConfig"
                    },
                    "createdAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key or user has no access to this resource (e.g. a project-scoped key used outside its projects)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (50 requests per minute per user), monthly page quota exhausted, or the domain is scanned too frequently (more than 10 scan starts per 5 minutes)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/org/projects": {
      "get": {
        "operationId": "listTeamProjects",
        "tags": [
          "Projects"
        ],
        "summary": "List team projects",
        "description": "Team projects of all organisations the user owns or belongs to.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/TeamProject"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (50 requests per minute per user), monthly page quota exhausted, or the domain is scanned too frequently (more than 10 scan starts per 5 minutes)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/projects/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Project ID",
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "operationId": "getProject",
        "tags": [
          "Projects"
        ],
        "summary": "Get a project",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key or user has no access to this resource (e.g. a project-scoped key used outside its projects)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (50 requests per minute per user), monthly page quota exhausted, or the domain is scanned too frequently (more than 10 scan starts per 5 minutes)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "operationId": "updateProject",
        "tags": [
          "Projects"
        ],
        "summary": "Update a project",
        "description": "Only the project owner. crawlConfig fields are merged into the existing settings.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProjectUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Project"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key or user has no access to this resource (e.g. a project-scoped key used outside its projects)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Invalid input",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (50 requests per minute per user), monthly page quota exhausted, or the domain is scanned too frequently (more than 10 scan starts per 5 minutes)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteProject",
        "tags": [
          "Projects"
        ],
        "summary": "Delete a project",
        "description": "Permanently deletes the project and all its scans. Only the project owner.",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key or user has no access to this resource (e.g. a project-scoped key used outside its projects)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (50 requests per minute per user), monthly page quota exhausted, or the domain is scanned too frequently (more than 10 scan starts per 5 minutes)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/projects/{id}/scans": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Project ID",
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "operationId": "listProjectScans",
        "tags": [
          "Scans"
        ],
        "summary": "List scans of a project",
        "description": "Newest first.",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ScanSummary"
                      }
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key or user has no access to this resource (e.g. a project-scoped key used outside its projects)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (50 requests per minute per user), monthly page quota exhausted, or the domain is scanned too frequently (more than 10 scan starts per 5 minutes)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "startScan",
        "tags": [
          "Scans"
        ],
        "summary": "Start a scan",
        "description": "Queues a scan and returns immediately; the crawl runs asynchronously. Poll GET /api/v1/scans/{id} every 5-10 seconds until the status is completed, failed or cancelled. Pages crawled count towards the monthly quota.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "notify": {
                    "type": "boolean",
                    "description": "Email a report to the project owner when the scan completes"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Scan queued",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "scanId": {
                      "type": "string",
                      "format": "uuid"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key or user has no access to this resource (e.g. a project-scoped key used outside its projects)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (50 requests per minute per user), monthly page quota exhausted, or the domain is scanned too frequently (more than 10 scan starts per 5 minutes)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scans/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Scan ID",
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "operationId": "getScan",
        "tags": [
          "Scans"
        ],
        "summary": "Get scan status and summary",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Scan"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key or user has no access to this resource (e.g. a project-scoped key used outside its projects)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (50 requests per minute per user), monthly page quota exhausted, or the domain is scanned too frequently (more than 10 scan starts per 5 minutes)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteScan",
        "tags": [
          "Scans"
        ],
        "summary": "Delete a scan",
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key or user has no access to this resource (e.g. a project-scoped key used outside its projects)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (50 requests per minute per user), monthly page quota exhausted, or the domain is scanned too frequently (more than 10 scan starts per 5 minutes)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scans/{id}/cancel": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Scan ID",
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "post": {
        "operationId": "cancelScan",
        "tags": [
          "Scans"
        ],
        "summary": "Cancel a running scan",
        "description": "No-op for scans that already finished; returns their status.",
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "cancelled",
                        "completed",
                        "failed"
                      ]
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key or user has no access to this resource (e.g. a project-scoped key used outside its projects)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (50 requests per minute per user), monthly page quota exhausted, or the domain is scanned too frequently (more than 10 scan starts per 5 minutes)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scans/{id}/results": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Scan ID",
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "operationId": "getScanResults",
        "tags": [
          "Results"
        ],
        "summary": "Checked URLs with their issues",
        "description": "Sorted by URL. By default only URLs with at least one issue are returned.",
        "parameters": [
          {
            "name": "issues_only",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": true
            },
            "description": "Set to false to include URLs without issues"
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "default": 1
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 200,
              "default": 50
            },
            "description": "Page size (max 200). 0 returns all records without pagination (no page/limit in the response)."
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/UrlRecord"
                      }
                    },
                    "page": {
                      "type": "integer"
                    },
                    "limit": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key or user has no access to this resource (e.g. a project-scoped key used outside its projects)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (50 requests per minute per user), monthly page quota exhausted, or the domain is scanned too frequently (more than 10 scan starts per 5 minutes)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scans/{id}/export": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "description": "Scan ID",
          "schema": {
            "type": "string",
            "format": "uuid"
          }
        }
      ],
      "get": {
        "operationId": "exportScanCsv",
        "tags": [
          "Results"
        ],
        "summary": "Export results as CSV",
        "description": "Columns: URL, Type, Status Code, Response Time (ms), Issue Types, Severity, Found On, Link Text.",
        "parameters": [
          {
            "name": "broken",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "1"
              ]
            },
            "description": "1 = only URLs with issues"
          }
        ],
        "responses": {
          "200": {
            "description": "CSV file",
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The key or user has no access to this resource (e.g. a project-scoped key used outside its projects)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Resource not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limit exceeded (50 requests per minute per user), monthly page quota exhausted, or the domain is scanned too frequently (more than 10 scan starts per 5 minutes)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "API key starting with 4f_"
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "example": "Unauthorized"
          }
        }
      },
      "Account": {
        "type": "object",
        "properties": {
          "plan": {
            "type": "string",
            "enum": [
              "free",
              "starter",
              "agency"
            ],
            "description": "Effective plan (includes active trials)"
          },
          "quotaUsed": {
            "type": "integer",
            "description": "Pages used in the current billing period"
          },
          "quotaMonthly": {
            "type": "integer",
            "description": "Pages included per month"
          },
          "bonusPages": {
            "type": "integer",
            "description": "Extra pages purchased on top of the monthly quota"
          },
          "quotaRemaining": {
            "type": "integer",
            "description": "Remaining monthly pages plus extra pages"
          },
          "billingPeriodStart": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "CrawlConfig": {
        "type": "object",
        "description": "Per-project crawl settings. Updates are merged into the existing settings.",
        "properties": {
          "maxDepth": {
            "type": "integer",
            "minimum": 1,
            "maximum": 20,
            "description": "Maximum link depth to follow"
          },
          "ignorePatterns": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "URL substrings to skip"
          },
          "respectRobots": {
            "type": "boolean",
            "description": "Respect robots.txt (default true)"
          },
          "emailAlerts": {
            "type": "string",
            "enum": [
              "always",
              "new_issues",
              "disabled"
            ]
          },
          "sitemapUrl": {
            "type": "string",
            "format": "uri",
            "description": "Custom sitemap URL (Starter and Agency)"
          },
          "slowThresholdMs": {
            "type": "integer",
            "minimum": 500,
            "maximum": 30000,
            "description": "Responses slower than this are reported as slow (clamped to 500-30000)"
          },
          "rateLimit": {
            "type": "integer",
            "enum": [
              1,
              2,
              3,
              5,
              10,
              15
            ],
            "description": "Crawler requests per second. Plan maximum: Free 5, Starter 10, Agency 15. Default 5."
          },
          "scheduledHour": {
            "type": "integer",
            "minimum": 0,
            "maximum": 23,
            "description": "Hour of day for scheduled scans"
          },
          "scheduledWeekday": {
            "type": "integer",
            "minimum": 0,
            "maximum": 6,
            "description": "Weekday for weekly scans (0 = Sunday)"
          },
          "scheduledTimezone": {
            "type": "string",
            "description": "IANA timezone for the schedule (invalid values fall back to UTC)",
            "example": "Europe/Vienna"
          },
          "maxUrls": {
            "type": "integer",
            "readOnly": true,
            "description": "Per-scan page limit, set by the server from your plan"
          }
        }
      },
      "ProjectListItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "rootUrl": {
            "type": "string",
            "format": "uri"
          },
          "scanFrequency": {
            "type": "string",
            "enum": [
              "manual",
              "weekly",
              "daily"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "personal",
              "team"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "TeamProject": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "rootUrl": {
            "type": "string",
            "format": "uri"
          },
          "scanFrequency": {
            "type": "string",
            "enum": [
              "manual",
              "weekly",
              "daily"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "orgName": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "Project": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "name": {
            "type": "string"
          },
          "rootUrl": {
            "type": "string",
            "format": "uri"
          },
          "scanFrequency": {
            "type": "string",
            "enum": [
              "manual",
              "weekly",
              "daily"
            ]
          },
          "crawlConfig": {
            "$ref": "#/components/schemas/CrawlConfig"
          },
          "orgId": {
            "type": [
              "string",
              "null"
            ],
            "format": "uuid"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "serverInfo": {
            "type": [
              "object",
              "null"
            ],
            "description": "Hosting, TLS and security header information collected for the root URL (GET only)",
            "additionalProperties": true
          }
        }
      },
      "ProjectCreate": {
        "type": "object",
        "required": [
          "name",
          "rootUrl"
        ],
        "properties": {
          "name": {
            "type": "string",
            "example": "My website"
          },
          "rootUrl": {
            "type": "string",
            "description": "Start URL; https:// is added when no scheme is given",
            "example": "https://example.com"
          },
          "isPersonal": {
            "type": "boolean",
            "default": true
          },
          "scanFrequency": {
            "type": "string",
            "enum": [
              "manual",
              "weekly",
              "daily"
            ],
            "default": "manual",
            "description": "weekly requires Starter, daily requires Agency"
          },
          "crawlConfig": {
            "$ref": "#/components/schemas/CrawlConfig"
          }
        }
      },
      "ProjectUpdate": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "scanFrequency": {
            "type": "string",
            "enum": [
              "manual",
              "weekly",
              "daily"
            ],
            "description": "weekly requires Starter, daily requires Agency"
          },
          "crawlConfig": {
            "$ref": "#/components/schemas/CrawlConfig"
          }
        }
      },
      "ScanSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "status": {
            "type": "string",
            "enum": [
              "pending",
              "running",
              "completed",
              "failed",
              "cancelled"
            ]
          },
          "triggeredBy": {
            "type": "string",
            "description": "What started the scan, e.g. user, schedule, api, api_notify"
          },
          "totalUrlsCrawled": {
            "type": [
              "integer",
              "null"
            ]
          },
          "totalPagesCrawled": {
            "type": [
              "integer",
              "null"
            ]
          },
          "totalIssuesFound": {
            "type": [
              "integer",
              "null"
            ]
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "startedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "finishedAt": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "Scan": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ScanSummary"
          },
          {
            "type": "object",
            "properties": {
              "projectId": {
                "type": "string",
                "format": "uuid"
              },
              "rootUrl": {
                "type": [
                  "string",
                  "null"
                ],
                "format": "uri",
                "description": "Effective start URL after redirects"
              },
              "errorMessage": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "Set for failed scans, or \"timeout\" when the plan's maximum scan duration was reached"
              }
            }
          }
        ],
        "description": "While a scan is pending or running, totalUrlsCrawled is the live number of URLs checked so far."
      },
      "Issue": {
        "type": "object",
        "properties": {
          "issueType": {
            "type": "string",
            "enum": [
              "broken_link",
              "server_error",
              "missing_asset",
              "redirect_chain",
              "slow_response",
              "ssl_error",
              "broken_anchor",
              "mixed_content",
              "blocked_by_server",
              "missing_title",
              "title_length",
              "missing_meta_description",
              "meta_description_length",
              "missing_h1",
              "multiple_h1",
              "missing_viewport",
              "missing_lang",
              "canonical_mismatch",
              "links_no_text",
              "images_missing_alt",
              "skipped_heading_levels",
              "relative_favicon_path"
            ]
          },
          "severity": {
            "type": "string",
            "enum": [
              "error",
              "warning",
              "info"
            ]
          },
          "detail": {
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": true,
            "description": "Type-specific details, e.g. {\"status_code\": 404} or {\"anchor\": \"pricing\"}",
            "example": {
              "status_code": 404
            }
          }
        }
      },
      "UrlRecord": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "url": {
            "type": "string"
          },
          "urlType": {
            "type": "string",
            "enum": [
              "internal",
              "external",
              "asset"
            ]
          },
          "statusCode": {
            "type": [
              "integer",
              "null"
            ]
          },
          "responseTimeMs": {
            "type": [
              "integer",
              "null"
            ]
          },
          "finalUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "URL after following redirects"
          },
          "redirectCount": {
            "type": [
              "integer",
              "null"
            ]
          },
          "firstStatusCode": {
            "type": [
              "integer",
              "null"
            ],
            "description": "Status of the first response when redirected (e.g. 301)"
          },
          "foundOnUrl": {
            "type": [
              "string",
              "null"
            ],
            "description": "Page that contains the link"
          },
          "linkText": {
            "type": [
              "string",
              "null"
            ]
          },
          "errorType": {
            "type": [
              "string",
              "null"
            ],
            "description": "Network-level error, e.g. timeout, dns_error, ssl_error, connection_refused"
          },
          "contentType": {
            "type": [
              "string",
              "null"
            ]
          },
          "issues": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Issue"
            }
          }
        }
      }
    }
  }
}
