{
    "name": "PublicWWW Search API",
    "version": "v1",
    "docs": "https://publicwww.com/docs/api/",
    "openapi": "https://api.publicwww.com/openapi.json",
    "authentication": {
        "header": "Authorization: Bearer <your token>",
        "note": "A paid plan is required for /v1. Tokens are issued at https://publicwww.com/profile/api . A token in the query string is not accepted here - it would end up in access logs and referrers.",
        "oauth": {
            "authorization_server": "https://publicwww.com",
            "metadata": "https://publicwww.com/.well-known/oauth-authorization-server",
            "resource_metadata": "https://api.publicwww.com/.well-known/oauth-protected-resource",
            "note": "Authorization code with PKCE (S256). client_id is the https URL of a client metadata document; there is no client registration. The token goes in the same Authorization: Bearer header and lives until revoked - no expiry, no refresh token. https://publicwww.com/docs/api/authentication/#oauth"
        }
    },
    "mcp": {
        "url": "https://api.publicwww.com/mcp",
        "transport": "Streamable HTTP, JSON-RPC 2.0, one message per POST, application/json answers",
        "auth": "Authorization: Bearer <the same token>; free accounts get a daily allowance",
        "tools": [
            "search",
            "account"
        ],
        "docs": "https://publicwww.com/docs/mcp/"
    },
    "endpoints": [
        {
            "path": "/v1/search",
            "methods": [
                "GET",
                "POST"
            ],
            "params": {
                "query": "required. Same syntax as the website search box. Several phrases: separate with newlines, or POST an array.",
                "page": "optional, default 1.",
                "per_page": "optional, default 100. Up to the row limit of your plan - see /v1/account.",
                "snippets": "optional, 1 to include matching text. Spends snippet quota.",
                "format": "optional, one of json, ndjson, xml, csv, tsv, txt. Default json.",
                "columns": "optional, comma separated subset of domain, url, rank, ranked, snippets.",
                "delimiter": "optional, for csv and tsv. Default ; and tab.",
                "header": "optional, 1 to put a header line on csv and tsv."
            },
            "note": "POST takes the same parameters as a JSON body, for queries too long or too multi-line to sit in a url."
        },
        {
            "path": "/v1/account",
            "methods": [
                "GET"
            ],
            "note": "Plan, quota and limits. Does not spend quota."
        }
    ],
    "formats": {
        "json": "default. One object; results in an array.",
        "ndjson": "one JSON object per line, metadata first. For streaming into a pipeline.",
        "xml": "the same document as XML.",
        "csv": "semicolon separated, no header. Defaults to domain;rank.",
        "tsv": "tab separated, otherwise as csv.",
        "txt": "one url per line."
    },
    "errors": {
        "400 missing_query": "'query' was empty or absent.",
        "400 unknown_format": "'format' is not one of the formats above.",
        "400 unknown_column": "'columns' names a field that does not exist.",
        "400 format_not_available": "csv, tsv and txt only fit search results.",
        "400 per_page_too_large": "'per_page' above what the plan allows.",
        "400 page_too_deep": "'page' * 'per_page' past the plan's rows per report: a plan covers the first N rows of a query.",
        "400 invalid_json": "POST body is not valid JSON.",
        "401 missing_key": "No Authorization: Bearer header.",
        "401 invalid_key": "The key does not name an account.",
        "403 plan_required": "The account has no paid plan.",
        "404 unknown_endpoint": "No such path.",
        "405 method_not_allowed": "Read-only API; use GET or POST.",
        "429 too_many_requests": "Arriving too fast. Wait Retry-After seconds and repeat.",
        "429 quota_exceeded": "Daily search quota spent. Resets at UTC midnight.",
        "429 snippet_quota_exceeded": "Daily snippet quota spent. Searching without snippets still works."
    },
    "rate_limit": {
        "requests": "10 per minute, per account, shared by the API and the MCP server.",
        "on_exceed": "429 with a Retry-After header, immediately. The API never holds the connection open to slow you down - it tells you how long to wait.",
        "headers": [
            "X-RateLimit-Limit",
            "X-RateLimit-Remaining",
            "X-RateLimit-Reset",
            "X-Snippets-Limit",
            "X-Snippets-Remaining"
        ]
    },
    "guarantees": [
        "Results are never silently reduced. If your quota is gone you get 429, not fewer rows.",
        "A short answer is marked: 'truncated' in the body, X-Truncated in the headers.",
        "An unranked domain has rank null and ranked false, never a placeholder number."
    ],
    "compatibility": "The old ?export= urls on https://publicwww.com keep working exactly as they always have, byte for byte. They are the compatibility layer; this API is not, and its output is free to be its own shape."
}
