{
  "openapi": "3.1.0",
  "info": {
    "title": "unlob Search API",
    "version": "1.0.0",
    "summary": "The agent-first web search API",
    "description": "Web search built for agents, on our own index. Metadata-only hits, a queryable coverage graph, and an MCP server — from $0.40 per 1,000 queries.\n\nSearch returns metadata-only passage hits — url, host, title, snippet, score and\nworld-model signals — never the page body. Fetch the text of a single passage with\n/doc/{id}. Six further endpoints traverse the coverage graph, and /describe publishes\nthe complete query vocabulary without authentication so an agent can discover the\nquery surface rather than hardcoding it.",
    "contact": {
      "name": "unlob",
      "email": "hello@unlob.com",
      "url": "https://www.unlob.com"
    },
    "termsOfService": "https://www.unlob.com/legal/terms"
  },
  "servers": [
    {
      "url": "https://api.unlob.com",
      "description": "Production"
    }
  ],
  "externalDocs": {
    "description": "Documentation",
    "url": "https://www.unlob.com/docs"
  },
  "tags": [
    {
      "name": "search",
      "description": "Find passages and fetch the one you chose to read."
    },
    {
      "name": "graph",
      "description": "Traverse the coverage graph: corroboration, authority, paths and packed context."
    },
    {
      "name": "ops",
      "description": "Capability discovery, coverage transparency, account state and probes."
    }
  ],
  "paths": {
    "/search": {
      "get": {
        "operationId": "search",
        "summary": "Search",
        "description": "The main query surface: returns metadata-only passage hits — url, host, title, snippet, score and world-model signals — but never the page body.\n\nSearch accepts a boolean query (`rust AND async`, `-python`, `\"exact phrase\"`) and roughly thirty filters covering language, site, date, quality, authority, topic and graph centrality. It runs in keyword, semantic or hybrid mode; hybrid is the default and fuses BM25 with vector results by reciprocal rank.\n\nEvery hit is metadata only. That is a deliberate constraint rather than a limitation: an agent that receives ten full page bodies has spent its context before it has decided which page it wanted. Fetch the text you actually need with `/doc/:id`.\n\nHits carry more than relevance. `centrality`, `community_id`, `independent_sources` and `in_degree` are baked into the index at build time, so you can filter and sort by how well-connected or how well-corroborated a result is without paying for a graph query.",
        "tags": [
          "search"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "The query. Supports AND / OR, `-` negation and \"quoted phrases\".",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "mode",
            "in": "query",
            "required": false,
            "description": "Retrieval strategy.",
            "schema": {
              "type": "string",
              "enum": [
                "keyword",
                "semantic",
                "hybrid"
              ],
              "default": "hybrid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Number of hits. `0` returns the count only.",
            "schema": {
              "type": "integer",
              "default": "10"
            }
          },
          {
            "name": "vertical",
            "in": "query",
            "required": false,
            "description": "Restrict to one vertical core. Omit to auto-route.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "site",
            "in": "query",
            "required": false,
            "description": "Restrict to a single host.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "exclude_site",
            "in": "query",
            "required": false,
            "description": "Hosts to drop from results.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "ISO language code. Also accepts `langs[]` for several.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Crawl-time window (`fetched_at`).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Crawl-time window (`fetched_at`).",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "published_from",
            "in": "query",
            "required": false,
            "description": "Content-date window — usually the recency you actually mean.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "published_to",
            "in": "query",
            "required": false,
            "description": "Content-date window — usually the recency you actually mean.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "term",
            "in": "query",
            "required": false,
            "description": "An exact salient-term needle that must be present.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "source",
            "in": "query",
            "required": false,
            "description": "Crawl provenance: the batch corpus or the delta crawler.",
            "schema": {
              "type": "string",
              "enum": [
                "cc",
                "delta"
              ]
            }
          },
          {
            "name": "min_host_rank",
            "in": "query",
            "required": false,
            "description": "Floor on host authority.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "min_quality",
            "in": "query",
            "required": false,
            "description": "Floor on passage quality score.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "min_centrality",
            "in": "query",
            "required": false,
            "description": "Floor on graph centrality.",
            "schema": {
              "type": "number"
            }
          },
          {
            "name": "min_independent_sources",
            "in": "query",
            "required": false,
            "description": "Require corroboration by N distinct hosts.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "content_type",
            "in": "query",
            "required": false,
            "description": "article, news, docs, code, academic, forum, and more.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "authority",
            "in": "query",
            "required": false,
            "description": "edu, gov, org, com, other.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "topic",
            "in": "query",
            "required": false,
            "description": "Topic tags assigned at index time.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "tld",
            "in": "query",
            "required": false,
            "description": "Top-level domains to include.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "safe",
            "in": "query",
            "required": false,
            "description": "Drops explicit content.",
            "schema": {
              "type": "boolean",
              "default": "true"
            }
          },
          {
            "name": "min_words",
            "in": "query",
            "required": false,
            "description": "Passage length bounds.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "max_words",
            "in": "query",
            "required": false,
            "description": "Passage length bounds.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "relevance, recency, host_rank, quality, published, words or centrality.",
            "schema": {
              "type": "string",
              "default": "relevance"
            }
          },
          {
            "name": "collapse",
            "in": "query",
            "required": false,
            "description": "Deduplicate results by host, page or story cluster.",
            "schema": {
              "type": "string",
              "enum": [
                "none",
                "host",
                "page",
                "story"
              ]
            }
          },
          {
            "name": "facets",
            "in": "query",
            "required": false,
            "description": "Return facet counts alongside results.",
            "schema": {
              "type": "boolean",
              "default": "false"
            }
          },
          {
            "name": "fields",
            "in": "query",
            "required": false,
            "description": "Project only the fields you need.",
            "schema": {
              "type": "array",
              "items": {
                "type": "string"
              }
            }
          },
          {
            "name": "prefer_recent",
            "in": "query",
            "required": false,
            "description": "Soft ranking preferences rather than hard filters.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "prefer_authority",
            "in": "query",
            "required": false,
            "description": "Soft ranking preferences rather than hard filters.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "`{ vertical, routed, mode, total, results: WebHit[], facets? }`, where each `WebHit` carries url, host, title, snippet, score, published_at, content_type, authority, word_count, host_rank, quality, centrality, community_id, independent_sources and in_degree.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key."
          },
          "402": {
            "description": "Monthly quota exhausted on a hard-capped plan."
          },
          "403": {
            "description": "Account suspended."
          },
          "408": {
            "description": "Query exceeded the server timeout."
          },
          "429": {
            "description": "Rate limit exceeded."
          },
          "503": {
            "description": "Index not loaded."
          }
        },
        "externalDocs": {
          "description": "Search reference",
          "url": "https://www.unlob.com/docs/api/search"
        }
      }
    },
    "/similar": {
      "get": {
        "operationId": "similar",
        "summary": "Similar",
        "description": "More-like-this: given a passage id, returns its semantic neighbours from the same embedding space the index was built in.\n\nSimilar takes a hit you already have and finds what sits near it in vector space. It is the cheapest way to widen a result set without writing a second query, and because the embedding space is multilingual, the neighbours are not restricted to the language of the seed passage.\n\nUse it when a search returned one good result and you want the rest of that conversation — rather than guessing at the query terms that would have found them.",
        "tags": [
          "search"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "A passage id from a previous search hit.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Number of neighbours.",
            "schema": {
              "type": "integer",
              "default": "10"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Restrict neighbours to one language.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The same `WebHit[]` shape as `/search`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key."
          },
          "402": {
            "description": "Monthly quota exhausted on a hard-capped plan."
          },
          "403": {
            "description": "Account suspended."
          },
          "408": {
            "description": "Query exceeded the server timeout."
          },
          "429": {
            "description": "Rate limit exceeded."
          },
          "503": {
            "description": "Index not loaded."
          }
        },
        "externalDocs": {
          "description": "Similar reference",
          "url": "https://www.unlob.com/docs/api/similar"
        }
      }
    },
    "/browse": {
      "get": {
        "operationId": "browse",
        "summary": "Browse",
        "description": "Query-free browsing: the most recent or highest-ranked passages, optionally within a single vertical.\n\nNot every retrieval starts with a question. Browse returns a window over the index sorted by recency or host rank, which is what you want for a monitoring loop, a digest, or any agent whose job is \"tell me what changed\".",
        "tags": [
          "search"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "vertical",
            "in": "query",
            "required": false,
            "description": "Restrict to one vertical.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "description": "Ordering.",
            "schema": {
              "type": "string",
              "enum": [
                "recency",
                "host_rank"
              ],
              "default": "recency"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Number of passages.",
            "schema": {
              "type": "integer",
              "default": "10"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Language filter.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The same `WebHit[]` shape as `/search`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key."
          },
          "402": {
            "description": "Monthly quota exhausted on a hard-capped plan."
          },
          "403": {
            "description": "Account suspended."
          },
          "408": {
            "description": "Query exceeded the server timeout."
          },
          "429": {
            "description": "Rate limit exceeded."
          },
          "503": {
            "description": "Index not loaded."
          }
        },
        "externalDocs": {
          "description": "Browse reference",
          "url": "https://www.unlob.com/docs/api/browse"
        }
      }
    },
    "/doc/{id}": {
      "get": {
        "operationId": "doc",
        "summary": "Get document",
        "description": "Fetches the full cleaned text of one passage by id — the only endpoint that returns page content.\n\nSearch deliberately returns metadata. This is the other half of that decision: when the agent has decided which passage it actually wants, it fetches exactly that one.\n\nThe text is the cleaned extraction, not raw HTML — no navigation, no cookie banners, no footer. That is the difference between spending 800 tokens on a page and spending 6,000.",
        "tags": [
          "search"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "Passage id, as a path segment.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "`{ id, url, host, title, text, published_at, fetched_at, word_count }`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key."
          },
          "402": {
            "description": "Monthly quota exhausted on a hard-capped plan."
          },
          "403": {
            "description": "Account suspended."
          },
          "408": {
            "description": "Query exceeded the server timeout."
          },
          "429": {
            "description": "Rate limit exceeded."
          },
          "503": {
            "description": "Index not loaded."
          }
        },
        "externalDocs": {
          "description": "Get document reference",
          "url": "https://www.unlob.com/docs/api/doc"
        }
      }
    },
    "/why_not": {
      "get": {
        "operationId": "whyNot",
        "summary": "Why not",
        "description": "Coverage transparency: explains whether a URL is present in the index, was removed and why, or was never admitted.\n\nEvery other search API is a black box about absence. Ask it for a URL it does not have and you get an empty result set, which tells you nothing about whether the page was rejected, dropped, or never seen.\n\n`why_not` reads an append-only removal ledger and answers precisely. `present` means it is there. `removed` carries the reason — `redundant`, `superseded`, `stale`, `access-starved`, `lost-replacement` or `tombstoned-source` — plus what superseded it and when. `unknown` means it was never admitted.\n\nFor a regulated buyer this is the difference between \"we searched\" and \"here is what was considered, and here is what was excluded and why\".",
        "tags": [
          "ops"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "url",
            "in": "query",
            "required": true,
            "description": "The absolute URL to explain.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "A tagged union: `present`, `removed { reason, superseded_by?, removed_at }`, or `unknown`.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key."
          },
          "402": {
            "description": "Monthly quota exhausted on a hard-capped plan."
          },
          "403": {
            "description": "Account suspended."
          },
          "408": {
            "description": "Query exceeded the server timeout."
          },
          "429": {
            "description": "Rate limit exceeded."
          },
          "503": {
            "description": "Index not loaded."
          }
        },
        "externalDocs": {
          "description": "Why not reference",
          "url": "https://www.unlob.com/docs/api/why-not"
        }
      }
    },
    "/related": {
      "get": {
        "operationId": "related",
        "summary": "Related",
        "description": "Coverage graph traversal: the connected neighbourhood of a passage, up to k hops — the edges are what to read next.\n\nA search API hands back a list and leaves the agent to work out how the results relate. The relationships already exist in the index: which host published what, which passages belong to the same story, which topics and entities they touch, who links to whom.\n\n`related` exposes them. One call gives an agent a bounded neighbourhood to follow, instead of a search per hop with the agent guessing the query terms each time.",
        "tags": [
          "graph"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "Seed passage id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "hops",
            "in": "query",
            "required": false,
            "description": "Traversal depth. Bounded breadth-first.",
            "schema": {
              "type": "integer",
              "default": "1"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum neighbours returned.",
            "schema": {
              "type": "integer",
              "default": "10"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Connected passages with the edge type that linked them.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key."
          },
          "402": {
            "description": "Monthly quota exhausted on a hard-capped plan."
          },
          "403": {
            "description": "Account suspended."
          },
          "408": {
            "description": "Query exceeded the server timeout."
          },
          "429": {
            "description": "Rate limit exceeded."
          },
          "503": {
            "description": "Index not loaded."
          }
        },
        "externalDocs": {
          "description": "Related reference",
          "url": "https://www.unlob.com/docs/api/related"
        }
      }
    },
    "/corroborate": {
      "get": {
        "operationId": "corroborate",
        "summary": "Corroborate",
        "description": "The anti-hallucination check: how many distinct hosts independently carry a story, and which ones.\n\nOne echoed rumour and six independent reports look identical in a ranked list. Corroborate separates them: it returns the story cluster grouped by host, with the count of genuinely independent sources.\n\nThe subtle part is that corroboration survives deduplication. When a near-duplicate is rejected at admission, its host is still recorded against the surviving passage — so `independent_sources` counts every asserting host even though only one copy is kept, and `merged_duplicates` tells you how many were folded in.",
        "tags": [
          "graph"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "description": "A passage id belonging to the story.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum source groups.",
            "schema": {
              "type": "integer",
              "default": "10"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "`{ story_id?, independent_sources, merged_duplicates, sources: [{ host, host_rank, passages }] }`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key."
          },
          "402": {
            "description": "Monthly quota exhausted on a hard-capped plan."
          },
          "403": {
            "description": "Account suspended."
          },
          "408": {
            "description": "Query exceeded the server timeout."
          },
          "429": {
            "description": "Rate limit exceeded."
          },
          "503": {
            "description": "Index not loaded."
          }
        },
        "externalDocs": {
          "description": "Corroborate reference",
          "url": "https://www.unlob.com/docs/api/corroborate"
        }
      }
    },
    "/authorities": {
      "get": {
        "operationId": "authorities",
        "summary": "Authorities",
        "description": "The top passages on a topic ranked by graph centrality, so an agent can triage a field before reading into it.\n\nRelevance ranking answers \"what matches?\". Centrality answers \"what does this field consider load-bearing?\". They are different questions, and for an agent entering an unfamiliar domain the second one is usually the useful one.\n\nAuthorities returns the well-connected passages for a topic or entity, which is how you fill a context window with the sources a domain expert would have started from rather than the ones that happened to match the query string.",
        "tags": [
          "graph"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "topic",
            "in": "query",
            "required": true,
            "description": "A topic tag or entity name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Number of passages.",
            "schema": {
              "type": "integer",
              "default": "10"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The same `WebHit[]` shape as `/search`, ordered by centrality.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key."
          },
          "402": {
            "description": "Monthly quota exhausted on a hard-capped plan."
          },
          "403": {
            "description": "Account suspended."
          },
          "408": {
            "description": "Query exceeded the server timeout."
          },
          "429": {
            "description": "Rate limit exceeded."
          },
          "503": {
            "description": "Index not loaded."
          }
        },
        "externalDocs": {
          "description": "Authorities reference",
          "url": "https://www.unlob.com/docs/api/authorities"
        }
      }
    },
    "/dossier": {
      "get": {
        "operationId": "dossier",
        "summary": "Dossier",
        "description": "A one-hop brief on an entity: where it is mentioned, which hosts cover it, and which entities co-occur with it.\n\nBuilding a picture of an entity by hand means running roughly ten searches and merging the results yourself, in context, expensively. Dossier is that operation as a single call.\n\nIt returns mention counts, the hosts that carry them ranked by authority, and the entities that appear alongside — which is usually where the next question comes from.",
        "tags": [
          "graph"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "entity",
            "in": "query",
            "required": true,
            "description": "The entity name.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum mentions, sources and related entities.",
            "schema": {
              "type": "integer",
              "default": "10"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "`{ entity, mentions, top_sources: [host, count][], related_entities: [entity, count][] }`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key."
          },
          "402": {
            "description": "Monthly quota exhausted on a hard-capped plan."
          },
          "403": {
            "description": "Account suspended."
          },
          "408": {
            "description": "Query exceeded the server timeout."
          },
          "429": {
            "description": "Rate limit exceeded."
          },
          "503": {
            "description": "Index not loaded."
          }
        },
        "externalDocs": {
          "description": "Dossier reference",
          "url": "https://www.unlob.com/docs/api/dossier"
        }
      }
    },
    "/path": {
      "get": {
        "operationId": "path",
        "summary": "Path",
        "description": "The shortest chain of edges linking two passages — reasoning-path retrieval, or connect-the-dots.\n\nGiven two things, how are they connected? Path answers it structurally: the shortest edge chain between two passages, with each intermediate node typed as a passage, host, story, topic or entity.\n\nThis is the investigative primitive. It is also the one that most obviously cannot be reconstructed from a ranked list, no matter how many searches an agent runs.",
        "tags": [
          "graph"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": true,
            "description": "Start passage id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "description": "End passage id.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "max_hops",
            "in": "query",
            "required": false,
            "description": "Search depth ceiling.",
            "schema": {
              "type": "integer",
              "default": "4"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "`{ found, hops, nodes: [{ key, kind: passage | host | story | topic | entity, hit? }] }`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key."
          },
          "402": {
            "description": "Monthly quota exhausted on a hard-capped plan."
          },
          "403": {
            "description": "Account suspended."
          },
          "408": {
            "description": "Query exceeded the server timeout."
          },
          "429": {
            "description": "Rate limit exceeded."
          },
          "503": {
            "description": "Index not loaded."
          }
        },
        "externalDocs": {
          "description": "Path reference",
          "url": "https://www.unlob.com/docs/api/path"
        }
      }
    },
    "/assemble_context": {
      "get": {
        "operationId": "assembleContext",
        "summary": "Assemble context",
        "description": "GraphRAG as a single call: a corroborated, story-deduplicated, trust-ranked context pack fitted to a token budget, with a reason attached to every passage.\n\nThe usual retrieval loop is: search, deduplicate, assess trust, verify against other sources, rank, truncate to fit the context window. That is five or six round trips of agent reasoning and a lot of tokens spent deciding what not to read.\n\n`assemble_context` runs that loop server-side. It retrieves a story-collapsed pool, ranks it by corroboration first, then authority, then relevance, and packs it to the token budget you specify.\n\nEvery item comes back with the reason it was included. Agents that must show their work — which is most of them, in production — get an explainable context set rather than an opaque blob.",
        "tags": [
          "graph"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "description": "The question to assemble context for.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "budget",
            "in": "query",
            "required": false,
            "description": "Token budget for the pack.",
            "schema": {
              "type": "integer",
              "default": "4000"
            }
          },
          {
            "name": "lang",
            "in": "query",
            "required": false,
            "description": "Language restriction.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "min_independent_sources",
            "in": "query",
            "required": false,
            "description": "Require corroboration before inclusion.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "`{ query, token_budget, estimated_tokens, items: [{ hit, reason }] }`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key."
          },
          "402": {
            "description": "Monthly quota exhausted on a hard-capped plan."
          },
          "403": {
            "description": "Account suspended."
          },
          "408": {
            "description": "Query exceeded the server timeout."
          },
          "429": {
            "description": "Rate limit exceeded."
          },
          "503": {
            "description": "Index not loaded."
          }
        },
        "externalDocs": {
          "description": "Assemble context reference",
          "url": "https://www.unlob.com/docs/api/assemble-context"
        }
      }
    },
    "/describe": {
      "get": {
        "operationId": "describe",
        "summary": "Describe",
        "description": "A self-describing capability catalog: the filter grammar and every controlled vocabulary, so an agent can discover the query surface instead of guessing it.\n\nHardcoding a vendor's filter vocabulary into an agent means the agent breaks quietly whenever the vocabulary changes. `/describe` returns the current grammar and the valid values for every enumerated field — content types, authorities, topics, safety levels, verticals and sort orders.\n\nIt needs no authentication, so an agent can read the capability surface before it holds a key.",
        "tags": [
          "ops"
        ],
        "security": [],
        "parameters": [],
        "responses": {
          "200": {
            "description": "The filter grammar plus every controlled vocabulary as JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "408": {
            "description": "Query exceeded the server timeout."
          },
          "503": {
            "description": "Index not loaded."
          }
        },
        "externalDocs": {
          "description": "Describe reference",
          "url": "https://www.unlob.com/docs/api/describe"
        }
      }
    },
    "/account": {
      "get": {
        "operationId": "account",
        "summary": "Account",
        "description": "Your tenant’s current plan, usage against quota and remaining allowance for this billing period.\n\nRead your own usage from the same key you search with — useful for surfacing quota state inside your product rather than asking a human to open the console.",
        "tags": [
          "ops"
        ],
        "security": [
          {
            "ApiKeyHeader": []
          },
          {
            "BearerAuth": []
          }
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "`{ plan, monthly_quota, usage_this_period, quota_remaining, rate_per_min, period_start_unix }`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key."
          },
          "402": {
            "description": "Monthly quota exhausted on a hard-capped plan."
          },
          "403": {
            "description": "Account suspended."
          },
          "408": {
            "description": "Query exceeded the server timeout."
          },
          "429": {
            "description": "Rate limit exceeded."
          },
          "503": {
            "description": "Index not loaded."
          }
        },
        "externalDocs": {
          "description": "Account reference",
          "url": "https://www.unlob.com/docs/api/account"
        }
      }
    },
    "/healthz": {
      "get": {
        "operationId": "healthz",
        "summary": "Health",
        "description": "Unauthenticated liveness probe. Returns 200 while the process is up.\n\nLiveness only — it says the process is running, not that it can serve results. For that, use `/readyz`.",
        "tags": [
          "ops"
        ],
        "security": [],
        "parameters": [],
        "responses": {
          "200": {
            "description": "`ok`",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "408": {
            "description": "Query exceeded the server timeout."
          },
          "503": {
            "description": "Index not loaded."
          }
        },
        "externalDocs": {
          "description": "Health reference",
          "url": "https://www.unlob.com/docs/api/healthz"
        }
      }
    },
    "/readyz": {
      "get": {
        "operationId": "readyz",
        "summary": "Readiness",
        "description": "Unauthenticated readiness probe: 200 once the index is loaded and non-empty, 503 while it is not.\n\nThe distinction from `/healthz` matters during a deploy. A node that is up but has not finished loading its index will answer health checks and return nothing useful — readiness refuses traffic until the index is actually there.",
        "tags": [
          "ops"
        ],
        "security": [],
        "parameters": [],
        "responses": {
          "200": {
            "description": "200 when ready, 503 with a reason when not.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "408": {
            "description": "Query exceeded the server timeout."
          },
          "503": {
            "description": "Index not loaded."
          }
        },
        "externalDocs": {
          "description": "Readiness reference",
          "url": "https://www.unlob.com/docs/api/readyz"
        }
      }
    },
    "/metrics": {
      "get": {
        "operationId": "metrics",
        "summary": "Metrics",
        "description": "Prometheus metrics: per-status request counters, a latency histogram and an in-flight gauge.\n\nStandard Prometheus text exposition, scrapeable without authentication.",
        "tags": [
          "ops"
        ],
        "security": [],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Prometheus text format.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "408": {
            "description": "Query exceeded the server timeout."
          },
          "503": {
            "description": "Index not loaded."
          }
        },
        "externalDocs": {
          "description": "Metrics reference",
          "url": "https://www.unlob.com/docs/api/metrics"
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "An API key of the form ulb_ followed by 48 hexadecimal characters."
      },
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "The same key, sent as Authorization: Bearer ulb_..."
      }
    }
  },
  "security": [
    {
      "ApiKeyHeader": []
    },
    {
      "BearerAuth": []
    }
  ],
  "x-plans": [
    {
      "id": "free",
      "name": "Free",
      "monthlyUsd": 0,
      "monthlyRequests": 10000,
      "requestsPerMinute": 60,
      "historyDays": 30,
      "hardCap": true
    },
    {
      "id": "build",
      "name": "Build",
      "monthlyUsd": 29,
      "monthlyRequests": 50000,
      "requestsPerMinute": 600,
      "historyDays": 365,
      "hardCap": false
    },
    {
      "id": "scale",
      "name": "Scale",
      "monthlyUsd": 199,
      "monthlyRequests": 500000,
      "requestsPerMinute": 3000,
      "historyDays": 1826,
      "hardCap": false
    },
    {
      "id": "archive",
      "name": "Archive",
      "monthlyUsd": 799,
      "monthlyRequests": 2000000,
      "requestsPerMinute": 12000,
      "historyDays": null,
      "hardCap": false
    }
  ]
}