{
  "openapi": "3.0.3",
  "info": {
    "title": "Markdown for AI Tester API",
    "description": "Check whether any public URL serves Markdown to AI-style clients.\n\nThe Markdown for AI Tester probes a public URL with three strategies used by\nLLM crawlers and AI-assistant fetchers:\n\n  1. `accept`  — request with `Accept: text/markdown` header.\n  2. `suffix`  — append `.md` to the URL path.\n  3. `query`   — append `?format=markdown` to the URL.\n\nThe API returns a structured report describing which strategy (if any)\nsucceeded, whether the origin is served through Cloudflare, and up to 64 KB\nof the returned Markdown preview.\n\n### Rate limits\n\nPublic endpoints are protected by Cloudflare rate-limit rules. Typical\nsteady-state limits are `20 req/min` and `200 req/hour` per IP address for\n`/api/check`. Abusive traffic is Managed-Challenged or blocked.\n\n### Tolerant input\n\nThe `url` parameter accepts any of:\n\n  - `https://example.com`\n  - `http://example.com`\n  - `example.com` / `www.example.com`\n  - `//example.com` (protocol-relative)\n  - `  example.com  ` (leading / trailing whitespace)\n  - `<https://example.com>` / `\"https://example.com\"` (wrapping chars)\n  - `URL: example.com` / `link=example.com` (labelled)\n\nInternal whitespace, zero-width characters, and trailing sentence\npunctuation (`,;!`) are stripped. Private/internal hosts\n(`localhost`, `127.0.0.1`, RFC1918, link-local, `.internal`) are refused.\n\n### Stability\n\n`v1` is stable. Field additions are non-breaking. Field removals or type\nchanges will bump the major version in the server URL.\n",
    "version": "1.0.0",
    "contact": {
      "name": "Markdown for AI Tester",
      "url": "https://ai-markdown.com"
    },
    "license": {
      "name": "MIT",
      "url": "https://opensource.org/licenses/MIT"
    }
  },
  "servers": [
    {
      "url": "https://ai-markdown.com",
      "description": "Production"
    }
  ],
  "security": [],
  "tags": [
    {
      "name": "check",
      "description": "Run a Markdown-for-AI check against a public URL."
    },
    {
      "name": "pages",
      "description": "HTML landing and shareable result pages."
    },
    {
      "name": "seo",
      "description": "Sitemap and robots.txt for crawlers."
    },
    {
      "name": "meta",
      "description": "API metadata (OpenAPI spec, docs)."
    }
  ],
  "paths": {
    "/api/check": {
      "get": {
        "tags": [
          "check"
        ],
        "summary": "Run a Markdown-for-AI check",
        "description": "Probes the target URL with the three Markdown strategies and returns a\nstructured report. Always returns HTTP 200 when the URL is parseable,\neven if the probe itself fails — the body's `result.success` field is\nthe source of truth.\n",
        "operationId": "runCheck",
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "description": "Target URL to probe. Tolerant to messy input (whitespace, missing\nscheme, wrapping punctuation). See the \"Tolerant input\" section\nabove for all accepted forms.\n",
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 2048,
              "example": "https://developers.cloudflare.com/workers/"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Check completed (may be a success or failure; see `result.success`).",
            "headers": {
              "Cache-Control": {
                "schema": {
                  "type": "string"
                },
                "example": "no-store"
              },
              "Access-Control-Allow-Origin": {
                "schema": {
                  "type": "string"
                },
                "example": "*"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckSuccessResponse"
                },
                "examples": {
                  "pass": {
                    "summary": "URL passes the check",
                    "value": {
                      "ok": true,
                      "result": {
                        "input": "https://developers.cloudflare.com/workers/",
                        "normalisedUrl": "https://developers.cloudflare.com/workers/",
                        "cloudflare": "yes",
                        "cloudflareSignals": [
                          "server: cloudflare",
                          "cf-ray: 9ef27b338fe2c45a-AMS"
                        ],
                        "success": true,
                        "winningStrategy": "accept",
                        "attempts": [
                          {
                            "strategy": "accept",
                            "requestUrl": "https://developers.cloudflare.com/workers/",
                            "ok": true,
                            "status": 200,
                            "contentType": "text/markdown; charset=utf-8",
                            "size": 15234,
                            "looksLikeMarkdown": true,
                            "preview": "# Cloudflare Workers\n\nBuild serverless applications..."
                          }
                        ],
                        "reasonCodes": [],
                        "durationMs": 82
                      }
                    }
                  },
                  "invalidUrl": {
                    "summary": "Unparseable URL",
                    "value": {
                      "ok": true,
                      "result": {
                        "input": "not a url",
                        "normalisedUrl": "not a url",
                        "cloudflare": "unknown",
                        "cloudflareSignals": [],
                        "success": false,
                        "winningStrategy": null,
                        "attempts": [],
                        "reasonCodes": [
                          "invalidUrl"
                        ],
                        "durationMs": 1
                      }
                    }
                  },
                  "notCloudflare": {
                    "summary": "Origin not on Cloudflare",
                    "value": {
                      "ok": true,
                      "result": {
                        "input": "https://example.org/",
                        "normalisedUrl": "https://example.org/",
                        "cloudflare": "no",
                        "cloudflareSignals": [],
                        "success": false,
                        "winningStrategy": null,
                        "attempts": [
                          {
                            "strategy": "accept",
                            "requestUrl": "https://example.org/",
                            "ok": true,
                            "status": 200,
                            "contentType": "text/html; charset=utf-8",
                            "size": 1256,
                            "looksLikeMarkdown": false,
                            "preview": "<!doctype html><html>..."
                          }
                        ],
                        "reasonCodes": [
                          "notCloudflare",
                          "blocked"
                        ],
                        "durationMs": 412
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing `url` query parameter.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "ok": false,
                  "error": "missing_url_param"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed.",
            "headers": {
              "Allow": {
                "schema": {
                  "type": "string"
                },
                "example": "GET, OPTIONS"
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                },
                "example": {
                  "ok": false,
                  "error": "method_not_allowed"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          }
        }
      },
      "options": {
        "tags": [
          "check"
        ],
        "summary": "CORS preflight",
        "description": "Returns permissive CORS headers for browser callers.",
        "operationId": "checkPreflight",
        "responses": {
          "204": {
            "description": "CORS preflight successful.",
            "headers": {
              "Access-Control-Allow-Origin": {
                "schema": {
                  "type": "string"
                },
                "example": "*"
              },
              "Access-Control-Allow-Methods": {
                "schema": {
                  "type": "string"
                },
                "example": "GET, OPTIONS"
              },
              "Access-Control-Allow-Headers": {
                "schema": {
                  "type": "string"
                },
                "example": "content-type"
              },
              "Access-Control-Max-Age": {
                "schema": {
                  "type": "string"
                },
                "example": "86400"
              }
            }
          }
        }
      }
    },
    "/check": {
      "post": {
        "tags": [
          "check"
        ],
        "summary": "Back-compat JSON submission",
        "description": "Accepts a JSON body and returns either a rendered HTML result page\n(default) or the same JSON payload as `/api/check` when the client\nsends `Accept: application/json`.\n\nRetained for backward compatibility with older embeds. New integrations\nshould use `GET /api/check`.\n\n> **Note:** the Worker also tolerates `application/x-www-form-urlencoded`\n> request bodies so that browser `<form>` POSTs from legacy embeds\n> continue to work. That surface is intentionally excluded from this\n> schema — programmatic clients must use `application/json`.\n",
        "operationId": "postCheck",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "Target URL (same tolerant rules as `/api/check`)."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Check completed.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string",
                  "description": "Rendered result page."
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CheckSuccessResponse"
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid URL.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ErrorResponse"
                }
              }
            }
          },
          "405": {
            "description": "Method not allowed.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/": {
      "get": {
        "tags": [
          "pages"
        ],
        "summary": "Landing page (or shared result)",
        "description": "Returns the localised landing page. When a `url` query parameter is\npresent, runs the check and renders the result inline, producing a\n**shareable URL**. Shared result pages are served with\n`X-Robots-Tag: noindex` so per-URL variants do not pollute search\nresults.\n\nThe root path redirects to `/:locale` only when `Accept-Language`\nnegotiates a non-default locale **and** no `url` parameter is present.\n",
        "operationId": "getLanding",
        "parameters": [
          {
            "$ref": "#/components/parameters/ShareUrl"
          }
        ],
        "responses": {
          "200": {
            "description": "Landing page or shared result.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "302": {
            "description": "Redirect to preferred locale (only when no shared URL)."
          }
        }
      }
    },
    "/{locale}": {
      "get": {
        "tags": [
          "pages"
        ],
        "summary": "Localised landing page (or shared result)",
        "description": "Localised equivalent of `/`. Accepts the same optional `url` param.",
        "operationId": "getLocalisedLanding",
        "parameters": [
          {
            "name": "locale",
            "in": "path",
            "required": true,
            "description": "Supported locale code.",
            "schema": {
              "$ref": "#/components/schemas/Locale"
            }
          },
          {
            "$ref": "#/components/parameters/ShareUrl"
          }
        ],
        "responses": {
          "200": {
            "description": "Localised landing page or shared result.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "301": {
            "description": "Default locale `/en` collapses to `/` to avoid duplicate content."
          }
        }
      }
    },
    "/sitemap.xml": {
      "get": {
        "tags": [
          "seo"
        ],
        "summary": "Sitemap for all localised landing pages",
        "operationId": "getSitemap",
        "responses": {
          "200": {
            "description": "XML sitemap listing all `hreflang` variants.",
            "content": {
              "application/xml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/robots.txt": {
      "get": {
        "tags": [
          "seo"
        ],
        "summary": "robots.txt",
        "operationId": "getRobots",
        "responses": {
          "200": {
            "description": "robots.txt file.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/openapi.yaml": {
      "get": {
        "tags": [
          "meta"
        ],
        "summary": "This OpenAPI spec (YAML)",
        "operationId": "getOpenApiYaml",
        "responses": {
          "200": {
            "description": "OpenAPI 3.1 specification in YAML.",
            "content": {
              "application/yaml": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    },
    "/api/openapi.json": {
      "get": {
        "tags": [
          "meta"
        ],
        "summary": "This OpenAPI spec (JSON)",
        "operationId": "getOpenApiJson",
        "responses": {
          "200": {
            "description": "OpenAPI 3.1 specification in JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/api/docs": {
      "get": {
        "tags": [
          "meta"
        ],
        "summary": "Human-readable API documentation",
        "description": "Bootstrap-styled reference page rendered from this spec.",
        "operationId": "getApiDocs",
        "responses": {
          "200": {
            "description": "HTML docs page.",
            "content": {
              "text/html": {
                "schema": {
                  "type": "string"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "parameters": {
      "ShareUrl": {
        "name": "url",
        "in": "query",
        "required": false,
        "description": "When present, runs a check against the given URL and renders the\nresult inline. Produces a **shareable link** — anyone opening the\nsame URL will see the same check result. The page is served with\n`X-Robots-Tag: noindex`.\n",
        "schema": {
          "type": "string",
          "maxLength": 2048,
          "example": "https://developers.cloudflare.com/workers/"
        }
      }
    },
    "schemas": {
      "Locale": {
        "type": "string",
        "enum": [
          "en",
          "es",
          "fr",
          "pt",
          "ru",
          "ar",
          "zh",
          "hi",
          "bn",
          "ja"
        ],
        "description": "Supported UI locale codes."
      },
      "CheckStrategy": {
        "type": "string",
        "enum": [
          "accept",
          "suffix",
          "query"
        ],
        "description": "Which strategy was used for a given probe attempt:\n  * `accept` — `Accept: text/markdown` request header.\n  * `suffix` — `.md` appended to the URL path.\n  * `query`  — `?format=markdown` appended to the URL.\n"
      },
      "CloudflareStatus": {
        "type": "string",
        "enum": [
          "yes",
          "no",
          "unknown"
        ],
        "description": "Whether the target URL is served through Cloudflare:\n  * `yes`     — at least one Cloudflare response header was observed.\n  * `no`      — no Cloudflare signals found.\n  * `unknown` — the precheck request failed before headers could be read.\n"
      },
      "ReasonCode": {
        "type": "string",
        "enum": [
          "notCloudflare",
          "notEnabled",
          "blocked",
          "redirect",
          "status4xx",
          "status5xx",
          "timeout",
          "invalidUrl",
          "fetchFailed"
        ],
        "description": "Machine-readable reason codes explaining why a check did not succeed.\nEach localised human-readable description is available in the UI. Any\ncombination may appear.\n"
      },
      "StrategyAttempt": {
        "type": "object",
        "required": [
          "strategy",
          "requestUrl",
          "ok",
          "status",
          "contentType",
          "size",
          "looksLikeMarkdown",
          "preview"
        ],
        "properties": {
          "strategy": {
            "$ref": "#/components/schemas/CheckStrategy"
          },
          "requestUrl": {
            "type": "string",
            "format": "uri",
            "description": "Exact URL that was requested for this attempt."
          },
          "ok": {
            "type": "boolean",
            "description": "Shorthand for `response.ok` (HTTP 2xx)."
          },
          "status": {
            "type": "integer",
            "nullable": true,
            "description": "HTTP status code, or `null` if the request failed before a response was received.",
            "minimum": 100,
            "maximum": 599
          },
          "contentType": {
            "type": "string",
            "nullable": true,
            "description": "Response `Content-Type` header."
          },
          "size": {
            "type": "integer",
            "nullable": true,
            "description": "Number of bytes read from the response body (capped at 64 KB).",
            "minimum": 0
          },
          "looksLikeMarkdown": {
            "type": "boolean",
            "description": "Whether the response body passed heuristic Markdown detection."
          },
          "preview": {
            "type": "string",
            "nullable": true,
            "description": "Up to 1200 chars of the response body if Markdown was detected,\n400 chars otherwise. May be `null` if the request failed.\n"
          },
          "error": {
            "type": "string",
            "description": "Error message from the fetch (set only when the request failed)."
          }
        }
      },
      "CheckResult": {
        "type": "object",
        "required": [
          "input",
          "normalisedUrl",
          "cloudflare",
          "cloudflareSignals",
          "success",
          "winningStrategy",
          "attempts",
          "reasonCodes",
          "durationMs"
        ],
        "properties": {
          "input": {
            "type": "string",
            "description": "Raw user input, unchanged."
          },
          "normalisedUrl": {
            "type": "string",
            "description": "URL after whitespace stripping, scheme prepending, and `new URL()`\nnormalisation. Equal to `input` if parsing failed.\n"
          },
          "cloudflare": {
            "$ref": "#/components/schemas/CloudflareStatus"
          },
          "cloudflareSignals": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Human-readable list of Cloudflare indicators observed (e.g.\n`server: cloudflare`, `cf-ray: <id>`). Empty when `cloudflare !== \"yes\"`.\n"
          },
          "success": {
            "type": "boolean",
            "description": "`true` if at least one strategy returned a valid Markdown response.\nSource of truth for \"does this URL pass the check\".\n"
          },
          "winningStrategy": {
            "type": "string",
            "nullable": true,
            "enum": [
              "accept",
              "suffix",
              "query",
              null
            ],
            "description": "The strategy that succeeded, or `null` if none did. Values match\n`CheckStrategy` — the enum is inlined (with `null`) here because\nOpenAPI 3.0 cannot mark a `$ref` as nullable.\n"
          },
          "attempts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/StrategyAttempt"
            },
            "description": "One entry per strategy tried, in order (`accept`, `suffix`, `query`).\nThe array ends early when a strategy wins.\n"
          },
          "reasonCodes": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReasonCode"
            },
            "description": "Zero or more machine-readable reasons explaining the result."
          },
          "durationMs": {
            "type": "integer",
            "minimum": 0,
            "description": "Wall-clock duration of the check in milliseconds."
          }
        }
      },
      "CheckSuccessResponse": {
        "type": "object",
        "required": [
          "ok",
          "result"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              true
            ],
            "description": "Always `true` for this response shape."
          },
          "result": {
            "$ref": "#/components/schemas/CheckResult"
          }
        }
      },
      "ErrorResponse": {
        "type": "object",
        "required": [
          "ok",
          "error"
        ],
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              false
            ],
            "description": "Always `false` for this response shape."
          },
          "error": {
            "type": "string",
            "description": "Short machine-readable error code:\n  * `missing_url_param` — no `url` query parameter provided.\n  * `method_not_allowed` — wrong HTTP method for the endpoint.\n  * other — unexpected server-side errors include the raw message.\n"
          }
        }
      }
    }
  }
}
