{
 "openapi": "3.1.0",
 "info": {
  "title": "Botworthy",
  "version": "1.0.0",
  "description": "Look up how well an AI agent can use a website, and request a scan. Results are snapshots from the scan date. The same tools are available as an MCP server at /mcp."
 },
 "servers": [
  {
   "url": "https://botworthy.dev"
  }
 ],
 "paths": {
  "/api/check": {
   "get": {
    "operationId": "checkSite",
    "summary": "Look up a site by any URL or domain",
    "description": "Normalizes the input (scheme, www, paths, subdomains) and returns the scan result if there is one. Limit: 60 requests a minute per IP.",
    "parameters": [
     {
      "name": "site",
      "in": "query",
      "required": true,
      "schema": {
       "type": "string"
      },
      "example": "https://www.allbirds.com/collections"
     }
    ],
    "responses": {
     "200": {
      "description": "found is true with the full result, or false with whether a scan is queued and how to request one",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "400": {
      "description": "Not a public website address",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "error": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "429": {
      "description": "Too many requests. See Retry-After.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "error": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/agent/scan": {
   "post": {
    "operationId": "requestScan",
    "summary": "Ask Botworthy to scan a site that isn't scanned yet",
    "description": "No account or email needed. Sites already scanned aren't queued again. Limits: 3 requests a minute and 10 new sites a day per IP.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "site"
        ],
        "properties": {
         "site": {
          "type": "string",
          "example": "example.com"
         },
         "agent": {
          "type": "string",
          "description": "Your agent's name, optional",
          "example": "Claude"
         },
         "note": {
          "type": "string",
          "description": "Optional, up to 300 characters"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Already scanned. The result is linked.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "202": {
      "description": "Queued, or already in the queue",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "400": {
      "description": "Not a public website address, or the domain doesn't resolve",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "error": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "429": {
      "description": "Too many requests. See Retry-After.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "error": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "503": {
      "description": "The queue is full for today. See Retry-After.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "error": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/api/agent/report": {
   "post": {
    "operationId": "reportVisit",
    "summary": "Tell Botworthy how one of the 5 tasks went on a site",
    "description": "Reports are reviewed, never published as-is. A report on a site we haven't scanned also requests a scan; reports that disagree with our result flag it for a rescan. Only visits that followed all three rules in followedRules are accepted. Limits: 5 a minute and 30 a day per IP, one per site and task per day.",
    "requestBody": {
     "required": true,
     "content": {
      "application/json": {
       "schema": {
        "type": "object",
        "required": [
         "site",
         "task",
         "outcome",
         "steps",
         "followedRules"
        ],
        "properties": {
         "site": {
          "type": "string",
          "example": "example.com"
         },
         "task": {
          "type": "string",
          "enum": [
           "find-pricing",
           "search",
           "start-checkout",
           "find-support",
           "find-policy"
          ]
         },
         "outcome": {
          "type": "string",
          "enum": [
           "success",
           "partial",
           "failed",
           "blocked"
          ]
         },
         "steps": {
          "type": "array",
          "maxItems": 40,
          "items": {
           "type": "object",
           "required": [
            "action"
           ],
           "properties": {
            "action": {
             "type": "string",
             "enum": [
              "open",
              "goto",
              "click",
              "type",
              "press",
              "scroll",
              "back",
              "find",
              "wait",
              "other"
             ]
            },
            "role": {
             "type": "string",
             "description": "ARIA role of the control, e.g. link or button"
            },
            "name": {
             "type": "string",
             "description": "Accessible name of the control"
            },
            "value": {
             "type": "string",
             "description": "Text typed. Search queries only, never personal data"
            },
            "url": {
             "type": "string",
             "description": "Page URL after the step. Query strings are dropped"
            },
            "ok": {
             "type": "boolean",
             "description": "false if the step didn't work"
            }
           }
          }
         },
         "followedRules": {
          "type": "object",
          "required": [
           "noLogin",
           "noPersonalData",
           "noBotCheckBypass"
          ],
          "properties": {
           "noLogin": {
            "type": "boolean",
            "const": true,
            "description": "Didn't log in or create an account"
           },
           "noPersonalData": {
            "type": "boolean",
            "const": true,
            "description": "Didn't enter personal data (a search query is fine)"
           },
           "noBotCheckBypass": {
            "type": "boolean",
            "const": true,
            "description": "Didn't solve or get around a CAPTCHA or bot check"
           }
          }
         },
         "ranFrom": {
          "type": "string",
          "enum": [
           "cloud",
           "user_browser",
           "unknown"
          ],
          "description": "Where your browser ran"
         },
         "agent": {
          "type": "string",
          "example": "Claude"
         },
         "reason": {
          "type": "string",
          "maxLength": 300,
          "description": "One line on what happened"
         },
         "startUrl": {
          "type": "string"
         },
         "finalUrl": {
          "type": "string"
         },
         "observedAt": {
          "type": "string",
          "format": "date-time",
          "description": "When you visited, within the last 30 days"
         }
        }
       }
      }
     }
    },
    "responses": {
     "200": {
      "description": "Duplicate: already have your report for this site and task today",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "202": {
      "description": "Received. Says whether it matches our result and whether a scan was queued.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "400": {
      "description": "Invalid report",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "error": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "422": {
      "description": "The visit didn't follow the rules, so it can't be used",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "error": {
           "type": "string"
          }
         }
        }
       }
      }
     },
     "429": {
      "description": "Too many requests. See Retry-After.",
      "content": {
       "application/json": {
        "schema": {
         "type": "object",
         "properties": {
          "error": {
           "type": "string"
          }
         }
        }
       }
      }
     }
    }
   }
  },
  "/{site}.json": {
   "get": {
    "operationId": "getSiteResult",
    "summary": "Full result for one scanned site (static file, 404 if not scanned)",
    "parameters": [
     {
      "name": "site",
      "in": "path",
      "required": true,
      "schema": {
       "type": "string"
      },
      "example": "allbirds.com"
     }
    ],
    "responses": {
     "200": {
      "description": "The result",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     },
     "404": {
      "description": "Not scanned"
     }
    }
   }
  },
  "/sites.json": {
   "get": {
    "operationId": "listSites",
    "summary": "Every scanned site with its status and score",
    "responses": {
     "200": {
      "description": "The index",
      "content": {
       "application/json": {
        "schema": {
         "type": "object"
        }
       }
      }
     }
    }
   }
  }
 }
}