RFC 9727 explained: a single endpoint for APIs
RFC 9727 defines a tiny but powerful convention: a publisher hosts a single document at /.well-known/api-catalog that lists every public API. Agents, SDKs, registry tools, and humans can fetch this one URL to find the OpenAPI specs, status pages, and human docs for the publisher's API surface. No HTML scraping, no guessing endpoint paths.
The wire format
The document uses the Linkset format with the content type application/linkset+json. Each API is a single linkset entry whose anchor is the canonical API URL, and whose related links point to the OpenAPI spec, the human docs, the status page, and so on.
{
"linkset": [
{
"anchor": "https://example.com/api/v1",
"service-desc": [
{
"href": "https://example.com/api/v1/openapi.json",
"type": "application/json"
}
],
"service-doc": [
{ "href": "https://example.com/docs/api/v1", "type": "text/html" }
],
"status": [{ "href": "https://status.example.com" }],
"description": "Public REST API v1."
}
]
}The link relations come from RFC-registered values. service-desc is the OpenAPI document. service-doc is the human-readable docs page. status is the operational status page.
Why this matters
Before RFC 9727, agents that wanted to call your APIs had to:
- Guess that you have docs at
/docsor/apior/developer. - Render the HTML to find a link to the OpenAPI spec.
- Hope nothing in the layout breaks the parse.
After RFC 9727, the same agent does:
GET /.well-known/api-catalogAnd gets a complete machine-readable map of every API the publisher exposes. This is the agent equivalent of what robots.txt and sitemap.xml are for search.
Implementation in Next.js
// app/.well-known/api-catalog/route.ts
import { NextResponse } from "next/server";
const PAYLOAD = {
linkset: [
{
anchor: "https://example.com/api/v1",
"service-desc": [
{
href: "https://example.com/api/v1/openapi.json",
type: "application/json",
},
],
"service-doc": [
{ href: "https://example.com/docs/api/v1", type: "text/html" },
],
status: [{ href: "https://status.example.com" }],
description: "Public REST API v1.",
},
],
};
export async function GET() {
return NextResponse.json(PAYLOAD, {
headers: {
"content-type": "application/linkset+json; charset=utf-8",
"cache-control": "public, max-age=300",
},
});
}The content type matters. Some agents reject application/json for this resource because the spec mandates application/linkset+json. Set it correctly the first time.
Advertise it from the homepage
Send a Link header on the homepage so agents can discover the catalog without guessing:
Link: </.well-known/api-catalog>; rel="api-catalog"In Next.js 16, this lives in proxy.ts (the convention previously known as middleware).
Anchor URLs are not endpoints
A common mistake: putting your real endpoint URL in anchor and expecting agents to call it. That is not what anchor means. Anchor is the conceptual identifier for the API. The actual callable endpoints are described by the OpenAPI document linked via service-desc. Keep anchor stable across versions even if your underlying URLs move.
Multiple APIs
If you ship two or three public APIs, list them all:
{
"linkset": [
{ "anchor": "https://example.com/api/v1", "service-desc": [...] },
{ "anchor": "https://example.com/api/admin", "service-desc": [...] },
{ "anchor": "https://example.com/api/webhooks", "service-desc": [...] }
]
}Order does not matter to agents, but humans tend to read it top-down. Put the most-used API first.
What this does not solve
api-catalog tells agents what APIs exist. It does not authenticate them, rate limit them, or bill them. Pair it with OAuth (/.well-known/oauth-authorization-server) and standard rate limit headers for a complete agent-ready API surface.
Build one
Use the API Catalog Builder to scaffold a Linkset and a Next.js route handler in a couple of clicks. Pair it with the Link Headers Builder so the catalog is advertised from your homepage.