{
  "openapi": "3.1.0",
  "info": {
    "title": "Hottub Search",
    "version": "1.0.0",
    "summary": "One typed search answer for agents: Hottub Directory entities, documents and Hottub content.",
    "description": "Free to read, no account or key required, rate limited per client IP (60 requests a minute across /v1, /mcp and results pages; 120 on /v1 with a signed-in session). Every response sends `RateLimit-Policy`. Paid rows appear only in `sponsored`, always labeled, and never change organic order. Branch on status codes and `error.code`, never on message text. The same search is an MCP tool (`hottub_search`) at https://search.joinhottub.com/mcp, and every results page is Markdown with `&format=md`.",
    "termsOfService": "https://joinhottub.com/terms"
  },
  "servers": [
    {
      "url": "https://search.joinhottub.com",
      "description": "Hottub Search (canonical)"
    },
    {
      "url": "https://joinhottub.com/api/search",
      "description": "The same API on joinhottub.com"
    }
  ],
  "externalDocs": {
    "url": "https://search.joinhottub.com/agents"
  },
  "paths": {
    "/v1": {
      "get": {
        "operationId": "search",
        "summary": "Search Hottub and the web",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1,
              "maxLength": 256
            },
            "description": "What the person needs, in plain words. Operators: `site:example.com` (that site and its subdomains), `\"exact phrase\"` and `-word`; they are read once, sent to the index as structured fields and echoed in `read`."
          },
          {
            "name": "scope",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "web",
                "places",
                "videos",
                "news",
                "images",
                "hottub"
              ],
              "default": "all"
            },
            "description": "`all`, `web` and `hottub` as always; `places` (Directory places only), `news` (Hottub News headlines in `news`), `videos` and `images` (results that carry a video or a picture)."
          },
          {
            "name": "safe",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "strict",
                "moderate",
                "off"
              ],
              "default": "moderate"
            },
            "description": "SafeSearch. `moderate` hides adult pictures and videos and adult-labelled sites; `strict` also hides explicit pages; `off` hides nothing that is legal. Never remembered between requests."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 25,
              "default": 10
            }
          },
          {
            "name": "near",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 48
            },
            "description": "\"lat,lon\" to rank entities by distance."
          },
          {
            "name": "radius_m",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 50,
              "maximum": 50000
            }
          },
          {
            "name": "min_confidence",
            "in": "query",
            "schema": {
              "type": "number",
              "minimum": 0,
              "maximum": 1
            },
            "description": "Drop entities below this Directory confidence."
          },
          {
            "name": "location",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 64
            },
            "description": "Where the person is (\"Spokane, WA\", \"99201\"), used when `q` names no place: \"wendys\" with location \"Spokane, WA\" returns the Wendy's in Spokane. Agents should pass the person's location here or as `near`; the service never infers it from the caller's IP."
          },
          {
            "name": "content",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "Also return `content`, the readable text of the first 10 web results (up to 4,000 characters each)."
          }
        ],
        "responses": {
          "200": {
            "description": "A search answer.",
            "headers": {
              "RateLimit-Policy": {
                "schema": {
                  "type": "string"
                },
                "description": "Structured field (draft-ietf-httpapi-ratelimit-headers): the quota `q` per window `w` seconds, e.g. `\"ip\";q=60;w=60`."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              }
            }
          },
          "400": {
            "description": "Unknown or malformed parameter (`invalid_request`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "414": {
            "description": "Query string over 1024 bytes (`request_too_large`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (`rate_limited`); wait `Retry-After` seconds.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                }
              },
              "RateLimit-Policy": {
                "schema": {
                  "type": "string"
                },
                "description": "Structured field (draft-ietf-httpapi-ratelimit-headers): the quota `q` per window `w` seconds, e.g. `\"ip\";q=60;w=60`."
              },
              "RateLimit": {
                "schema": {
                  "type": "string"
                },
                "description": "Remaining quota `r` and seconds `t` until it resets, e.g. `\"ip\";r=0;t=42`."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Search is unavailable (`search_unavailable`); retry later.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_request",
                  "request_too_large",
                  "rate_limited",
                  "search_unavailable"
                ]
              }
            }
          }
        }
      },
      "Provenance": {
        "type": "object",
        "required": [
          "source",
          "dataset",
          "release",
          "observed_at_unix",
          "license",
          "attribution",
          "current_details_verified"
        ],
        "properties": {
          "source": {
            "const": "hottub-directory"
          },
          "dataset": {
            "type": [
              "string",
              "null"
            ]
          },
          "release": {
            "type": [
              "string",
              "null"
            ]
          },
          "observed_at_unix": {
            "type": [
              "integer",
              "null"
            ]
          },
          "license": {
            "type": [
              "string",
              "null"
            ]
          },
          "attribution": {
            "type": [
              "string",
              "null"
            ]
          },
          "current_details_verified": {
            "const": false,
            "description": "Hours, phone and other current details are not verified; confirm before relying on them."
          }
        }
      },
      "Entity": {
        "type": "object",
        "description": "A Hottub Directory business or organization that offers services. Further compact Directory fields (address, phone, locality, hours, operating_status, services, service_record_url, verification, commercial, location, confidence, categories, freshness, capabilities) may be present.",
        "required": [
          "id",
          "entity_id",
          "name",
          "website",
          "provenance"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "entity_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "website": {
            "type": [
              "string",
              "null"
            ]
          },
          "thumb": {
            "type": "string",
            "format": "uri",
            "description": "Optional. A 320 × 180 preview (WebP or PNG) of the picture the business's own website declares for itself (og:image), served by Hottub Search. Present only when one is ready; display only, it never affects order, and paid placements never carry one."
          },
          "provenance": {
            "$ref": "#/components/schemas/Provenance"
          }
        },
        "additionalProperties": true
      },
      "Result": {
        "type": "object",
        "required": [
          "kind",
          "url",
          "display_url",
          "title",
          "snippet",
          "sources"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "hottub",
              "web"
            ]
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "display_url": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "snippet": {
            "type": "string"
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "host": {
            "type": "string",
            "description": "Optional. Host name of `url`."
          },
          "site": {
            "type": "string",
            "description": "Optional. Registrable site of `url`."
          },
          "breadcrumb": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Optional. Path segments shown under the title."
          },
          "site_name": {
            "type": "string",
            "description": "Optional. The site's own name, when known."
          },
          "language": {
            "type": "string",
            "description": "Optional. BCP 47 language of the document."
          },
          "captured_at": {
            "type": "integer",
            "description": "Optional. When the source last saw the document (Unix seconds)."
          },
          "quality": {
            "type": "integer",
            "minimum": 0,
            "maximum": 1000,
            "description": "Optional. Site quality from the link graph (0–1000); compare only within one answer."
          },
          "score": {
            "type": "number",
            "minimum": 0,
            "maximum": 1,
            "description": "Optional. Relevance 0–1, comparable only within one answer; never rises down the list."
          },
          "extra_snippets": {
            "type": "array",
            "maxItems": 3,
            "items": {
              "type": "string",
              "maxLength": 240
            },
            "description": "Optional. More query-relevant passages from the page."
          },
          "content": {
            "type": "string",
            "maxLength": 4000,
            "description": "Optional, only with `content=true`: the readable text of one of the first 10 web results."
          },
          "video": {
            "type": "object",
            "description": "Optional (videos tab): the video the page declares about itself. The page's own claim; display only.",
            "properties": {
              "duration_s": {
                "type": "integer"
              },
              "channel": {
                "type": "string"
              },
              "uploaded_at": {
                "type": "integer"
              },
              "thumb_url": {
                "type": "string",
                "format": "uri"
              }
            }
          },
          "image": {
            "type": "object",
            "description": "Optional (images tab): the picture the page declares for itself. Display only.",
            "required": [
              "src"
            ],
            "properties": {
              "src": {
                "type": "string",
                "format": "uri"
              },
              "width": {
                "type": "integer"
              },
              "height": {
                "type": "integer"
              },
              "alt": {
                "type": "string"
              }
            }
          }
        }
      },
      "News": {
        "type": "object",
        "description": "Present on the news tab: headlines from Hottub News (https://hottub.news/), news reports only and never paid. Show the headline, outlet and link, and credit Hottub News. A `status` other than `ok` means it did not answer, not that there is no news.",
        "required": [
          "source",
          "status",
          "count",
          "items",
          "attribution"
        ],
        "properties": {
          "source": {
            "const": "hottub_news"
          },
          "status": {
            "type": "string",
            "enum": [
              "ok",
              "unavailable",
              "timeout",
              "not_configured",
              "skipped",
              "budget_exhausted",
              "circuit_open"
            ],
            "description": "Per-source outcome. Anything but `ok` means that source contributed nothing; it is not a negative answer."
          },
          "count": {
            "type": "integer"
          },
          "lang": {
            "type": "string",
            "description": "The language asked for (ISO 639-1): the query's own, else the request's `Accept-Language`, else `en`."
          },
          "lang_source": {
            "type": "string",
            "enum": [
              "query",
              "request",
              "default"
            ]
          },
          "widened": {
            "type": "boolean",
            "description": "Fewer than 3 headlines in `lang`: these are from every language, that language's first."
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "title",
                "url",
                "outlet"
              ],
              "properties": {
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                },
                "outlet": {
                  "type": "string"
                },
                "published_unix": {
                  "type": "integer"
                },
                "teaser": {
                  "type": "string",
                  "maxLength": 201
                },
                "lang": {
                  "type": "string"
                }
              }
            }
          },
          "attribution": {
            "type": "object",
            "properties": {
              "text": {
                "type": "string"
              },
              "url": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "Sponsored": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "kind",
              "label",
              "disclosure",
              "entity"
            ],
            "properties": {
              "kind": {
                "const": "directory_placement"
              },
              "label": {
                "const": "Sponsored"
              },
              "disclosure": {
                "type": "string"
              },
              "entity": {
                "$ref": "#/components/schemas/Entity"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "kind",
              "label",
              "disclosure",
              "advertiser_name",
              "url",
              "display_url",
              "event_id"
            ],
            "properties": {
              "kind": {
                "const": "intent_word"
              },
              "label": {
                "const": "Sponsored"
              },
              "disclosure": {
                "type": "string"
              },
              "advertiser_name": {
                "type": "string"
              },
              "url": {
                "type": "string",
                "format": "uri"
              },
              "display_url": {
                "type": "string"
              },
              "event_id": {
                "type": "string"
              }
            }
          }
        ]
      },
      "SearchResponse": {
        "type": "object",
        "required": [
          "schema",
          "version",
          "query",
          "scope",
          "entities",
          "results",
          "sponsored",
          "sources",
          "directory",
          "coverage",
          "partial",
          "as_of_unix"
        ],
        "properties": {
          "schema": {
            "const": "hottub.search.v1"
          },
          "version": {
            "const": 1
          },
          "query": {
            "type": "string"
          },
          "scope": {
            "type": "string",
            "enum": [
              "all",
              "web",
              "places",
              "videos",
              "news",
              "images",
              "hottub"
            ]
          },
          "safe": {
            "type": "string",
            "enum": [
              "strict",
              "moderate",
              "off"
            ],
            "description": "SafeSearch for this answer."
          },
          "read": {
            "type": "object",
            "description": "Optional. How the query was read when it carried operators, exactly as sent to the index.",
            "required": [
              "text"
            ],
            "properties": {
              "text": {
                "type": "string",
                "description": "The words searched, operators removed."
              },
              "site": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "phrases": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "exclude": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "vertical": {
                "type": "string",
                "enum": [
                  "web",
                  "videos",
                  "images"
                ]
              }
            }
          },
          "news": {
            "$ref": "#/components/schemas/News"
          },
          "intent": {
            "type": "object",
            "description": "Optional. What the query text alone says (engines/hottub-search-core intent.rs).",
            "properties": {
              "local": {
                "type": "boolean"
              },
              "question": {
                "type": "boolean"
              }
            }
          },
          "location": {
            "type": "object",
            "description": "Optional. The location given beside the query, when it found places that answer it; `entities` are then in it.",
            "required": [
              "label",
              "source"
            ],
            "properties": {
              "label": {
                "type": "string"
              },
              "source": {
                "type": "string",
                "enum": [
                  "param",
                  "ip"
                ]
              }
            }
          },
          "place": {
            "type": "object",
            "description": "Optional. The place the query names, present only after the Directory found something there.",
            "required": [
              "label"
            ],
            "properties": {
              "label": {
                "type": "string"
              },
              "what": {
                "type": "string"
              },
              "city": {
                "type": "string"
              },
              "region": {
                "type": "string"
              }
            }
          },
          "entities": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Entity"
            }
          },
          "results": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Result"
            }
          },
          "sponsored": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Sponsored"
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "source",
                "status",
                "count"
              ],
              "properties": {
                "source": {
                  "type": "string"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "ok",
                    "unavailable",
                    "timeout",
                    "not_configured",
                    "skipped",
                    "budget_exhausted",
                    "circuit_open"
                  ],
                  "description": "Per-source outcome. Anything but `ok` means that source contributed nothing; it is not a negative answer."
                },
                "count": {
                  "type": "integer"
                }
              }
            }
          },
          "directory": {
            "type": "object",
            "required": [
              "source",
              "status",
              "count"
            ],
            "properties": {
              "source": {
                "const": "directory"
              },
              "status": {
                "type": "string",
                "enum": [
                  "ok",
                  "unavailable",
                  "timeout",
                  "not_configured",
                  "skipped",
                  "budget_exhausted",
                  "circuit_open"
                ],
                "description": "Per-source outcome. Anything but `ok` means that source contributed nothing; it is not a negative answer."
              },
              "count": {
                "type": "integer"
              }
            }
          },
          "coverage": {
            "type": "object",
            "required": [
              "entities"
            ],
            "properties": {
              "entities": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "What the entity index does not cover. Abstain rather than treat absence as a negative."
              }
            }
          },
          "partial": {
            "type": "boolean",
            "description": "True when a source failed or timed out."
          },
          "as_of_unix": {
            "type": "integer"
          }
        }
      }
    }
  }
}
