unlob Docs
Browse documentation

Operations and discovery

How an agent finds out what this API can do, and the probes your infrastructure expects.

Eight endpoints that do not search anything. Six let a caller learn what the API is before it has a key; two are the probes an orchestrator expects. None of them are billed, and none of them need a key.

They are open, and open is not the same as unmetered: a flood of unauthenticated requests from one address is refused with a 429 and a retry-after. Reading them the way they are meant to be read — once at startup, cached — is nowhere near that.

Discovery, and why it is open

An agent has to be able to learn what a service does before deciding whether to sign up for it. If the only way to find out what filters exist is to read a documentation site, then every agent that uses this API has a human in its setup loop.

So the descriptions of the API are served by the API, unauthenticated:

EndpointWhat it answers
GET /describeWhat can this deployment do right now?
GET /openapi.jsonThe full OpenAPI 3.1 description of every endpoint
GET /mcp/tools.jsonExactly what the MCP server’s tools/list reports; ?profile=grounding for the two-tool profile
GET /llms.txtA short prose orientation, for a model to read
GET /skill.mdThe Agent Skill: how to use the API, in the shape a coding agent installs
GET /guides/{slug}Long-form worked examples as markdown

All six are generated from the API’s own definitions. There is no hand-maintained copy anywhere that could disagree with them.

GET /describe — read this at startup

The one worth building against. /openapi.json tells you what the API accepts; /describe tells you what this deployment currently holds.

curl -s https://api.unlob.com/describe
{
  "filters": [["query", "free text with boolean/phrase/field syntax"], ["vertical", "…"]],
  "sort": ["relevance", "recency", "host_rank", "quality", "published", "words", "centrality"],
  "collapse": ["none", "host", "page", "story"],
  "modes": ["keyword", "semantic", "hybrid"],
  "verticals": ["code", "science", "cooking"],
  "content_types": ["article", "docs", "qa", "…"],
  "authorities": ["…"],
  "topics": ["…"]
}

The difference matters. verticals, content_types, authorities and topics are read from the live index, so they list the values that actually have data behind them. A filter is not useful because it is accepted; it is useful because something matches it.

Four more blocks describe the evidence layer: profiles (the MCP profiles this server answers), ground (the defaults, the statuses, the actions it may suggest, and how independence is counted), vocab (every source_role, origin_type, risk flag and claim relation), and capabilities — including on_demand_crawl: false, so an agent can plan around what the deployment will not do rather than discover it.

Read it once at startup and cache it, rather than hard-coding the filter list from the reference. A vertical added next month then costs you nothing, and a filter you hard-coded against an empty vocabulary stops silently returning zero.

GET /openapi.json

The complete OpenAPI 3.1 document, generated from the running definitions. Point a code generator, a Custom GPT action or an MCP client’s schema loader at it directly:

curl -s https://api.unlob.com/openapi.json | jq '.paths | keys'

www.unlob.com/openapi.json answers with a small JSON pointer to this URL rather than a second copy of the document. One API, one specification.

GET /mcp/tools.json

The MCP tool definitions, readable without opening a session. Each carries a description written for a model, an inputSchema and an outputSchema. Add ?profile=grounding for the two-tool profile — the body then also names the profile.

This is the shortest path to giving a non-MCP agent the same tools — the schemas are already in the shape the Anthropic Messages API wants:

spec = httpx.get("https://api.unlob.com/mcp/tools.json").json()
tools = [
    {"name": t["name"], "description": t["description"], "input_schema": t["inputSchema"]}
    for t in spec["tools"]
]

See Tool use, directly for the rest of that pattern.

GET /llms.txt and GET /guides/{slug}

/llms.txt is a short plain-text orientation: what the service is, how to call it, and the handful of behaviours worth knowing before the first request. It is deliberately short enough to paste into a system prompt.

/guides/{slug} serves the worked examples as markdown. Three slugs: search-recipes, graph-recipes and agent-skill. They are a compile-time table rather than a path join, so a slug cannot traverse to a file — /guides/../../etc/passwd is a 404, not a surprise.

curl -s https://api.unlob.com/llms.txt
curl -s https://api.unlob.com/guides/search-recipes

This site serves the same guides at Search recipes, Graph recipes and The Agent Skill, rendered from the same source.

GET /skill.md

The Agent Skill, in the SKILL.md shape coding agents install: YAML front matter naming the skill, then the ground → receipt → get_documentwhy_not loop with the exit semantics of status. Save it as .claude/skills/unlob/SKILL.md (or your agent’s equivalent) and the agent can use the API without a human reading this site first.

curl -s https://api.unlob.com/skill.md

The probes

EndpointAnswersUse it for
GET /healthzok while the process is upLiveness
GET /readyzready, or 503 with no shards configuredReadiness

Both answer while the service is saturated — that is the point of a probe, and admitting them through the same capacity that traffic contends for would fail liveness during a slowdown and restart the process mid-incident.

An empty index is ready

/readyz answers 200 when the node has a backend wired up, even if that backend holds zero documents. Only “no shards configured” fails readiness.

That is a correction, and it is worth knowing why. Readiness used to fail while the index was empty, which turns no data into no service: a node that fails readiness is taken out of rotation, and then every route on it goes with — /search, but also /describe, which is exactly what an agent reads to find out what the deployment currently holds.

If you are proxying this API and mirroring its readiness, mirror that behaviour too.

Readiness does not mean the index is answering

/readyz reports that this node has a backend wired up. It does not report that the search plane is serving, and that is deliberate rather than an oversight — a node that fails readiness is taken out of rotation, so making readiness follow the index would take /describe offline exactly during the incident you need to understand. The instrument would go dark with the thing it measures.

So the two questions are answered separately. If you are diagnosing 503s, /readyz is not the endpoint to ask.

What tells you the data plane is down

A 503 from a search route. When no shard can serve a request, every search and graph endpoint answers 503 — and unlike the at-capacity 503, it carries no retry-after, because it does not clear on a schedule your client can wait out. A run of those, while /healthz and /readyz still answer 200, is the signal that the problem is the index rather than your request. See Errors.

There is no public endpoint that enumerates the serving fleet’s internals. The operational surface — the counters and the per-shard view an operator watches — is not part of this API, and nothing in it is a caller’s to poll.

None of this is billed

Every endpoint on this page is free and needs no key. They cost no credits, and they do not spend from your plan’s credits-a-minute window, because a caller that has to spend credits to find out what it can do is a caller that will guess instead. The only limit on them is the per-address one above, which exists to stop a flood rather than to pace a client.

Everything on the endpoint reference costs the credits listed on Pricing, with one exception: /account needs a key but costs no credits, because a caller should not have to spend credits to find out how many are left. See Pricing.

Next