{
  "openapi": "3.1.0",
  "info": {
    "title": "LinkPulse",
    "version": "0.3.0",
    "contact": {
      "email": "digitalpromohub.support@gmail.com"
    },
    "description": "Check ONE web page for broken links, redirect chains (hop by hop), redirect loops and mixed content. Up to 200 links per check, about 45 s max. Free, no key: 20 checks/hour per IP (+ burst ~10/min) and 400 checks/day shared by all users. Private/loopback/metadata targets are refused."
  },
  "servers": [
    {
      "url": "https://linkpulse.mike-tusa.workers.dev"
    }
  ],
  "security": [
    {}
  ],
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": {
            "type": "object",
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              },
              "retryAfterSec": {
                "type": "integer"
              },
              "scope": {
                "type": "string",
                "enum": [
                  "ip",
                  "global",
                  "burst"
                ]
              }
            },
            "required": [
              "code",
              "message"
            ]
          },
          "version": {
            "type": "string"
          }
        },
        "required": [
          "error"
        ]
      },
      "CheckResult": {
        "type": "object",
        "description": "See /docs#schema for every field.",
        "properties": {
          "schema": {
            "type": "string"
          },
          "version": {
            "type": "string"
          },
          "checkedAt": {
            "type": "string"
          },
          "input": {
            "type": "string"
          },
          "page": {
            "type": "object"
          },
          "summary": {
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "truncated": {
            "type": "boolean"
          },
          "truncatedReasons": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "links": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "notChecked": {
            "type": "object"
          },
          "skipped": {
            "type": "object"
          },
          "notes": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "limits": {
            "type": "object"
          },
          "timings": {
            "type": "object"
          },
          "subrequests": {
            "type": "integer"
          },
          "dnsLookups": {
            "type": "integer"
          }
        },
        "required": [
          "schema",
          "version",
          "page",
          "summary",
          "links",
          "truncated"
        ]
      },
      "McpCheckLinksResult": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "description": "The page URL as requested."
          },
          "final_url": {
            "type": "string"
          },
          "page_status": {
            "type": "integer"
          },
          "page_redirects": {
            "type": "integer"
          },
          "page_hops": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string"
                },
                "status": {
                  "type": "integer"
                },
                "location": {
                  "type": "string"
                }
              },
              "required": [
                "url"
              ]
            },
            "maxItems": 11
          },
          "summary": {
            "type": "object",
            "description": "Counts: linksFound, uniqueLinks, checked, ok, redirected, broken, blocked, unchecked, mixedContent, insecureLinks, longChains, redirectLoops, maybeBotBlocked, resolvesPrivate.",
            "additionalProperties": {
              "type": "integer"
            }
          },
          "truncated": {
            "type": "boolean",
            "description": "True when not every link on the page was checked (link cap, budget or time limit)."
          },
          "truncated_reasons": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "problems": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "url": {
                  "type": "string"
                },
                "category": {
                  "type": "string",
                  "enum": [
                    "broken",
                    "blocked",
                    "unchecked",
                    "redirect",
                    "ok"
                  ]
                },
                "status": {
                  "type": "integer",
                  "description": "Final HTTP status (omitted when nothing was fetched)."
                },
                "final_url": {
                  "type": "string"
                },
                "redirects": {
                  "type": "integer"
                },
                "error_code": {
                  "type": "string"
                },
                "flags": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "kind": {
                  "type": "string"
                }
              },
              "required": [
                "url",
                "category"
              ]
            },
            "maxItems": 25,
            "description": "Links needing attention (broken first, then blocked, unchecked, redirects with flags such as long_chain or https_downgrade, then other flagged links). At most 25 here; see problems_total."
          },
          "problems_total": {
            "type": "integer"
          },
          "problems_returned": {
            "type": "integer"
          },
          "not_checked_count": {
            "type": "integer"
          },
          "checked_at": {
            "type": "string"
          },
          "version": {
            "type": "string"
          }
        },
        "required": [
          "url",
          "page_status",
          "summary",
          "truncated",
          "problems",
          "problems_total"
        ]
      }
    }
  },
  "paths": {
    "/api/check": {
      "get": {
        "operationId": "checkPage",
        "summary": "Check one page",
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "description": "Public http(s) page URL (ports 80/443). Without a scheme, https:// is assumed.",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 2048
            }
          },
          {
            "name": "assets",
            "in": "query",
            "required": false,
            "description": "false = only <a>/<area> links (default true)",
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            }
          },
          {
            "name": "max",
            "in": "query",
            "required": false,
            "description": "Links to check (default 200)",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Check result (linkpulse.check.v1)",
            "headers": {
              "x-ratelimit-limit": {
                "description": "Checks per hour per IP",
                "schema": {
                  "type": "string"
                }
              },
              "x-ratelimit-remaining": {
                "description": "Checks left in the current hour",
                "schema": {
                  "type": "string"
                }
              },
              "x-ratelimit-reset": {
                "description": "Seconds until the hourly window resets",
                "schema": {
                  "type": "string"
                }
              },
              "x-cache": {
                "description": "MISS, or HIT for a cached identical check (not counted)",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckResult"
                }
              }
            }
          },
          "400": {
            "description": "invalid_url, invalid_scheme, credentials_in_url or invalid_request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "blocked_target, blocked_port or blocked_redirect (private/loopback/metadata target, incl. names that resolve to one)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "method_not_allowed",
            "headers": {
              "allow": {
                "description": "GET, POST, OPTIONS",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "invalid_request: POST body over 8 KB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "dns_not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited (error.scope ip, global or burst; error.retryAfterSec)",
            "headers": {
              "retry-after": {
                "description": "Seconds to wait",
                "schema": {
                  "type": "string"
                }
              },
              "x-ratelimit-limit": {
                "description": "Checks per hour per IP",
                "schema": {
                  "type": "string"
                }
              },
              "x-ratelimit-remaining": {
                "description": "Checks left in the current hour",
                "schema": {
                  "type": "string"
                }
              },
              "x-ratelimit-reset": {
                "description": "Seconds until the hourly window resets",
                "schema": {
                  "type": "string"
                }
              },
              "x-cache": {
                "description": "MISS, or HIT for a cached identical check (not counted)",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal_error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "page_fetch_failed, dns_error, bad_redirect, redirect_loop, too_many_redirects or budget_exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "504": {
            "description": "page_timeout",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "checkPagePost",
        "summary": "Check one page (JSON or form body, max 8 KB)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "maxLength": 2048
                  },
                  "assets": {
                    "type": "boolean"
                  },
                  "max": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 200
                  }
                },
                "required": [
                  "url"
                ]
              }
            },
            "application/x-www-form-urlencoded": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string"
                  },
                  "assets": {
                    "type": "string"
                  },
                  "max": {
                    "type": "string"
                  }
                },
                "required": [
                  "url"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Check result (linkpulse.check.v1)",
            "headers": {
              "x-ratelimit-limit": {
                "description": "Checks per hour per IP",
                "schema": {
                  "type": "string"
                }
              },
              "x-ratelimit-remaining": {
                "description": "Checks left in the current hour",
                "schema": {
                  "type": "string"
                }
              },
              "x-ratelimit-reset": {
                "description": "Seconds until the hourly window resets",
                "schema": {
                  "type": "string"
                }
              },
              "x-cache": {
                "description": "MISS, or HIT for a cached identical check (not counted)",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckResult"
                }
              }
            }
          },
          "400": {
            "description": "invalid_url, invalid_scheme, credentials_in_url or invalid_request",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "blocked_target, blocked_port or blocked_redirect (private/loopback/metadata target, incl. names that resolve to one)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "405": {
            "description": "method_not_allowed",
            "headers": {
              "allow": {
                "description": "GET, POST, OPTIONS",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "invalid_request: POST body over 8 KB",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "dns_not_found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "rate_limited (error.scope ip, global or burst; error.retryAfterSec)",
            "headers": {
              "retry-after": {
                "description": "Seconds to wait",
                "schema": {
                  "type": "string"
                }
              },
              "x-ratelimit-limit": {
                "description": "Checks per hour per IP",
                "schema": {
                  "type": "string"
                }
              },
              "x-ratelimit-remaining": {
                "description": "Checks left in the current hour",
                "schema": {
                  "type": "string"
                }
              },
              "x-ratelimit-reset": {
                "description": "Seconds until the hourly window resets",
                "schema": {
                  "type": "string"
                }
              },
              "x-cache": {
                "description": "MISS, or HIT for a cached identical check (not counted)",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "internal_error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "page_fetch_failed, dns_error, bad_redirect, redirect_loop, too_many_redirects or budget_exceeded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "504": {
            "description": "page_timeout",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/health": {
      "get": {
        "operationId": "health",
        "summary": "Service version, limits and rate limits",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "405": {
            "description": "method_not_allowed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/mcp": {
      "post": {
        "operationId": "mcp",
        "summary": "MCP JSON-RPC 2.0 (Streamable HTTP, stateless). Protocol versions 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26, 2024-11-05. Max body 65536 bytes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "JSON-RPC response (tool errors are results with isError and _meta[\"linkpulse/error\"])",
            "headers": {
              "x-ratelimit-limit": {
                "description": "Checks per hour per IP",
                "schema": {
                  "type": "string"
                }
              },
              "x-ratelimit-remaining": {
                "description": "Checks left in the current hour",
                "schema": {
                  "type": "string"
                }
              },
              "x-ratelimit-reset": {
                "description": "Seconds until the hourly window resets",
                "schema": {
                  "type": "string"
                }
              },
              "x-cache": {
                "description": "MISS, or HIT for a cached identical check (not counted)",
                "schema": {
                  "type": "string"
                }
              },
              "retry-after": {
                "description": "Seconds (on a rate-limited tools/call)",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "202": {
            "description": "Notification or client response accepted"
          },
          "400": {
            "description": "Parse error, invalid request, header mismatch or unsupported version"
          },
          "401": {
            "description": "Reserved (no API keys yet; no WWW-Authenticate)"
          },
          "403": {
            "description": "Origin not allowed"
          },
          "404": {
            "description": "Unknown method (2026-07-28)"
          },
          "413": {
            "description": "Body too large"
          },
          "429": {
            "description": "Too many MCP requests",
            "headers": {
              "retry-after": {
                "description": "Seconds",
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "500": {
            "description": "Internal error"
          },
          "503": {
            "description": "Unavailable"
          }
        }
      },
      "get": {
        "operationId": "mcpGet",
        "summary": "Not supported (stateless, no SSE)",
        "responses": {
          "405": {
            "description": "Method not allowed",
            "headers": {
              "allow": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "POST, OPTIONS"
                  ]
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "mcpDelete",
        "summary": "Not supported (no sessions)",
        "responses": {
          "405": {
            "description": "Method not allowed",
            "headers": {
              "allow": {
                "schema": {
                  "type": "string",
                  "enum": [
                    "POST, OPTIONS"
                  ]
                }
              }
            }
          }
        }
      }
    }
  }
}