{
  "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, and 50 a day (UTC) per address across /v1, /mcp and results as JSON or Markdown, after which `429 daily_limit_reached` points to a free key with 2,500 searches a month; 120 a minute 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. With a Search API key (`Authorization: Bearer hsk_…` or `X-API-Key`; keys and prices at https://hottub.tech/developers) limits belong to the key's account and plan (`RateLimit` windows `minute` and `month`), refusals are `401 key_invalid` or `429 rate_limited` / `monthly_limit_reached` / `spend_cap_reached`, and the answer adds `terms` and a `storable` flag per result. Paying never changes the order of results. The same search is an MCP tool (`hottub_search`) at https://hottub.tech/mcp, and every results page is Markdown with `&format=md`.",
    "termsOfService": "https://joinhottub.com/terms"
  },
  "servers": [
    {
      "url": "https://hottub.tech",
      "description": "Hottub Search (canonical)"
    },
    {
      "url": "https://joinhottub.com/api/search",
      "description": "The same API on joinhottub.com"
    }
  ],
  "externalDocs": {
    "url": "https://hottub.tech/agents"
  },
  "security": [
    {},
    {
      "searchKey": []
    },
    {
      "searchKeyHeader": []
    }
  ],
  "paths": {
    "/v1/watches": {
      "get": {
        "operationId": "listWatches",
        "summary": "This key's account's watches",
        "security": [
          {
            "searchApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Watches and the plan limits.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "watches": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Watch"
                      }
                    },
                    "limits": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "No or unknown agent key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatchError"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createWatch",
        "summary": "Watch for something new or changed",
        "description": "A saved search Hottub re-reads on its servers. Identical watches share one read. The first read is a baseline and reports nothing; after that each change is one event, delivered by webhook (if `webhook_id`), on the SSE stream at `/v1/watches/{id}/events`, and as an MCP resource update. Re-checks are free. Sources `recalls`, `bills`, `permits` and `weather_alerts` answer 409 `source_not_yet_available` until their readers ship.",
        "security": [
          {
            "searchApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WatchRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Watch"
                }
              }
            }
          },
          "400": {
            "description": "A field or parameter was refused; `error.code` says which and `error.message` how to fix it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatchError"
                }
              }
            }
          },
          "404": {
            "description": "`webhook_not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatchError"
                }
              }
            }
          },
          "409": {
            "description": "`watch_limit` (the plan's watches: Free 10, Developer 100, Scale 1,000) or `source_not_yet_available`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatchError"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (60 calls a minute per key).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatchError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/watches/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^wat_[0-9a-f]{24}$"
          }
        }
      ],
      "get": {
        "operationId": "getWatch",
        "summary": "One watch and its latest events",
        "security": [
          {
            "searchApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "The watch and up to 20 latest events.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "watch": {
                      "$ref": "#/components/schemas/Watch"
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WatchEventRecord"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "`watch_not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatchError"
                }
              }
            }
          }
        }
      },
      "delete": {
        "operationId": "deleteWatch",
        "summary": "Delete a watch (its events and deliveries go with it)",
        "security": [
          {
            "searchApiKey": []
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted."
          },
          "404": {
            "description": "`watch_not_found`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatchError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/watches/{id}/events": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^wat_[0-9a-f]{24}$"
          }
        },
        {
          "name": "after",
          "in": "query",
          "required": false,
          "schema": {
            "type": "integer",
            "minimum": 0
          },
          "description": "Only events after this sequence number (JSON form). The stream uses Last-Event-ID instead."
        },
        {
          "name": "Last-Event-ID",
          "in": "header",
          "required": false,
          "schema": {
            "type": "string"
          },
          "description": "Resume a stream after this event."
        }
      ],
      "get": {
        "operationId": "watchEvents",
        "summary": "Events: a live SSE stream, or JSON to poll",
        "description": "With `Accept: text/event-stream`, a Server-Sent Events stream: `id` is the event sequence number, `event` the type (`watch.changed`, `watch.source_failing`), `data` the same JSON body a webhook receives. Keep-alive comments every 20 seconds; the server ends a connection after 15 minutes with `retry: 5000`, and a reconnect with Last-Event-ID loses nothing. Otherwise JSON, oldest first, up to 50 per call.",
        "security": [
          {
            "searchApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Events.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              },
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "events": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WatchEventRecord"
                      }
                    },
                    "next_after": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "`stream_limit`: too many live streams for this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatchError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks": {
      "get": {
        "operationId": "listWebhooks",
        "summary": "Webhook endpoints (by host)",
        "security": [
          {
            "searchApiKey": []
          }
        ],
        "responses": {
          "200": {
            "description": "Endpoints.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createWebhook",
        "summary": "Register a webhook endpoint; its signing secret is shown once",
        "description": "Deliveries follow Standard Webhooks: headers `webhook-id` (stable across retries; your idempotency key), `webhook-timestamp` (Unix seconds) and `webhook-signature` (`v1,` + base64 HMAC-SHA256 of `{id}.{timestamp}.{body}` with the secret's bytes). Refuse timestamps more than 5 minutes off. Retries: at once, 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 10 h; then dead. A 410 switches the endpoint off, as do 5 dead deliveries in a row. Only public https:// URLs on port 443, named by a domain.",
        "security": [
          {
            "searchApiKey": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "url"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "maxLength": 512
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 80
                  }
                },
                "additionalProperties": false
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Registered. `secret` (whsec_…) appears only here.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "host": {
                      "type": "string"
                    },
                    "secret": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Not a public https URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatchError"
                }
              }
            }
          },
          "409": {
            "description": "`webhook_limit`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/WatchError"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/{id}": {
      "parameters": [
        {
          "name": "id",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string",
            "pattern": "^whk_[0-9a-f]{24}$"
          }
        }
      ],
      "delete": {
        "operationId": "deleteWebhook",
        "summary": "Remove a webhook endpoint",
        "security": [
          {
            "searchApiKey": []
          }
        ],
        "responses": {
          "204": {
            "description": "Removed."
          }
        }
      }
    },
    "/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",
                "events",
                "videos",
                "news",
                "images",
                "hottub"
              ],
              "default": "all"
            },
            "description": "`all`, `web` and `hottub` as always; `places` (Directory places only), `events` (upcoming local events in `events`, up to 20), `news` (Hottub News headlines in `news`), `videos` and `images` (results that carry a video or a picture, and openly licensed pictures and videos in `media`)."
          },
          {
            "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)."
          },
          {
            "name": "record",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "Typed `record` on web results and entities when the source states one: hours, price, availability, address, phone, event start/end/status, bill status, recall remedy. Each field carries `value`, `source_url`, `fetched_at` and `method` (`markup`, `directory`, `official_record`); nothing is inferred."
          },
          {
            "name": "quotes",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 0,
              "maximum": 3,
              "default": 0
            },
            "description": "Up to this many supporting passages per web result in `evidence.quotes`: exact text, `start`/`end` character offsets into the stored text named by `evidence.content_hash` (`sha256:`), a `url` with a `#:~:text=` fragment, and the `licence`. Publisher pages: 2 quotes of 200 characters (3 of 300 on paid plans)."
          },
          {
            "name": "verify",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "top",
                "all"
              ]
            },
            "description": "Check it now: re-fetch the first result (`top`) or the first five (`all`) live within 800 ms, honouring robots.txt, and report `current`, `changed` (with the changed quote and the page's passage now), `gone` or `unreachable` in `evidence.verify`. Counted per caller; never cached."
          },
          {
            "name": "snapshot",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 512
            },
            "description": "A `snapshot.id` from an earlier answer: the web results are computed again from that index generation and ranking if it is still retained (`snapshot.replayed`), else the answer says why (`snapshot.reason`)."
          },
          {
            "name": "prefer",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated `gov`, `recent`, `local` (needs `region`), `primary`: bounded boosts (each ×1.25, together at most ×1.5) for this request only."
          },
          {
            "name": "sites",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Only results on these sites (comma-separated, at most 10; subdomains included)."
          },
          {
            "name": "exclude_sites",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Never results on these sites (comma-separated, at most 10)."
          },
          {
            "name": "since",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Only pages dated on or after this day (UTC): their own date, else when Hottub fetched them."
          },
          {
            "name": "until",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Only pages dated on or before this day (UTC)."
          },
          {
            "name": "lang",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-z]{2}$"
            },
            "description": "Only pages in this language (ISO 639-1)."
          },
          {
            "name": "region",
            "in": "query",
            "schema": {
              "type": "string",
              "pattern": "^[a-z]{2}$"
            },
            "description": "Lean toward pages from this country (by country domain), ×1.15; ×1.25 with `prefer=local`."
          },
          {
            "name": "format",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "json",
                "md",
                "ndjson",
                "text"
              ],
              "default": "json"
            },
            "description": "The same answer as JSON, compact Markdown, NDJSON (one element a line: `meta`, elements with `type`, `done`) or plain text."
          },
          {
            "name": "fields",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 600
            },
            "description": "Comma-separated sections (`entities`) or `section.field` (`results.url,results.title`). Elements always keep `id`, `kind` and `fb`; paid rows always keep their label, `paid`, who paid and the disclosure."
          },
          {
            "name": "max_tokens",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 200,
              "maximum": 32000
            },
            "description": "About this many tokens (4 bytes each), cut the same way every time: verbose fields, then shorter snippets, fewer cards, minimal rows, compact status, then rows from the longest list. `truncated` and `truncation` say what went. Sponsored rows are measured last and never take room from results (`Hottub-Sponsored-Omitted: 1` when one did not fit)."
          },
          {
            "name": "depth",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "fast",
                "standard",
                "deep"
              ]
            },
            "description": "`fast`: the web index only. `standard`: plus the Directory, events and news where asked (no overviews). `deep`: every source plus the readable text of the first pages. Absent: today's plan."
          },
          {
            "name": "intent",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "find",
                "answer",
                "local",
                "buy",
                "compare",
                "navigate",
                "news",
                "data"
              ]
            },
            "description": "What the person wants; sources that cannot help are not read (`news` reads Hottub News, `navigate`/`compare`/`answer` read the web)."
          },
          {
            "name": "entity",
            "in": "query",
            "schema": {
              "type": "string",
              "maxLength": 128
            },
            "description": "The business, place or thing asked about; may stand in for `q`."
          },
          {
            "name": "types",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated: results, web, hottub, entities, places, events, news, answer, sponsored, house. Only these are returned, and only their sources are read."
          },
          {
            "name": "time",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "day",
                "week",
                "month",
                "year",
                "any"
              ]
            },
            "description": "Dated results and news older than this are left out; undated ones stay."
          },
          {
            "name": "stream",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "Server-sent events: `meta`, each organic `result` as soon as it is composed, then cards (`entity`, `place`, `news`, `event`, `answer`, `sponsored`…) as their sources finish, then `done` with the final `results_order`."
          },
          {
            "name": "sponsored",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "on",
                "off"
              ]
            },
            "description": "Paid plans may pass `off` to leave sponsored rows out. The free plan and keyless calls keep them."
          },
          {
            "name": "fb",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "Also print each element's `fb` token in `md` and `text` (about 16 tokens a row)."
          },
          {
            "name": "commerce",
            "in": "query",
            "schema": {
              "type": "boolean"
            },
            "description": "Commerce links (Search API keys whose account turned them on for the key; keyless calls never get them): results at a merchant with an approved affiliate programme carry a separate `offer` (see Offer). `url`, order and membership never change, offers are added last and only while the answer fits `max_tokens`, and they add nothing to the cost units or the price of the call. A key set to \"always\" gets offers without this parameter. Show each offer's `disclosure` beside its `buy_url`: https://hottub.tech/terms-for-agents."
          }
        ],
        "responses": {
          "200": {
            "description": "A search answer, in the format asked for.",
            "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`."
              },
              "ETag": {
                "schema": {
                  "type": "string"
                },
                "description": "Send it back as `If-None-Match` for a `304`."
              },
              "Hottub-Tokens": {
                "schema": {
                  "type": "integer"
                },
                "description": "About how many tokens the body is."
              },
              "Hottub-Truncated": {
                "schema": {
                  "type": "string"
                },
                "description": "`1` when organic content was cut to `max_tokens`."
              },
              "Hottub-Sponsored-Omitted": {
                "schema": {
                  "type": "string"
                },
                "description": "`1` when a sponsored row did not fit the budget."
              },
              "Hottub-Depth": {
                "schema": {
                  "type": "string"
                },
                "description": "The depth this answer was read at (`auto` when none was asked)."
              },
              "Hottub-Cost-Units": {
                "schema": {
                  "type": "string"
                },
                "description": "Upstream reads this answer made, e.g. `web=1, directory=2`. Never the query."
              },
              "Server-Timing": {
                "schema": {
                  "type": "string"
                },
                "description": "Milliseconds per stage, including `wasm` (the Rust core)."
              },
              "Hottub-Commerce": {
                "schema": {
                  "type": "string"
                },
                "description": "When Commerce links were asked for: `on; offers=2`, or `off; reason=…` (`no_key`, `key_not_enabled`, `not_asked`, `terms_not_accepted`)."
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SearchResponse"
                }
              },
              "text/markdown": {
                "schema": {
                  "type": "string"
                }
              },
              "application/x-ndjson": {
                "schema": {
                  "type": "string"
                }
              },
              "text/plain": {
                "schema": {
                  "type": "string"
                }
              },
              "text/event-stream": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "304": {
            "description": "Not modified (`If-None-Match` matched the `ETag`)."
          },
          "400": {
            "description": "Unknown or malformed parameter (`invalid_request`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "The key is malformed, unknown or turned off (`key_invalid`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "414": {
            "description": "Query string over 2048 bytes (`request_too_large`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited (`rate_limited`), or without a key this address used today's 50 searches (`daily_limit_reached`, until midnight UTC; a free key gives 2,500 a month); 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. With a key, nothing was counted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/feedback": {
      "post": {
        "operationId": "feedback",
        "summary": "Say how one element of an answer worked out (free; never counted as a search)",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "fb",
                  "verdict"
                ],
                "additionalProperties": false,
                "properties": {
                  "fb": {
                    "type": "string",
                    "maxLength": 256,
                    "description": "The element's `fb` token."
                  },
                  "verdict": {
                    "type": "string",
                    "enum": [
                      "used",
                      "helpful",
                      "wrong",
                      "stale",
                      "irrelevant",
                      "spam",
                      "missing_info",
                      "broken_link"
                    ]
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 280,
                    "description": "Optional. Scrubbed of emails, long numbers and link queries; kept only on review queues, never shown to anyone else."
                  },
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Optional: the element's own URL; kept for a recrawl only when it matches the element's id."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "`already_counted`: this caller already sent this verdict on this token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeedbackResult"
                }
              }
            }
          },
          "202": {
            "description": "Counted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeedbackResult"
                }
              }
            }
          },
          "400": {
            "description": "`invalid_request`, `verdict_invalid` or `fb_invalid`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "`feedback_off`: not turned on here yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "`fb_expired`: the token is over 7 days old.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "`rate_limited`: wait `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/doc/{id}": {
      "get": {
        "operationId": "doc",
        "summary": "One element in full (untrimmed), or a Directory entity's whole record",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "An element id from an answer, e.g. `web:1a2b3c4d5e6f7a8b` or `directory:abc`."
          }
        ],
        "responses": {
          "200": {
            "description": "The element.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "schema",
                    "id",
                    "kind",
                    "record"
                  ],
                  "properties": {
                    "schema": {
                      "const": "hottub.search.doc.v1"
                    },
                    "id": {
                      "type": "string"
                    },
                    "kind": {
                      "type": "string"
                    },
                    "source": {
                      "type": "string"
                    },
                    "record": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "`doc_not_found`: no longer held (elements are kept minutes, per server). Search again, or ask with `depth=deep`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/batch": {
      "post": {
        "operationId": "batch",
        "summary": "Up to 10 searches at once, with one token budget",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "queries"
                ],
                "properties": {
                  "queries": {
                    "type": "array",
                    "minItems": 1,
                    "maxItems": 10,
                    "items": {
                      "type": "object",
                      "description": "The /v1 parameters as JSON ({\"q\": \"…\", \"depth\": \"fast\", \"types\": [\"entities\"]})."
                    }
                  },
                  "max_tokens": {
                    "type": "integer",
                    "description": "Shared across the queries, split evenly."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "One answer (or one error) per query, in order. Each query is admitted and counted as one search.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "schema": {
                      "const": "hottub.search.batch.v1"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "answers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "index": {
                            "type": "integer"
                          },
                          "tokens": {
                            "type": "integer"
                          },
                          "answer": {
                            "$ref": "#/components/schemas/SearchResponse"
                          },
                          "error": {
                            "type": "object"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "operationId": "usage",
        "summary": "This month's searches for a key's account (free; never counted)",
        "security": [
          {
            "searchKey": []
          },
          {
            "searchKeyHeader": []
          }
        ],
        "responses": {
          "200": {
            "description": "Plan, searches used, limits and the cost so far this month.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            }
          },
          "401": {
            "description": "`key_invalid`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "searchKey": {
        "type": "http",
        "scheme": "bearer",
        "description": "A Search API key, `hsk_…`. Make one at https://search.joinhottub.com/account/keys."
      },
      "searchKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "X-API-Key"
      },
      "searchApiKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "hsk_…",
        "description": "A Hottub Search API key (https://search.joinhottub.com/account/keys); X-API-Key also works. Watches belong to the key's account; limits follow its plan (Free 10 watches, Developer 100, Scale 1,000). Business agent keys (hbak_) are not accepted."
      }
    },
    "schemas": {
      "WatchError": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code"
            ],
            "properties": {
              "code": {
                "type": "string"
              },
              "message": {
                "type": "string"
              }
            }
          }
        }
      },
      "WatchRequest": {
        "type": "object",
        "required": [
          "source"
        ],
        "additionalProperties": false,
        "properties": {
          "source": {
            "type": "string",
            "enum": [
              "events",
              "directory_place",
              "pages",
              "recalls",
              "bills",
              "permits",
              "weather_alerts"
            ]
          },
          "params": {
            "type": "object",
            "description": "events: {city, region} or {near:\"lat,lon\", radius_m}, or {q:\"events near Spokane, WA\"}; optional category, days. directory_place: {entity_id}. pages: {q, site} or {q, gov_only:true}. recalls: {vin} or {year, make, model}. bills: {congress, type, number}. permits: {city, region, filter_field?, filter_value?, since_days?}. weather_alerts: {near:\"lat,lon\"}. Only the normalized form is kept."
          },
          "label": {
            "type": "string",
            "maxLength": 80
          },
          "every": {
            "type": "string",
            "enum": [
              "5m",
              "15m",
              "1h",
              "6h",
              "12h",
              "1d",
              "7d"
            ],
            "description": "No faster than the source allows (weather 5m, records 1h, events 6h)."
          },
          "filters": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "kinds": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "added",
                    "changed",
                    "removed"
                  ]
                },
                "maxItems": 3
              },
              "changes_in": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "maxItems": 8,
                "description": "Only changes to these fields (e.g. directory_place [\"hours\"])."
              },
              "words": {
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 32
                },
                "maxItems": 3
              },
              "min_severity": {
                "type": "string",
                "enum": [
                  "minor",
                  "moderate",
                  "severe",
                  "extreme"
                ],
                "description": "weather_alerts only."
              }
            }
          },
          "quiet": {
            "type": "object",
            "required": [
              "from",
              "to"
            ],
            "properties": {
              "from": {
                "type": "string",
                "example": "22:00"
              },
              "to": {
                "type": "string",
                "example": "07:00"
              },
              "utc_offset_minutes": {
                "type": "integer"
              },
              "urgent": {
                "type": "boolean",
                "default": true
              }
            },
            "description": "Changes wait, merged, until the quiet hours end; urgent items (severe weather) go anyway unless `urgent` is false."
          },
          "cooldown": {
            "type": "string",
            "example": "1h",
            "description": "At most one delivery per cooldown; changes in between are merged."
          },
          "webhook_id": {
            "type": "string",
            "pattern": "^whk_[0-9a-f]{24}$"
          }
        }
      },
      "Watch": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "params": {
            "type": "object"
          },
          "label": {
            "type": "string"
          },
          "every": {
            "type": "string"
          },
          "filters": {
            "type": "object"
          },
          "status": {
            "type": "string"
          },
          "delivery": {
            "type": "object",
            "properties": {
              "webhook_id": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "events_url": {
                "type": "string"
              },
              "mcp_resource": {
                "type": "string"
              }
            }
          },
          "links": {
            "type": "object"
          }
        }
      },
      "WatchEventRecord": {
        "type": "object",
        "properties": {
          "seq": {
            "type": "integer"
          },
          "id": {
            "type": "string",
            "description": "The webhook-id of this event."
          },
          "type": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "event": {
            "$ref": "#/components/schemas/WatchEvent"
          }
        }
      },
      "WatchEvent": {
        "type": "object",
        "description": "The body every delivery carries (Standard Webhooks payload shape).",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "watch.changed",
              "watch.source_failing"
            ]
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "properties": {
              "watch_id": {
                "type": "string"
              },
              "source": {
                "type": "string"
              },
              "label": {
                "type": "string"
              },
              "summary": {
                "type": "object",
                "properties": {
                  "added": {
                    "type": "integer"
                  },
                  "changed": {
                    "type": "integer"
                  },
                  "removed": {
                    "type": "integer"
                  }
                }
              },
              "added": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              },
              "changed": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              },
              "removed": {
                "type": "array",
                "items": {
                  "type": "object"
                }
              },
              "truncated": {
                "type": "integer"
              },
              "links": {
                "type": "object"
              }
            }
          }
        }
      },
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "object",
            "required": [
              "code"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "invalid_request",
                  "request_too_large",
                  "rate_limited",
                  "daily_limit_reached",
                  "search_unavailable",
                  "key_invalid",
                  "monthly_limit_reached",
                  "spend_cap_reached",
                  "invalid_format",
                  "invalid_fields",
                  "invalid_max_tokens",
                  "invalid_depth",
                  "invalid_intent",
                  "invalid_types",
                  "invalid_time",
                  "invalid_entity",
                  "invalid_stream",
                  "invalid_flag",
                  "option_unavailable",
                  "fb_invalid",
                  "fb_expired",
                  "verdict_invalid",
                  "feedback_off",
                  "feedback_unavailable",
                  "doc_not_found",
                  "invalid_id",
                  "invalid_batch",
                  "invalid_record",
                  "invalid_quotes",
                  "invalid_verify",
                  "invalid_snapshot",
                  "snapshot_signature",
                  "snapshot_not_configured",
                  "invalid_prefer",
                  "invalid_sites",
                  "invalid_exclude_sites",
                  "invalid_since",
                  "invalid_until",
                  "invalid_lang",
                  "invalid_region"
                ]
              },
              "message": {
                "type": "string",
                "description": "With a key: the same refusal in one plain sentence. Do not branch on it."
              },
              "retry_after_seconds": {
                "type": "integer"
              },
              "plan": {
                "type": "string",
                "enum": [
                  "free",
                  "developer",
                  "scale"
                ]
              },
              "docs": {
                "type": "string",
                "format": "uri"
              }
            }
          }
        }
      },
      "ElementFields": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable and namespaced: `web:<16 hex>` (first 8 bytes of SHA-256 of the URL), `hottub:…`, `directory:<entity>`, `news:…`, `sponsored:…`. Entities, events and the answer card keep the id their source gave them."
          },
          "kind": {
            "type": "string",
            "enum": [
              "web",
              "hottub",
              "entity",
              "place",
              "answer",
              "correction",
              "suggestion",
              "news",
              "event",
              "video",
              "image",
              "intent_word",
              "directory_placement",
              "house",
              "site"
            ]
          },
          "source": {
            "description": "Where it came from (`web_index`, `brave`, `hottub`, `hottub-directory`, `hottub_news`, `hottub_business`…); events keep `{kind, host}`."
          },
          "fetched_at": {
            "type": "integer",
            "description": "Unix seconds when the source last saw it (crawl, Directory observation, publication), else the answer's time."
          },
          "license": {
            "type": "string",
            "description": "Only where a third party's terms name one and no `provenance` says it."
          },
          "attribution": {
            "type": "string",
            "description": "Only where a third party asks for credit (e.g. \"Brave Search\", \"From Hottub News\")."
          },
          "fb": {
            "type": "string",
            "description": "Feedback token for POST /v1/feedback (when feedback is on). Names the element, its kind, position and the hour it was served; never the query or the caller. Valid 7 days."
          }
        }
      },
      "Offer": {
        "type": "object",
        "description": "Commerce links (opt-in, keys only): a paid buy link beside a result at a merchant with an approved affiliate programme. Separate from `url`, which stays the clean page address; never present when the page link already belongs to another affiliate programme, for Amazon without Amazon's approval, or for Google-derived results. Show `disclosure` beside the link (in HTML, `rel=\"sponsored nofollow\"`); do not change, re-wrap or prefetch the link. Markdown prints it under the result as `Buy: [Merchant](buy_url) (Paid link: …)`; NDJSON carries it on the result line.",
        "required": [
          "buy_url",
          "merchant",
          "network",
          "paid_link",
          "disclosure",
          "revenue_share"
        ],
        "properties": {
          "buy_url": {
            "type": "string",
            "format": "uri",
            "description": "The affiliate network's own link (never through Hottub). Carries only the network's tracking and an opaque per-key, per-day sub-ID."
          },
          "merchant": {
            "type": "string"
          },
          "network": {
            "type": "string",
            "description": "`impact`, `cj`, `rakuten`, `awin`, `shareasale`, `partnerize`, `amazon_associates`, `direct` …"
          },
          "paid_link": {
            "type": "boolean",
            "const": true
          },
          "disclosure": {
            "type": "string",
            "description": "\"Paid link: Hottub may earn a commission.\" Show it beside the link, every time."
          },
          "revenue_share": {
            "type": "boolean",
            "description": "Whether a sale through this link credits your account a share of the commission."
          },
          "price": {
            "type": "object",
            "description": "Optional: the page's own stated price.",
            "properties": {
              "amount": {
                "type": "string"
              },
              "currency": {
                "type": "string"
              }
            }
          },
          "availability": {
            "type": "string",
            "description": "Optional: the page's own stated availability (`in_stock` …)."
          }
        }
      },
      "FeedbackResult": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "status": {
            "type": "integer"
          },
          "code": {
            "type": "string",
            "enum": [
              "counted",
              "already_counted"
            ]
          },
          "message": {
            "type": "string"
          },
          "element": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "kind": {
                "type": "string"
              },
              "position": {
                "type": "integer"
              },
              "surface": {
                "type": "string",
                "enum": [
                  "page",
                  "agent"
                ]
              }
            }
          },
          "verdict": {
            "type": "string"
          },
          "routes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "ranking",
                "recrawl",
                "quality_label",
                "directory_correction",
                "scoreboard",
                "card_threshold",
                "advertiser_quality",
                "house_review",
                "answer_review"
              ]
            },
            "description": "Where the verdict went."
          }
        }
      },
      "Terms": {
        "type": "object",
        "description": "Only on answers to a key: what you may do with the results.",
        "properties": {
          "version": {
            "type": "string"
          },
          "plan": {
            "type": "string"
          },
          "store": {
            "type": "string"
          },
          "not_storable": {
            "type": "string"
          },
          "places": {
            "type": "string"
          },
          "attribution_required": {
            "type": "boolean"
          },
          "attribution": {
            "type": "object",
            "properties": {
              "text": {
                "type": "string"
              },
              "url": {
                "type": "string",
                "format": "uri"
              }
            }
          },
          "ranking": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          }
        }
      },
      "Usage": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "description": "The key's hint (`hsk_{id}_…`), never the key."
          },
          "card": {
            "type": "string"
          },
          "plan": {
            "type": "string"
          },
          "plan_name": {
            "type": "string"
          },
          "chosen_plan": {
            "type": "string"
          },
          "billing_needed": {
            "type": "boolean"
          },
          "month": {
            "type": "string"
          },
          "calls": {
            "type": "integer"
          },
          "free_calls": {
            "type": "integer"
          },
          "free_left": {
            "type": "integer"
          },
          "limit": {
            "type": "integer"
          },
          "left": {
            "type": "integer"
          },
          "per_minute": {
            "type": "integer"
          },
          "spend_cap_cents": {
            "type": [
              "integer",
              "null"
            ]
          },
          "estimated_cents": {
            "type": "integer"
          },
          "resets_in_seconds": {
            "type": "integer"
          },
          "attribution_required": {
            "type": "boolean"
          }
        }
      },
      "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"
          },
          "kind": {
            "type": "string",
            "enum": [
              "web",
              "hottub",
              "entity",
              "place",
              "answer",
              "correction",
              "suggestion",
              "news",
              "event",
              "video",
              "image",
              "intent_word",
              "directory_placement",
              "house",
              "site"
            ]
          },
          "source": {
            "description": "Where it came from (`web_index`, `brave`, `hottub`, `hottub-directory`, `hottub_news`, `hottub_business`…); events keep `{kind, host}`."
          },
          "fetched_at": {
            "type": "integer",
            "description": "Unix seconds when the source last saw it (crawl, Directory observation, publication), else the answer's time."
          },
          "license": {
            "type": "string",
            "description": "Only where a third party's terms name one and no `provenance` says it."
          },
          "attribution": {
            "type": "string",
            "description": "Only where a third party asks for credit (e.g. \"Brave Search\", \"From Hottub News\")."
          },
          "fb": {
            "type": "string",
            "description": "Feedback token for POST /v1/feedback (when feedback is on). Names the element, its kind, position and the hour it was served; never the query or the caller. Valid 7 days."
          },
          "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": {
          "id": {
            "type": "string",
            "description": "Stable and namespaced: `web:<16 hex>` (first 8 bytes of SHA-256 of the URL), `hottub:…`, `directory:<entity>`, `news:…`, `sponsored:…`. Entities, events and the answer card keep the id their source gave them."
          },
          "kind": {
            "type": "string",
            "enum": [
              "hottub",
              "web"
            ]
          },
          "source": {
            "description": "Where it came from (`web_index`, `brave`, `hottub`, `hottub-directory`, `hottub_news`, `hottub_business`…); events keep `{kind, host}`."
          },
          "fetched_at": {
            "type": "integer",
            "description": "Unix seconds when the source last saw it (crawl, Directory observation, publication), else the answer's time."
          },
          "license": {
            "type": "string",
            "description": "Only where a third party's terms name one and no `provenance` says it."
          },
          "attribution": {
            "type": "string",
            "description": "Only where a third party asks for credit (e.g. \"Brave Search\", \"From Hottub News\")."
          },
          "fb": {
            "type": "string",
            "description": "Feedback token for POST /v1/feedback (when feedback is on). Names the element, its kind, position and the hour it was served; never the query or the caller. Valid 7 days."
          },
          "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."
          },
          "storable": {
            "type": "boolean",
            "description": "Only with a key: whether you may keep this result (false for rows another search provider supplied)."
          },
          "offer": {
            "$ref": "#/components/schemas/Offer"
          },
          "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"
              }
            }
          }
        }
      },
      "Media": {
        "type": "object",
        "description": "Present on the images and videos tabs when openly licensed sources are on: pictures (Smithsonian Open Access CC0, NASA, Openverse CC0 / Public Domain Mark / CC BY / CC BY-SA) or NASA videos, in one organic order that payment never changes. NonCommercial and NoDerivatives works are never included. When reusing a work, copy its `attribution`: CC BY and CC BY-SA require the creator, a link to the work and a link to the licence. NASA asks for credit and that no endorsement be implied.",
        "required": [
          "kind",
          "items",
          "sources"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "image",
              "video"
            ]
          },
          "credit": {
            "type": "string"
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "source",
                "name",
                "url",
                "status",
                "count"
              ],
              "properties": {
                "source": {
                  "type": "string",
                  "enum": [
                    "smithsonian",
                    "nasa",
                    "openverse"
                  ]
                },
                "name": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                },
                "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"
                }
              }
            }
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "kind",
                "title",
                "url",
                "thumb",
                "source",
                "source_name",
                "license",
                "attribution"
              ],
              "properties": {
                "kind": {
                  "type": "string",
                  "enum": [
                    "image",
                    "video"
                  ]
                },
                "title": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri",
                  "description": "The original page: the work, its creator and its licence."
                },
                "thumb": {
                  "type": "string",
                  "format": "uri",
                  "description": "Hottub's own copy of the thumbnail."
                },
                "width": {
                  "type": "integer"
                },
                "height": {
                  "type": "integer"
                },
                "duration_s": {
                  "type": "integer"
                },
                "creator": {
                  "type": "string"
                },
                "source": {
                  "type": "string",
                  "enum": [
                    "smithsonian",
                    "nasa",
                    "openverse"
                  ]
                },
                "source_name": {
                  "type": "string",
                  "description": "\"Flickr via Openverse\", \"NASA JPL\", \"Smithsonian National Museum of American History\"."
                },
                "license": {
                  "type": "object",
                  "required": [
                    "code",
                    "name",
                    "badge",
                    "url",
                    "attribution_required"
                  ],
                  "properties": {
                    "code": {
                      "type": "string",
                      "enum": [
                        "cc0",
                        "pdm",
                        "by",
                        "by-sa",
                        "nasa"
                      ]
                    },
                    "version": {
                      "type": "string"
                    },
                    "name": {
                      "type": "string"
                    },
                    "badge": {
                      "type": "string"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri"
                    },
                    "attribution_required": {
                      "type": "boolean"
                    },
                    "share_alike": {
                      "type": "boolean"
                    }
                  }
                },
                "attribution": {
                  "type": "string",
                  "description": "The line to copy when reusing the work, exactly as its licence asks."
                },
                "date": {
                  "type": "string",
                  "format": "date"
                }
              }
            }
          }
        }
      },
      "Conditions": {
        "type": "object",
        "description": "Present for a weather ask (\"weather in spokane\", \"spokane weather\", \"weather\" with `location`; kind `weather`) and with a bare place's overview (\"spokane wa\"; kind `place`): live conditions in °F and °C beside plain words. Sources: National Weather Service forecasts and alerts (public domain), AirNow observations (EPA; preliminary, not yet verified), USGS earthquakes and river gauges, Transitland agencies, Census ACS 5-year facts. Credit each source in `credits` as named, with its link and note. A source that did not answer is absent (its row in `sources` says why), never guessed.",
        "required": [
          "kind",
          "label",
          "name",
          "point",
          "forecast_url",
          "credits",
          "sources"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "weather",
              "place"
            ]
          },
          "label": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "point": {
            "type": "object",
            "properties": {
              "lat": {
                "type": "number"
              },
              "lon": {
                "type": "number"
              }
            }
          },
          "summary": {
            "type": "string",
            "description": "One plain line: \"68°F and clear. Tonight: partly cloudy, low 46°F.\""
          },
          "now": {
            "type": "object",
            "description": "The current hour of the hourly forecast."
          },
          "periods": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "The next named forecast periods (\"Tonight\", \"Saturday\")."
          },
          "days": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Weather only: later days, high and low."
          },
          "alerts": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Active National Weather Service alerts, most severe first; `severity_words` in plain words."
          },
          "air": {
            "type": "object",
            "description": "AirNow's worst current pollutant: AQI, the EPA category name and colour, advice. Preliminary."
          },
          "river": {
            "type": "object",
            "description": "Place only: the nearest USGS river gauge, water level and flow over six hours; `notable` when rising fast or under a flood alert."
          },
          "quakes": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Place only: magnitude 3 or more within 100 km in the last 7 days."
          },
          "transit": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Place only: transit agencies serving near the city (Transitland)."
          },
          "facts": {
            "type": "object",
            "description": "Place only: median age and households (Census ACS 5-year)."
          },
          "forecast_url": {
            "type": "string",
            "format": "uri"
          },
          "credits": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                },
                "note": {
                  "type": "string"
                }
              }
            }
          },
          "sources": {
            "type": "array",
            "items": {
              "type": "object",
              "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."
                }
              }
            }
          }
        }
      },
      "Knowledge": {
        "type": "object",
        "description": "Present when the query asks a public record something: a Wikipedia topic panel (\"albert einstein\"), vehicle recalls (\"2019 honda civic recall\", or a VIN), consumer product recalls, a bill by number (\"hr 1\"), Federal Register rules and notices, open comment periods, FEC campaign finance, a book or research papers. Organic facts from the record itself, never paid, and never a reason to reorder `results`. Show `source` (and its `license` where given: Wikipedia text is CC BY-SA 4.0, credit and link it; a Commons `image` carries its own `author` and `license`) and `as_of`. Each record field is accompanied by plain display lines (`lead`, `text`, `facts`, and per item `head`, `meta`, `warn`, `text`, `fix`, `act`). Absent when no record answered in time.",
        "required": [
          "kind",
          "title",
          "source"
        ],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "topic",
              "vehicle_recalls",
              "product_recalls",
              "bill",
              "rules",
              "comment_periods",
              "campaign_finance",
              "book",
              "papers"
            ]
          },
          "title": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "format": "uri"
          },
          "source": {
            "type": "object",
            "required": [
              "name",
              "url"
            ],
            "properties": {
              "name": {
                "type": "string"
              },
              "url": {
                "type": "string",
                "format": "uri"
              },
              "license": {
                "type": "string"
              },
              "license_url": {
                "type": "string",
                "format": "uri"
              },
              "note": {
                "type": "string"
              }
            }
          },
          "as_of": {
            "type": "object",
            "properties": {
              "label": {
                "type": "string"
              },
              "date": {
                "type": "string",
                "format": "date"
              }
            }
          },
          "facts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "label": {
                  "type": "string"
                },
                "value": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          },
          "image": {
            "type": "object",
            "description": "A topic picture from Wikimedia Commons under a free licence.",
            "properties": {
              "url": {
                "type": "string",
                "format": "uri"
              },
              "page": {
                "type": "string",
                "format": "uri"
              },
              "author": {
                "type": "string"
              },
              "license": {
                "type": "string"
              },
              "license_url": {
                "type": "string",
                "format": "uri"
              },
              "width": {
                "type": "integer"
              },
              "height": {
                "type": "integer"
              }
            }
          }
        },
        "additionalProperties": true
      },
      "LocalPlace": {
        "type": "object",
        "required": [
          "label",
          "city",
          "region"
        ],
        "properties": {
          "label": {
            "type": "string",
            "description": "\"Spokane, WA\"."
          },
          "city": {
            "type": "string",
            "description": "Lowercase, as the Census writes it."
          },
          "region": {
            "type": "string",
            "description": "Two-letter state code."
          },
          "from_location": {
            "type": "boolean",
            "description": "The place is the `location` given, not one the query named."
          }
        }
      },
      "Stations": {
        "type": "object",
        "description": "Present on the all tab when the query asks for radio (\"kexp\", \"kexp 90.3\", \"listen to kexp\", \"npr spokane\", \"jazz radio seattle\", \"radio stations near me\" with `location`): live stations from Hottub FM (hottub.fm), each once, in Hottub FM's order (most listened first). Nothing plays by itself: `page` is the station's Hottub FM page with its Listen button. No payment changes the order. A `status` other than `ok` means Hottub FM did not answer in time, not that there are no stations.",
        "required": [
          "source",
          "status",
          "label",
          "count",
          "items",
          "more"
        ],
        "properties": {
          "source": {
            "const": "hottub_fm"
          },
          "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."
          },
          "label": {
            "type": "string",
            "description": "For people: \"KEXP on Hottub FM\", \"Public radio in Spokane, WA\"."
          },
          "place": {
            "$ref": "#/components/schemas/LocalPlace"
          },
          "count": {
            "type": "integer"
          },
          "more": {
            "type": "string",
            "format": "uri",
            "description": "Every station for these words, on hottub.fm."
          },
          "items": {
            "type": "array",
            "maxItems": 4,
            "items": {
              "type": "object",
              "required": [
                "name",
                "country_code",
                "page"
              ],
              "properties": {
                "name": {
                  "type": "string"
                },
                "call_sign": {
                  "type": "string",
                  "description": "\"KEXP\", when the listing says it."
                },
                "frequency": {
                  "type": "string",
                  "description": "\"90.3 FM\", \"700 AM\", when the listing says it."
                },
                "place": {
                  "type": "string",
                  "description": "\"Seattle, WA\", a state, or a country."
                },
                "country_code": {
                  "type": "string"
                },
                "page": {
                  "type": "string",
                  "format": "uri",
                  "description": "The station's hottub.fm page: where to listen."
                },
                "website": {
                  "type": "string",
                  "format": "uri"
                },
                "stream_url": {
                  "type": "string",
                  "format": "uri",
                  "description": "The station's own public HTTPS stream, for a player the user starts."
                }
              }
            }
          }
        }
      },
      "Groups": {
        "type": "object",
        "description": "Present on the all tab when the query asks for a group (\"hiking groups spokane\", \"book club near me\" with `location`, \"nonprofits in boise\"): public Hottub groups (Hottubs and subTubs staff approved for discovery) and public organizations on joinhottub.com, exactly what a signed-out visitor sees, groups first, each in the community plane's order. No member is ever named. A `status` other than `ok` means the read did not answer, not that no group exists.",
        "required": [
          "source",
          "status",
          "label",
          "count",
          "items"
        ],
        "properties": {
          "source": {
            "const": "hottub_groups"
          },
          "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."
          },
          "label": {
            "type": "string",
            "description": "For people: \"Hiking groups in Spokane, WA\"."
          },
          "place": {
            "$ref": "#/components/schemas/LocalPlace"
          },
          "count": {
            "type": "integer"
          },
          "items": {
            "type": "array",
            "maxItems": 5,
            "items": {
              "type": "object",
              "required": [
                "kind",
                "name",
                "url"
              ],
              "properties": {
                "kind": {
                  "type": "string",
                  "enum": [
                    "group",
                    "organization"
                  ]
                },
                "name": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri",
                  "description": "Its public page on joinhottub.com; joining happens there, by a person."
                },
                "place": {
                  "type": "string"
                },
                "about": {
                  "type": "string"
                },
                "category": {
                  "type": "string"
                },
                "members": {
                  "type": "integer",
                  "description": "How many people belong (groups only)."
                },
                "part_of": {
                  "type": "string",
                  "description": "The Hottub a subTub belongs to."
                },
                "verified": {
                  "type": "boolean",
                  "description": "A Hottub-verified organization."
                }
              }
            }
          }
        }
      },
      "Events": {
        "type": "object",
        "description": "Present when the query asks for local events (\"newport wa events\", \"things to do in boise this weekend\", \"concerts near me\" with `location`) and on the events tab: upcoming events from the Hottub Directory (city, library and park calendars, the National Park Service, TicketSea and businesses), in start-time order, then distance. A ticket link or payment never changes the order. A `status` other than `ok` means the read did not answer, not that there are no events.",
        "required": [
          "source",
          "status",
          "label",
          "place",
          "window",
          "count",
          "items"
        ],
        "properties": {
          "source": {
            "const": "directory_events"
          },
          "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."
          },
          "label": {
            "type": "string",
            "description": "For people: \"Upcoming in Newport, WA\", \"This weekend in Boise, ID\"."
          },
          "place": {
            "type": "object",
            "required": [
              "label"
            ],
            "properties": {
              "label": {
                "type": "string"
              },
              "city": {
                "type": "string"
              },
              "region": {
                "type": "string"
              },
              "postcode": {
                "type": "string"
              },
              "from_location": {
                "type": "boolean"
              }
            }
          },
          "window": {
            "type": "object",
            "required": [
              "name",
              "label",
              "days"
            ],
            "properties": {
              "name": {
                "type": "string",
                "enum": [
                  "upcoming",
                  "today",
                  "tonight",
                  "tomorrow",
                  "this_weekend",
                  "this_week",
                  "next_week",
                  "this_month"
                ]
              },
              "label": {
                "type": "string"
              },
              "from": {
                "type": "string",
                "format": "date",
                "description": "First day, in the place's own time."
              },
              "days": {
                "type": "integer"
              }
            }
          },
          "count": {
            "type": "integer"
          },
          "more_q": {
            "type": "string",
            "description": "On the all tab: the query that lists every event with `scope=events`."
          },
          "items": {
            "type": "array",
            "items": {
              "type": "object",
              "required": [
                "id",
                "title",
                "start",
                "status",
                "attendance",
                "venue",
                "url",
                "source"
              ],
              "properties": {
                "id": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "start": {
                  "$ref": "#/components/schemas/EventTime"
                },
                "end": {
                  "$ref": "#/components/schemas/EventTime"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "scheduled",
                    "cancelled",
                    "postponed",
                    "rescheduled"
                  ]
                },
                "attendance": {
                  "type": "string",
                  "enum": [
                    "in_person",
                    "online",
                    "mixed"
                  ]
                },
                "venue": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": "string"
                    },
                    "street": {
                      "type": "string"
                    },
                    "city": {
                      "type": "string"
                    },
                    "region": {
                      "type": "string"
                    },
                    "postcode": {
                      "type": "string"
                    },
                    "lat": {
                      "type": "number"
                    },
                    "lon": {
                      "type": "number"
                    }
                  }
                },
                "distance_m": {
                  "type": "integer"
                },
                "url": {
                  "type": "string",
                  "format": "uri",
                  "description": "The source's own page for the event."
                },
                "tickets": {
                  "type": "string",
                  "format": "uri",
                  "description": "Where to buy tickets or register, when the source names one."
                },
                "free": {
                  "type": "boolean"
                },
                "summary": {
                  "type": "string"
                },
                "organizer": {
                  "type": "string"
                },
                "category": {
                  "type": "string"
                },
                "source": {
                  "type": "object",
                  "properties": {
                    "kind": {
                      "type": "string",
                      "enum": [
                        "page",
                        "calendar",
                        "nps",
                        "ticketsea",
                        "business"
                      ]
                    },
                    "host": {
                      "type": "string"
                    }
                  }
                },
                "entity_id": {
                  "type": "string"
                },
                "page_url": {
                  "type": "string",
                  "format": "uri",
                  "description": "The Hottub page of the place the event belongs to."
                }
              }
            }
          }
        }
      },
      "EventTime": {
        "type": "object",
        "required": [
          "date"
        ],
        "description": "Local date and time where the event is; `utc` (Unix seconds) when the zone is known.",
        "properties": {
          "date": {
            "type": "string",
            "format": "date"
          },
          "time": {
            "type": "string",
            "description": "HH:MM; absent for all day."
          },
          "utc": {
            "type": "integer"
          },
          "zone": {
            "type": "string"
          }
        }
      },
      "OpenData": {
        "type": "object",
        "description": "Present on the all tab when the query names a place with an official open data site and a civic topic (\"seattle building permits\", \"chicago crime data\", \"nyc restaurant inspections\"; \"311 requests\" with `location`): up to three of that government's own datasets, from its Socrata (Tyler Data & Insights) sites only, never paid. Each has its page, its API (JSON rows; SoQL `$select`, `$where`, `$order`, `$limit`) and the API's documentation. `rows` appears when it was counted in time. A `status` other than `ok` means the read did not answer, not that there is no data.",
        "required": [
          "source",
          "status",
          "label",
          "place",
          "topic",
          "sites",
          "count",
          "datasets",
          "more_url",
          "attribution"
        ],
        "properties": {
          "source": {
            "const": "socrata"
          },
          "status": {
            "type": "string"
          },
          "label": {
            "type": "string",
            "description": "\"Open data from Seattle, WA\"."
          },
          "place": {
            "type": "object",
            "properties": {
              "label": {
                "type": "string"
              },
              "kind": {
                "type": "string",
                "enum": [
                  "city",
                  "county",
                  "state"
                ]
              },
              "region": {
                "type": "string"
              },
              "from_location": {
                "type": "boolean"
              }
            }
          },
          "topic": {
            "type": "string",
            "description": "The words searched on the government's sites; empty for its data at large."
          },
          "sites": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "domain": {
                  "type": "string"
                },
                "government": {
                  "type": "string"
                }
              }
            }
          },
          "count": {
            "type": "integer"
          },
          "datasets": {
            "type": "array",
            "maxItems": 3,
            "items": {
              "type": "object",
              "required": [
                "id",
                "name",
                "agency",
                "domain",
                "page_url",
                "api_url",
                "docs_url"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "description": "The dataset's four-by-four id."
                },
                "name": {
                  "type": "string"
                },
                "agency": {
                  "type": "string",
                  "description": "Who publishes it."
                },
                "domain": {
                  "type": "string"
                },
                "category": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                },
                "license": {
                  "type": "string",
                  "description": "As the dataset states it."
                },
                "updated": {
                  "type": "string",
                  "format": "date"
                },
                "updated_label": {
                  "type": "string"
                },
                "rows": {
                  "type": "integer"
                },
                "rows_label": {
                  "type": "string"
                },
                "page_url": {
                  "type": "string",
                  "format": "uri"
                },
                "api_url": {
                  "type": "string",
                  "format": "uri"
                },
                "docs_url": {
                  "type": "string",
                  "format": "uri"
                }
              }
            }
          },
          "more_url": {
            "type": "string",
            "format": "uri",
            "description": "The same search on the government's own site."
          },
          "attribution": {
            "type": "string"
          }
        }
      },
      "Sponsored": {
        "oneOf": [
          {
            "type": "object",
            "required": [
              "kind",
              "label",
              "disclosure",
              "entity"
            ],
            "properties": {
              "id": {
                "type": "string",
                "description": "Stable and namespaced: `web:<16 hex>` (first 8 bytes of SHA-256 of the URL), `hottub:…`, `directory:<entity>`, `news:…`, `sponsored:…`. Entities, events and the answer card keep the id their source gave them."
              },
              "kind": {
                "const": "directory_placement"
              },
              "source": {
                "description": "Where it came from (`web_index`, `brave`, `hottub`, `hottub-directory`, `hottub_news`, `hottub_business`…); events keep `{kind, host}`."
              },
              "fetched_at": {
                "type": "integer",
                "description": "Unix seconds when the source last saw it (crawl, Directory observation, publication), else the answer's time."
              },
              "license": {
                "type": "string",
                "description": "Only where a third party's terms name one and no `provenance` says it."
              },
              "attribution": {
                "type": "string",
                "description": "Only where a third party asks for credit (e.g. \"Brave Search\", \"From Hottub News\")."
              },
              "fb": {
                "type": "string",
                "description": "Feedback token for POST /v1/feedback (when feedback is on). Names the element, its kind, position and the hour it was served; never the query or the caller. Valid 7 days."
              },
              "paid": {
                "const": true
              },
              "label": {
                "const": "Sponsored"
              },
              "disclosure": {
                "type": "string"
              },
              "entity": {
                "$ref": "#/components/schemas/Entity"
              }
            }
          },
          {
            "type": "object",
            "required": [
              "kind",
              "label",
              "disclosure",
              "advertiser_name",
              "url"
            ],
            "properties": {
              "id": {
                "type": "string",
                "description": "Stable and namespaced: `web:<16 hex>` (first 8 bytes of SHA-256 of the URL), `hottub:…`, `directory:<entity>`, `news:…`, `sponsored:…`. Entities, events and the answer card keep the id their source gave them."
              },
              "kind": {
                "const": "intent_word"
              },
              "source": {
                "description": "Where it came from (`web_index`, `brave`, `hottub`, `hottub-directory`, `hottub_news`, `hottub_business`…); events keep `{kind, host}`."
              },
              "fetched_at": {
                "type": "integer",
                "description": "Unix seconds when the source last saw it (crawl, Directory observation, publication), else the answer's time."
              },
              "license": {
                "type": "string",
                "description": "Only where a third party's terms name one and no `provenance` says it."
              },
              "attribution": {
                "type": "string",
                "description": "Only where a third party asks for credit (e.g. \"Brave Search\", \"From Hottub News\")."
              },
              "fb": {
                "type": "string",
                "description": "Feedback token for POST /v1/feedback (when feedback is on). Names the element, its kind, position and the hour it was served; never the query or the caller. Valid 7 days."
              },
              "label": {
                "const": "Sponsored"
              },
              "paid": {
                "const": true
              },
              "disclosure": {
                "type": "string"
              },
              "advertiser_name": {
                "type": "string",
                "description": "Who paid."
              },
              "url": {
                "type": "string",
                "format": "uri",
                "description": "For Search API answers, a click link (/go/routing-word/…): the advertiser is billed only when a person follows it, never for a prefetch."
              },
              "display_url": {
                "type": "string"
              },
              "routing_word": {
                "type": "string",
                "description": "The advertiser's bid phrase this row answers (never your query)."
              },
              "bid_id": {
                "type": "string"
              },
              "billing": {
                "const": "per_click"
              },
              "event_id": {
                "type": "string",
                "description": "Signed-in principals only (per-view slot)."
              }
            }
          }
        ]
      },
      "House": {
        "type": "object",
        "description": "\"From Hottub\": up to two of Hottub's own products, present only when the query asks for that kind of product (for example \"crm\" or \"api\"). Never on the news tab, never beside operators or a query for another company by name. It is not paid, it is never one of `results`, and the results are ordered exactly as they would be without it.",
        "required": [
          "kind",
          "label",
          "products"
        ],
        "properties": {
          "kind": {
            "const": "house"
          },
          "label": {
            "const": "From Hottub"
          },
          "products": {
            "type": "array",
            "maxItems": 2,
            "items": {
              "type": "object",
              "required": [
                "id",
                "name",
                "url",
                "line"
              ],
              "properties": {
                "id": {
                  "type": "string",
                  "description": "Stable product id, e.g. `hottub_crm`."
                },
                "name": {
                  "type": "string"
                },
                "url": {
                  "type": "string",
                  "format": "uri",
                  "description": "The product's own public page."
                },
                "line": {
                  "type": "string",
                  "description": "What it does, in one plain line."
                },
                "openapi": {
                  "type": "string",
                  "format": "uri",
                  "description": "Optional. For an API: its OpenAPI document."
                },
                "mcp": {
                  "type": "string",
                  "format": "uri",
                  "description": "Optional. For an API: its MCP endpoint (Streamable HTTP)."
                }
              }
            }
          },
          "fb": {
            "type": "string",
            "description": "Optional. A feedback token for this element."
          }
        }
      },
      "SearchResponse": {
        "type": "object",
        "required": [
          "schema",
          "version",
          "query",
          "scope",
          "entities",
          "results",
          "sponsored",
          "sources",
          "directory",
          "coverage",
          "partial",
          "as_of_unix"
        ],
        "description": "Every element (results, entities, place and its suggestions, the answer card, a spelling correction, news and event items, `house` rows, sponsored rows) also carries the element fields: `id`, `kind`, `source`, `fetched_at`, `license`/`attribution` where relevant, and `fb` when feedback is on (see ElementFields).",
        "properties": {
          "schema": {
            "const": "hottub.search.v1"
          },
          "version": {
            "const": 1
          },
          "elements_revision": {
            "type": "string",
            "description": "The element-fields revision (`2026-10-03`). Additive: v1 clients keep working."
          },
          "truncated": {
            "type": "boolean",
            "description": "Present when `max_tokens` was asked: whether organic content was cut."
          },
          "truncation": {
            "type": "object",
            "description": "What was cut: `budget_tokens`, `steps`, `dropped` (counts by list)."
          },
          "query": {
            "type": "string"
          },
          "scope": {
            "type": "string",
            "enum": [
              "all",
              "web",
              "places",
              "events",
              "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"
                ]
              }
            }
          },
          "knowledge": {
            "$ref": "#/components/schemas/Knowledge"
          },
          "news": {
            "$ref": "#/components/schemas/News"
          },
          "events": {
            "$ref": "#/components/schemas/Events"
          },
          "media": {
            "$ref": "#/components/schemas/Media"
          },
          "conditions": {
            "$ref": "#/components/schemas/Conditions"
          },
          "open_data": {
            "$ref": "#/components/schemas/OpenData"
          },
          "stations": {
            "$ref": "#/components/schemas/Stations"
          },
          "groups": {
            "$ref": "#/components/schemas/Groups"
          },
          "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"
              }
            }
          },
          "house": {
            "$ref": "#/components/schemas/House"
          },
          "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"
          },
          "terms": {
            "$ref": "#/components/schemas/Terms"
          }
        }
      }
    }
  }
}
