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:
| Endpoint | What it answers |
|---|---|
GET /describe | What can this deployment do right now? |
GET /openapi.json | The full OpenAPI 3.1 description of every endpoint |
GET /mcp/tools.json | Exactly what the MCP server’s tools/list reports; ?profile=grounding for the two-tool profile |
GET /llms.txt | A short prose orientation, for a model to read |
GET /skill.md | The 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_document → why_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
| Endpoint | Answers | Use it for |
|---|---|---|
GET /healthz | ok while the process is up | Liveness |
GET /readyz | ready, or 503 with no shards configured | Readiness |
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
- Filters and sorting — what
/describeis describing - API reference — the endpoints that do search
- Quickstart