{
    "openapi": "3.1.0",
    "info": {
        "title": "PublicWWW Search API",
        "version": "1.0",
        "summary": "Search the source code (HTML, JavaScript, CSS) of hundreds of millions of websites.",
        "description": "The same search the website runs, returned as data. Every call is a read. Authenticate with `Authorization: Bearer <token>`; tokens are issued at https://publicwww.com/profile/api and a paid plan is required. Applications can obtain the same token through OAuth 2.1 (authorization code with PKCE, client metadata documents instead of client registration): https://publicwww.com/docs/api/authentication/#oauth\n\nAt most 10 requests per minute per account, shared with the MCP server; a request over the limit gets 429 with Retry-After at once. Searches count against the plan's daily quota, which resets at UTC midnight.\n\nAI assistants can use the same search through the MCP server at /mcp: https://publicwww.com/docs/mcp/"
    },
    "externalDocs": {
        "description": "API documentation",
        "url": "https://publicwww.com/docs/api/"
    },
    "servers": [
        {
            "url": "https://api.publicwww.com"
        }
    ],
    "security": [
        {
            "bearerAuth": []
        }
    ],
    "tags": [
        {
            "name": "search",
            "description": "Websites whose page source contains the query."
        },
        {
            "name": "account",
            "description": "Plan, quota and limits."
        }
    ],
    "paths": {
        "/v1/search": {
            "get": {
                "operationId": "searchWebsites",
                "tags": [
                    "search"
                ],
                "summary": "Find websites whose page source contains a code fragment",
                "description": "Results are websites ranked by popularity. Bulk is this endpoint with a large per_page (up to the plan's row limit, see /v1/account); the answer streams as it is produced. A plan covers the first max_per_page rows of a query: page * per_page may not exceed it.",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/query"
                    },
                    {
                        "$ref": "#/components/parameters/page"
                    },
                    {
                        "$ref": "#/components/parameters/per_page"
                    },
                    {
                        "$ref": "#/components/parameters/snippets"
                    },
                    {
                        "$ref": "#/components/parameters/format"
                    },
                    {
                        "$ref": "#/components/parameters/columns"
                    },
                    {
                        "$ref": "#/components/parameters/delimiter"
                    },
                    {
                        "$ref": "#/components/parameters/header"
                    }
                ],
                "responses": {
                    "200": {
                        "$ref": "#/components/responses/SearchOk"
                    },
                    "400": {
                        "$ref": "#/components/responses/BadRequest"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "429": {
                        "$ref": "#/components/responses/TooManyRequests"
                    }
                }
            },
            "post": {
                "operationId": "searchWebsitesPost",
                "tags": [
                    "search"
                ],
                "summary": "The same search with parameters in a JSON body",
                "description": "For queries too long or too multi-line for a url. Takes the same parameters as GET; an array in query means all of those phrases.",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/SearchRequest"
                            },
                            "example": {
                                "query": "\"angular.min.js\"",
                                "per_page": 10
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "$ref": "#/components/responses/SearchOk"
                    },
                    "400": {
                        "$ref": "#/components/responses/BadRequest"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "$ref": "#/components/responses/Forbidden"
                    },
                    "429": {
                        "$ref": "#/components/responses/TooManyRequests"
                    }
                }
            }
        },
        "/v1/account": {
            "get": {
                "operationId": "getAccount",
                "tags": [
                    "account"
                ],
                "summary": "Plan, quota used today and limits",
                "description": "Spends nothing and does not count against the request rate.",
                "parameters": [
                    {
                        "name": "format",
                        "in": "query",
                        "required": false,
                        "description": "json (default) or xml.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "json",
                                "xml"
                            ],
                            "default": "json"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The account.",
                        "headers": {
                            "X-RateLimit-Limit": {
                                "$ref": "#/components/headers/X-RateLimit-Limit"
                            },
                            "X-RateLimit-Remaining": {
                                "$ref": "#/components/headers/X-RateLimit-Remaining"
                            },
                            "X-RateLimit-Reset": {
                                "$ref": "#/components/headers/X-RateLimit-Reset"
                            },
                            "X-Snippets-Limit": {
                                "$ref": "#/components/headers/X-Snippets-Limit"
                            },
                            "X-Snippets-Remaining": {
                                "$ref": "#/components/headers/X-Snippets-Remaining"
                            }
                        },
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Account"
                                }
                            },
                            "application/xml": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    },
                    "400": {
                        "$ref": "#/components/responses/BadRequest"
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    }
                }
            }
        }
    },
    "components": {
        "securitySchemes": {
            "bearerAuth": {
                "type": "http",
                "scheme": "bearer",
                "description": "A token from https://publicwww.com/profile/api, or one obtained through OAuth 2.1. Tokens live until deleted or revoked. A token in the query string is not accepted."
            }
        },
        "parameters": {
            "query": {
                "name": "query",
                "in": "query",
                "required": true,
                "description": "The search, in the same syntax as the website search box: \"exact phrase\", several words for all of them, -word to exclude, site:de, depth:all. Several phrases: separate them with newlines. https://publicwww.com/docs/query-syntax/",
                "schema": {
                    "type": "string",
                    "minLength": 1
                },
                "example": "\"angular.min.js\""
            },
            "page": {
                "name": "page",
                "in": "query",
                "required": false,
                "description": "Page number, from 1.",
                "schema": {
                    "type": "integer",
                    "minimum": 1,
                    "default": 1
                }
            },
            "per_page": {
                "name": "per_page",
                "in": "query",
                "required": false,
                "description": "Rows per page, up to the plan's max_per_page (max_per_page_snippets with snippets) from /v1/account.",
                "schema": {
                    "type": "integer",
                    "minimum": 1,
                    "default": 100
                }
            },
            "snippets": {
                "name": "snippets",
                "in": "query",
                "required": false,
                "description": "1 to include fragments of page source around the match (up to 5 per row). Spends the daily snippet quota.",
                "schema": {
                    "type": "boolean",
                    "default": false
                }
            },
            "format": {
                "name": "format",
                "in": "query",
                "required": false,
                "description": "json (default); ndjson - one object per line, metadata first; xml; csv - semicolon separated, no header, domain;rank by default; tsv - tab separated; txt - one url per line. Totals for csv, tsv and txt are in the X-Total-Results, X-Truncated and X-Index-Complete headers.",
                "schema": {
                    "type": "string",
                    "enum": [
                        "json",
                        "ndjson",
                        "xml",
                        "csv",
                        "tsv",
                        "txt"
                    ],
                    "default": "json"
                }
            },
            "columns": {
                "name": "columns",
                "in": "query",
                "required": false,
                "description": "Comma separated subset and order of domain, url, rank, ranked, snippets (snippets needs snippets=1). Defaults: all fields for json, ndjson and xml; domain,rank for csv and tsv; url for txt.",
                "schema": {
                    "type": "string",
                    "pattern": "^(domain|url|rank|ranked|snippets)(,(domain|url|rank|ranked|snippets))*$"
                },
                "example": "url,rank"
            },
            "delimiter": {
                "name": "delimiter",
                "in": "query",
                "required": false,
                "description": "Field separator for csv and tsv. Default ; for csv and tab for tsv.",
                "schema": {
                    "type": "string"
                }
            },
            "header": {
                "name": "header",
                "in": "query",
                "required": false,
                "description": "1 to put a header line on csv and tsv.",
                "schema": {
                    "type": "boolean",
                    "default": false
                }
            }
        },
        "headers": {
            "X-Total-Results": {
                "description": "Websites matching the query in the index.",
                "schema": {
                    "type": "integer"
                }
            },
            "X-Returned-Results": {
                "description": "Rows in this answer.",
                "schema": {
                    "type": "integer"
                }
            },
            "X-Truncated": {
                "description": "true when the plan cut this page short.",
                "schema": {
                    "type": "string",
                    "enum": [
                        "true",
                        "false"
                    ]
                }
            },
            "X-Index-Complete": {
                "description": "false when part of the index did not answer and the results may be incomplete.",
                "schema": {
                    "type": "string",
                    "enum": [
                        "true",
                        "false"
                    ]
                }
            },
            "X-RateLimit-Limit": {
                "description": "Searches per day on the plan.",
                "schema": {
                    "type": "integer"
                }
            },
            "X-RateLimit-Remaining": {
                "description": "Searches left today.",
                "schema": {
                    "type": "integer"
                }
            },
            "X-RateLimit-Reset": {
                "description": "Unix time of the next quota reset (UTC midnight).",
                "schema": {
                    "type": "integer"
                }
            },
            "X-Snippets-Limit": {
                "description": "Searches with snippets per day on the plan.",
                "schema": {
                    "type": "integer"
                }
            },
            "X-Snippets-Remaining": {
                "description": "Searches with snippets left today.",
                "schema": {
                    "type": "integer"
                }
            },
            "Retry-After": {
                "description": "Seconds to wait before repeating.",
                "schema": {
                    "type": "integer"
                }
            }
        },
        "responses": {
            "SearchOk": {
                "description": "Search results. The body follows format; the totals are also in the headers.",
                "headers": {
                    "X-Total-Results": {
                        "$ref": "#/components/headers/X-Total-Results"
                    },
                    "X-Returned-Results": {
                        "$ref": "#/components/headers/X-Returned-Results"
                    },
                    "X-Truncated": {
                        "$ref": "#/components/headers/X-Truncated"
                    },
                    "X-Index-Complete": {
                        "$ref": "#/components/headers/X-Index-Complete"
                    },
                    "X-RateLimit-Limit": {
                        "$ref": "#/components/headers/X-RateLimit-Limit"
                    },
                    "X-RateLimit-Remaining": {
                        "$ref": "#/components/headers/X-RateLimit-Remaining"
                    },
                    "X-RateLimit-Reset": {
                        "$ref": "#/components/headers/X-RateLimit-Reset"
                    },
                    "X-Snippets-Limit": {
                        "$ref": "#/components/headers/X-Snippets-Limit"
                    },
                    "X-Snippets-Remaining": {
                        "$ref": "#/components/headers/X-Snippets-Remaining"
                    }
                },
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/SearchResponse"
                        },
                        "example": {
                            "query": "\"angular.min.js\"",
                            "page": 1,
                            "per_page": 2,
                            "total": 278,
                            "total_pages": 139,
                            "returned": 2,
                            "truncated": false,
                            "index_complete": true,
                            "took_ms": 2,
                            "results": [
                                {
                                    "domain": "imgbox.com",
                                    "url": "https://imgbox.com/",
                                    "rank": 4187,
                                    "ranked": true
                                },
                                {
                                    "domain": "example.org",
                                    "url": "https://example.org/",
                                    "rank": null,
                                    "ranked": false
                                }
                            ]
                        }
                    },
                    "application/x-ndjson": {
                        "schema": {
                            "type": "string",
                            "description": "First line: the metadata object with \"object\": \"meta\" (fields as in SearchMeta). Then one SearchResult object per line."
                        }
                    },
                    "application/xml": {
                        "schema": {
                            "type": "string",
                            "description": "<response> with the metadata elements and <results><result>...</result></results>."
                        }
                    },
                    "text/csv": {
                        "schema": {
                            "type": "string",
                            "description": "One row per line, columns as asked, no header unless header=1."
                        },
                        "example": "imgbox.com;4187\n"
                    },
                    "text/tab-separated-values": {
                        "schema": {
                            "type": "string"
                        }
                    },
                    "text/plain": {
                        "schema": {
                            "type": "string",
                            "description": "One url per line."
                        },
                        "example": "https://imgbox.com/\n"
                    }
                }
            },
            "BadRequest": {
                "description": "The request cannot be served as asked: missing_query, unknown_format, unknown_column, format_not_available, per_page_too_large, page_too_deep, invalid_json.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/Error"
                        }
                    }
                }
            },
            "Unauthorized": {
                "description": "missing_key - no Authorization: Bearer header; invalid_key - the token does not name an account.",
                "headers": {
                    "WWW-Authenticate": {
                        "description": "Bearer realm=\"publicwww\"",
                        "schema": {
                            "type": "string"
                        }
                    }
                },
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/Error"
                        }
                    }
                }
            },
            "Forbidden": {
                "description": "plan_required - the account has no paid plan.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/Error"
                        }
                    }
                }
            },
            "TooManyRequests": {
                "description": "too_many_requests - more than 10 requests a minute, wait Retry-After seconds; quota_exceeded - the day's searches are used up; snippet_quota_exceeded - the day's snippet searches are used up, searching without snippets still works.",
                "headers": {
                    "Retry-After": {
                        "$ref": "#/components/headers/Retry-After"
                    }
                },
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/Error"
                        }
                    }
                }
            }
        },
        "schemas": {
            "SearchRequest": {
                "type": "object",
                "required": [
                    "query"
                ],
                "properties": {
                    "query": {
                        "description": "The search; an array means all of those phrases.",
                        "oneOf": [
                            {
                                "type": "string",
                                "minLength": 1
                            },
                            {
                                "type": "array",
                                "items": {
                                    "type": "string"
                                },
                                "minItems": 1
                            }
                        ]
                    },
                    "page": {
                        "type": "integer",
                        "minimum": 1,
                        "default": 1
                    },
                    "per_page": {
                        "type": "integer",
                        "minimum": 1,
                        "default": 100
                    },
                    "snippets": {
                        "type": "boolean",
                        "default": false
                    },
                    "format": {
                        "type": "string",
                        "enum": [
                            "json",
                            "ndjson",
                            "xml",
                            "csv",
                            "tsv",
                            "txt"
                        ],
                        "default": "json"
                    },
                    "columns": {
                        "type": "string",
                        "pattern": "^(domain|url|rank|ranked|snippets)(,(domain|url|rank|ranked|snippets))*$"
                    },
                    "delimiter": {
                        "type": "string"
                    },
                    "header": {
                        "type": "boolean",
                        "default": false
                    }
                }
            },
            "SearchMeta": {
                "type": "object",
                "required": [
                    "query",
                    "page",
                    "per_page",
                    "total",
                    "total_pages",
                    "returned",
                    "truncated",
                    "index_complete",
                    "took_ms"
                ],
                "properties": {
                    "query": {
                        "type": "string"
                    },
                    "page": {
                        "type": "integer"
                    },
                    "per_page": {
                        "type": "integer"
                    },
                    "total": {
                        "type": "integer",
                        "description": "Websites matching the query in the index."
                    },
                    "total_pages": {
                        "type": "integer"
                    },
                    "returned": {
                        "type": "integer",
                        "description": "Rows in this answer."
                    },
                    "truncated": {
                        "type": "boolean",
                        "description": "true when the plan cut this page short."
                    },
                    "index_complete": {
                        "type": "boolean",
                        "description": "false when part of the index did not answer in time; the results may be incomplete."
                    },
                    "took_ms": {
                        "type": "integer"
                    },
                    "query_note": {
                        "type": "string",
                        "description": "Present when the query could not be run as written, with the reason."
                    }
                }
            },
            "SearchResult": {
                "type": "object",
                "description": "Fields as selected with columns; all of them by default.",
                "properties": {
                    "domain": {
                        "type": "string"
                    },
                    "url": {
                        "type": "string",
                        "description": "The page the match was found on."
                    },
                    "rank": {
                        "type": [
                            "integer",
                            "null"
                        ],
                        "description": "Popularity rank, 1 is the most popular; null for an unranked website."
                    },
                    "ranked": {
                        "type": "boolean"
                    },
                    "snippets": {
                        "type": "array",
                        "description": "Only with snippets=1. Up to 5 fragments of page source around the match.",
                        "items": {
                            "$ref": "#/components/schemas/Snippet"
                        }
                    }
                }
            },
            "Snippet": {
                "type": "object",
                "required": [
                    "text",
                    "match"
                ],
                "properties": {
                    "text": {
                        "type": "string",
                        "description": "The fragment of page source."
                    },
                    "match": {
                        "type": "string",
                        "description": "The part of text that matched the query."
                    }
                }
            },
            "SearchResponse": {
                "allOf": [
                    {
                        "$ref": "#/components/schemas/SearchMeta"
                    },
                    {
                        "type": "object",
                        "required": [
                            "results"
                        ],
                        "properties": {
                            "results": {
                                "type": "array",
                                "items": {
                                    "$ref": "#/components/schemas/SearchResult"
                                }
                            }
                        }
                    }
                ]
            },
            "QuotaCounter": {
                "type": "object",
                "required": [
                    "limit",
                    "used",
                    "resets_at"
                ],
                "properties": {
                    "limit": {
                        "type": "integer"
                    },
                    "used": {
                        "type": "integer"
                    },
                    "resets_at": {
                        "type": "integer",
                        "description": "Unix time of the next reset (UTC midnight)."
                    }
                }
            },
            "Account": {
                "type": "object",
                "required": [
                    "plan",
                    "plan_until",
                    "full_access",
                    "quota",
                    "limits"
                ],
                "properties": {
                    "plan": {
                        "type": "string"
                    },
                    "plan_until": {
                        "type": "integer",
                        "description": "Unix time the plan is paid until; 0 without a plan."
                    },
                    "full_access": {
                        "type": "boolean",
                        "description": "Whether the API is available to this account."
                    },
                    "quota": {
                        "type": "object",
                        "required": [
                            "searches",
                            "snippets"
                        ],
                        "properties": {
                            "searches": {
                                "$ref": "#/components/schemas/QuotaCounter"
                            },
                            "snippets": {
                                "$ref": "#/components/schemas/QuotaCounter"
                            }
                        }
                    },
                    "limits": {
                        "type": "object",
                        "required": [
                            "disclosed_positions",
                            "disclosed_positions_snippets",
                            "max_per_page",
                            "max_per_page_snippets"
                        ],
                        "properties": {
                            "disclosed_positions": {
                                "type": "integer",
                                "description": "How far down the popularity ranking results are shown."
                            },
                            "disclosed_positions_snippets": {
                                "type": "integer"
                            },
                            "max_per_page": {
                                "type": "integer",
                                "description": "Rows one query may return: the largest per_page, and the limit of page * per_page."
                            },
                            "max_per_page_snippets": {
                                "type": "integer"
                            }
                        }
                    }
                }
            },
            "Error": {
                "type": "object",
                "required": [
                    "error"
                ],
                "properties": {
                    "error": {
                        "type": "object",
                        "required": [
                            "code",
                            "message"
                        ],
                        "description": "Branch on code; message is for people and may be reworded.",
                        "properties": {
                            "code": {
                                "type": "string",
                                "enum": [
                                    "missing_query",
                                    "unknown_format",
                                    "unknown_column",
                                    "format_not_available",
                                    "per_page_too_large",
                                    "page_too_deep",
                                    "invalid_json",
                                    "missing_key",
                                    "invalid_key",
                                    "plan_required",
                                    "unknown_endpoint",
                                    "method_not_allowed",
                                    "too_many_requests",
                                    "quota_exceeded",
                                    "snippet_quota_exceeded"
                                ]
                            },
                            "message": {
                                "type": "string"
                            },
                            "parameter": {
                                "type": "string",
                                "description": "The parameter that was wrong."
                            },
                            "max": {
                                "type": "integer"
                            },
                            "max_rows": {
                                "type": "integer"
                            },
                            "max_page": {
                                "type": "integer"
                            },
                            "limit": {
                                "type": "integer"
                            },
                            "used": {
                                "type": "integer"
                            },
                            "retry_after": {
                                "type": "integer",
                                "description": "Seconds to wait, also in the Retry-After header."
                            },
                            "plan": {
                                "type": "string"
                            },
                            "endpoints": {
                                "type": "array",
                                "items": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
