Table of contents
- Quick summary
- Two files answer two different questions
- What the file actually has to contain
- Path one: point it at an OpenAPI spec you already publish
- Path two: several API domains, one catalog
- What to keep out of a public catalog
- Verify it actually works
- What 32 API providers taught us
- Where AIScan fits, and where it doesn't
- Next step
Quick summary
Agents that want to call your API have to guess the URL today. RFC 9727 gives them a fixed place to look instead: /.well-known/api-catalog, a small JSON document that points straight at your OpenAPI spec and your docs. We probed 32 API-heavy domains on 25 September 2026 and found the file on 4, with a fifth serving the right JSON under the wrong content type, roughly one in six among sites that would benefit most.
| If you want to… | Do this | Check |
|---|---|---|
| Pass the fastest | Add one Link header plus a 15-line JSON file | check:P1 |
| Point agents at an existing OpenAPI spec | Use the service-desc relation from RFC 8631 | check:E2 |
| Cover several API domains from one place | Use item links plus nested api-catalog relations | RFC 9727 §5.1 |
| Confirm it actually works | curl a path that should 404, then compare | Landing check, below |
Two files answer two different questions
llms.txt tells an agent what to read. /.well-known/api-catalog tells it what to call. A blog post, a pricing page, a docs article: those are llms.txt's job, and check:C2 grades it. An endpoint that takes a request and returns structured data: that's what an API catalog exists for, and check:P1 grades it. A site can have one without the other. A SaaS product with a public REST API and no blog needs the catalog and can skip llms.txt entirely; a documentation site with no API needs the reverse.
The gap the RFC closes is a genuine one. robots.txt says what a crawler may fetch. sitemap.xml says what pages exist. Neither says "here is a machine you can operate, and here is its instruction manual." An agent that wants to book a flight, check an order status, or pull a metric today has to already know your API's base URL, or has to have been told about it in a prompt, because there was never a well-known place to look for the answer. RFC 9727, published by the IETF in June 2025, fixes that the same way robots.txt fixed crawl discovery in 1994: one fixed path, resolved the same way everywhere.
What the file actually has to contain
The RFC is short and the requirements are specific, so it's worth reading them straight rather than guessing:
- The document MUST be reachable at
/.well-known/api-catalogand MUST be served asapplication/linkset+json, the format defined in RFC 9264. Other formats are allowed alongside it through content negotiation, never instead of it. - The top-level key is
linkset, an array of objects. Each object has ananchor(the resource the links describe) and one or more link-relation arrays hanging off it. - Four relations, defined by RFC 8631, do the actual work:
service-desc(a machine-readable spec, usually your OpenAPI document),service-doc(human docs),service-meta(policies, rate limits, anything else machine-readable), andstatus(a health or uptime endpoint). A simpleritemrelation, from RFC 6573, just lists API endpoints as bookmarks when you don't want to describe each one inline. - The Linkset SHOULD carry a
profileparameter pointing athttps://www.rfc-editor.org/info/rfc9727, so a client can confirm the document is specifically an API catalog and not just any linkset. - Serve it over HTTPS. The spec calls this out explicitly in its security section, alongside read-only access for external requests and a periodic prune of "zombie" APIs nobody maintains anymore.
That's the whole content requirement. What trips people up is everything the file is not: it is not your OpenAPI spec (that's what service-desc points to), not a sitemap, and not a replacement for API documentation. It is a signpost with exactly two required stops: what's here, and where the real description lives.
Path one: point it at an OpenAPI spec you already publish
Most APIs worth cataloging already have an OpenAPI document sitting at /openapi.json. If that's you, this is a fifteen-minute change.
- Confirm your OpenAPI URL resolves.
curl -s https://yourapi.com/openapi.json | head -c 200should return JSON starting with"openapi"or"swagger". - Write the linkset, using your API's base URL as the anchor:
{ "linkset": [ { "anchor": "https://api.yourcompany.com/", "service-desc": [ { "href": "https://api.yourcompany.com/openapi.json", "type": "application/json" } ], "service-doc": [ { "href": "https://docs.yourcompany.com/api", "type": "text/html" } ] } ] } - Serve it at
/.well-known/api-catalogwithContent-Type: application/linkset+json. On a static site, that's a file inpublic/.well-known/api-catalog; on a framework with typed routes, a route handler that sets the header explicitly, since most static-file servers default new extensions toapplication/octet-streamor refuse to serve dotfile directories at all. - Add the
Linkheader to your homepage response so a crawler that never thinks to try the well-known path still finds it:Link: </.well-known/api-catalog>; rel="api-catalog"; type="application/linkset+json" - Add the
profileparameter to the response'sContent-Type:application/linkset+json; profile="https://www.rfc-editor.org/info/rfc9727".
Path two: several API domains, one catalog
Bigger platforms split APIs across subdomains: a payments API at api.example.com, a webhooks service at hooks.example.com. RFC 9727 §5.1 covers exactly this with two extra relations: item, to bookmark each sub-API, and a nested api-catalog relation when a domain you don't own hosts part of your surface.
- Publish one catalog as canonical, typically on your primary API domain, and make every other domain's
/.well-known/api-catalogredirect to it rather than duplicating the content (the RFC's own recommendation), so the catalog never drifts out of sync with itself. - List each sub-API as an
item, then give each its own anchor entry with its ownservice-desc:{ "linkset": [ { "anchor": "https://www.example.com/.well-known/api-catalog", "item": [ { "href": "https://api.example.com/v1" }, { "href": "https://hooks.example.com/v1" } ] }, { "anchor": "https://api.example.com/v1", "service-desc": [{ "href": "https://api.example.com/openapi.json", "type": "application/json" }] }, { "anchor": "https://hooks.example.com/v1", "service-desc": [{ "href": "https://hooks.example.com/openapi.json", "type": "application/json" }] } ] } - For a domain a partner controls, don't inline their APIs. Link to their catalog with the
api-catalogrelation instead, so ownership stays where the RFC says it belongs. - Group by category once the catalog gets large. The spec's scalability guidance (§5.3) is to publish sub-catalogs per business or technology area and have the root catalog link to each, rather than shipping one enormous file.
What to keep out of a public catalog
Not every API belongs in the public file. RFC 9727 splits this into two deployment shapes, and mixing them up is the most consequential mistake available here.
A public catalog is for APIs third parties should find and call. Before publishing, the spec's own security guidance (§8) says to review it for anything that leaks business or personal metadata, and to check that no internal-only API rode along by accident. A catalog hosted on your public developer domain should never expose paths that only make sense on your internal network, even if hand-writing the JSON made it tempting to just list everything you have.
Some organizations also want the well-known URI for internal APIs that employees or internal systems should discover, but that the public should never see. RFC 9727 §5.2 allows this, with the caveat that it carries its own risk: the well-known path is a predictable, guessable location, so an internal catalog needs real access control (CORS policy, an allowlist, authentication) in front of it, not just obscurity. Never serve a private catalog from the same public path a search engine or a curious agent might request.
Both shapes need the same ongoing care once live. The spec's monitoring guidance (§5.4) is specific: treat catalog upkeep as a release-lifecycle step, not a one-time publish. Remove an API from the catalog the same day you decommission it, since a stale entry that still resolves is what the RFC calls a "zombie API," undocumented, unpatched, and still reachable by anything that read the catalog before you took it down. TLS is a SHOULD for the whole file, not optional for the parts that feel sensitive. And keep write access to the catalog itself restricted to whichever role actually manages your API release process; a file anyone can edit is a file that drifts from what your APIs actually do.
Verify it actually works
A status-code check alone proves nothing here, and we found that out the hard way on our own site. curl -o /dev/null -w '%{http_code}' https://aiscan.site/.well-known/api-catalog returns 200. So does https://aiscan.site/.well-known/api-catalog-this-path-was-never-registered, byte for byte, because our route matches on the path prefix rather than the exact suffix. A crawler that only checks for 200 would conclude the file exists at ten different URLs; only one of them is real. Run the control probe before trusting the real one:
| Step | Command | Pass signal |
|---|---|---|
| 1. Fetch the real path | curl -sI https://yoursite.com/.well-known/api-catalog | 200, Content-Type: application/linkset+json |
| 2. Fetch a path that must not exist | curl -sI https://yoursite.com/.well-known/api-catalog-xyz123 | 404 |
| 3. Compare | n/a | Step 1 succeeds, step 2 fails. If both return 200 with the same byte count, the route is a catch-all, not a real check |
| 4. Parse the body | curl -s .../.well-known/api-catalog | python3 -m json.tool | Top-level key is linkset, not links, a common typo that breaks parsing silently since both look valid at a glance |
| 5. Confirm the link resolves | curl -sI $(curl -s .../.well-known/api-catalog | jq -r '.linkset[0]["service-desc"][0].href') | 200 on the OpenAPI document itself |
We caught our own soft-200 with exactly this test while researching this post: /.well-known/api-catalog, /.well-known/api-catalog/anything, and /.well-known/api-catalog2026 all return the identical 2,667-byte document instead of the last two 404ing. It's a routing-precision bug, not a missing file, and it's the kind a status-only scanner will never surface. We're disclosing it here rather than quietly patching it, because it's the cleanest live example of why step 2 above matters.
What 32 API providers taught us
We probed 32 hosts on 25 September 2026, API-first platforms (Stripe, GitHub, Twilio, PayPal, Plaid, Slack, Auth0), a handful of infrastructure vendors, and our own domain, for /.well-known/api-catalog, running the control-path check above against every one.
| Result | Count | Hosts |
|---|---|---|
Conformant (correct type + profile parameter) | 2 | developers.cloudflare.com, vercel.com |
Adopted, correct media type, no profile parameter | 2 | netlify.com, supabase.com |
| Adopted, valid linkset body, wrong media type | 1 | zoom.us (binary/octet-stream) |
| Soft-200 false positive: passes a status-only check, fails the control probe | 2 | platform.openai.com, api.slack.com (both return the same SPA shell for the real path and a nonsense sibling path) |
| Hard 404 / 403 / 400 | 24 | Stripe, GitHub, Twilio, PayPal, Plaid, Slack, Auth0, OpenAI, and 16 others |
Five of 32 (15.6%) serve something real at the path; two of those five are fully spec-conformant. Two more sites would pass a naive curl | grep 200 check and fail the moment you run the control probe from the verification section above, an external, independent confirmation of the exact failure mode we found on our own site. That lines up with the wider figure: according to Cloudflare Radar, which measured the 200,000 most visited domains for its April 2026 agent-readiness study, "MCP Server Cards and API Catalogs (RFC 9727) together appear on fewer than 15 sites in the entire dataset." This is a small, first-party sample of API-heavy domains, not a census. But the two companies that got it fully right, Cloudflare and Vercel, are also the two whose own developer platforms other companies build on, which is at least evidence this is a solvable fifteen-minute change rather than a genuinely hard problem.
Where AIScan fits, and where it doesn't
AIScan grades this as check:P1, and the related check:E2 (Machine-readable API description) checks whether service-desc actually resolves to something an OpenAPI parser accepts. check:D3 (Link header for discovery) grades whether the homepage response advertises the catalog at all. Run npx aiscan-cli yoursite.com, or paste the URL at aiscan.site, free, no account required, and all three come back in one pass.
What the scan can't tell you: whether the OpenAPI document your catalog points to is accurate. A service-desc link that resolves to a 200 with valid JSON passes P1 and E2 even if that spec describes endpoints you retired eight months ago. RFC 9727's own maintenance guidance (§5.4) is blunt about this: a Publisher should treat catalog upkeep as part of every API release, removing stale entries as part of the same change that ships the API update, because nothing external checks that for you. AIScan can confirm the door exists and opens; it can't confirm what's behind it still matches the label.
If your catalog needs to describe an MCP server rather than a REST API, that's a different file with its own check: see Publish an MCP Server Card So Agents Can Find Your Tools for the check:P2–check:P4 mechanism. The two aren't interchangeable: a server card answers "what tools can an agent call through MCP," while an API catalog answers "what HTTP APIs does this domain expose." A platform with both should publish both, cross-linked from each other's service-meta.
Full check definitions live on /docs/checks/capabilities; more on the wider discovery-file picture is at /guides.
Next step
Run npx aiscan-cli yoursite.com, or scan at aiscan.site, for check:P1, check:E2, and check:D3 together. If you're weighing this against an MCP server card, read Publish an MCP Server Card So Agents Can Find Your Tools next, and see the full rubric at /docs/checks/capabilities or browse every guide at /guides.
Frequently asked questions
Do I need this if my site has no public API?
No. check:P1 is informational for a pure content site, and AIScan's own guidance says exactly that: if you expose nothing programmatic, this check costs you nothing either way.
Is /.well-known/api-catalog the same thing as llms.txt?
No. llms.txt is a curated index of pages an agent should read; the API catalog is an index of endpoints an agent can call. A site can need either, both, or neither depending on whether it has a real API.
My catalog returns 200 but AIScan still marks P1 as failing. Why?
Check the Content-Type header first. A body that's valid JSON but served as text/html or application/json instead of application/linkset+json fails the format requirement even though curl shows you readable content. Zoom's public catalog has exactly this problem today.
I probed a path that should be a 404 and got a 200 with the same file back. Is that a bug?
Yes, and it's worth fixing on your side the same way we're fixing it on ours: it means the route matches on a path prefix instead of the exact suffix, so any nonexistent sibling path silently passes. Assert a real 404 on a nonsense path before you trust the real one.
Does publishing this replace my OpenAPI spec?
No. The catalog is a pointer, not a substitute. service-desc in the linkset has to resolve to your actual OpenAPI document; if that document doesn't exist yet, publish it first, then point the catalog at it.
Do I need a separate catalog for every subdomain my APIs live on?
No, and the RFC recommends against it. Publish one canonical catalog, then make /.well-known/api-catalog on every other domain you control redirect to it, so there's a single copy to keep current.
What if part of my API is hosted by a third party I don't control?
Don't inline their endpoints in your own catalog. Link to their /.well-known/api-catalog using the api-catalog link relation instead, so their team stays responsible for keeping their own entries accurate.
I published the file but AIScan's Link header check (D3) still fails. What's wrong?
The file existing is necessary but not sufficient. RFC 9727 also requires the homepage response to carry a Link header pointing at the catalog, for crawlers that never guess the well-known path on their own. Check the response headers on your root URL, not just on the catalog file itself.
Related guides
Link Headers for AI Agent Discovery: Set Up RFC 8288 Relations
An agent that lands on your homepage has already paid for the request. If that response can also say where your llms.txt, sitemap and API catalog live, the agent skips the guessing entirely. The…
Your llms.txt Is Sending AI Agents to 404s: We Measured It on Our Own Site
| Question | Short answer | ||| | What broke? | Our own llms.txt, parsed the way most link extractors parse Markdown, sent roughly 55% of nonscan agent traffic to a 404. | | Why? | The standard…
Publish an MCP Server Card So Agents Can Find Your Tools
On 10 September 2026 we probed every host running a remote MCP server listed in the official MCP Registry: 79 domains, three requests each. Seven of them publish a parseable server card at…
llms.txt vs robots.txt vs sitemap.xml in 2026: Six Files, Six Jobs
Verified 4 September 2026. Every figure below was measured or fetched on that date. Three files keep getting compared as if they were competing for the same job: robots.txt, sitemap.xml and llms.txt.…
