Every WordPress Site Has an API. Almost None Advertise It.

AI agents and automated tools are starting to poke at websites looking for APIs. Right now they mostly guess: try /api, try /wp-json, read the HTML and hope for a link tag. There’s never been a standard place to just ask “what APIs does this site have?”

The standard

RFC 9727, published in June 2025, fixes that.1 It registers a well-known URI, /.well-known/api-catalog, as the standard front door for API discovery. Any client, human or automated, can request that one path and get back a machine-readable list of what’s available.

The response format is a linkset, defined in RFC 9264, served as application/linkset+json: a flat JSON structure with an array of entries, each anchored to an API’s base URL with links to its documentation or description.2 I built a WordPress plugin that serves one: wp-api-catalog. Here’s the catalog it returns on a test install:

{
  "linkset": [
    {
      "anchor": "https://example.com/wp-json/oembed/1.0",
      "service-desc": [
        { "href": "https://example.com/wp-json/oembed/1.0", "type": "application/json" }
      ]
    },
    {
      "anchor": "https://example.com/wp-json/wp/v2",
      "service-desc": [
        { "href": "https://example.com/wp-json/wp/v2", "type": "application/json" }
      ],
      "service-doc": [
        { "href": "https://developer.wordpress.org/rest-api/reference/", "type": "text/html" }
      ]
    }
  ]
}

Two entries, one per REST namespace. The wp/v2 entry also carries a service-doc link pointing at the actual WordPress REST API handbook, so a client that finds this file has both the endpoint and the documentation for it in one response.

WordPress already has the data

Every WordPress install already has the raw material for this: /wp-json/ has served a namespace index since the REST API infrastructure landed in core in 4.4, and the content endpoints followed in 4.7.3 It’s been sitting there for a decade, and almost nobody publishes it at the one location a spec-compliant client would look.

A large tricolor dog sitting upright among ferns and hostas in a garden, captioned BEEN SITTING HERE above and FOR A DECADE below

The plugin

Activate the plugin and it’s done. No settings screen, no configuration. It walks the registered REST namespaces, builds the linkset, and serves it at /.well-known/api-catalog. Here’s the response from the same install:

$ curl -k -sS -D - https://example.com/.well-known/api-catalog

HTTP/2 200
content-type: application/linkset+json
cache-control: max-age=3600
link: <https://example.com/.well-known/api-catalog>; rel="api-catalog"

RFC 9727 also allows the discovery relation to show up on other responses, not just the catalog endpoint itself, so a client that lands on any page of the site can still find it.1 This plugin adds that Link header to every front-end response:

$ curl -k -sS -I https://example.com/

link: <https://example.com/.well-known/api-catalog>; rel="api-catalog"

That cache-control header tells clients they can hold the response for an hour. Server-side, the plugin caches the built catalog for five minutes so it doesn’t rebuild on every request. One requirement worth calling out: pretty permalinks have to be turned on. With plain permalinks WordPress never consults rewrite rules, and the endpoint 404s.

Extending it

If the built-in namespace listing isn’t enough, five filters cover the rest:

  • wp_api_catalog_entry modifies or drops a single namespace’s entry
  • wp_api_catalog_linkset touches the whole document before it’s cached
  • wp_api_catalog_send_link_header turns off the front-end header
  • wp_api_catalog_cache_ttl and wp_api_catalog_cache_type control caching

wp_api_catalog_linkset is the one to reach for when an API isn’t a WordPress REST namespace at all, say a separate service running alongside the site. It appends to whatever the plugin already found:

add_filter( 'wp_api_catalog_linkset', function ( array $linkset ): array {
	$linkset['linkset'][] = array(
		'anchor'       => 'https://external.example.com/api',
		'service-desc' => array(
			array(
				'href' => 'https://external.example.com/openapi.json',
				'type' => 'application/vnd.oai.openapi+json',
			),
		),
	);
	return $linkset;
} );

The plugin is free, GPL-2.0-or-later, no Composer dependency, no build step.

84em.com ships an llms.txt for the same reason this plugin exists: agents work better when a site hands them a structured entry point instead of making them guess.

Need some help?

Reach out and describe your project.

Folks We've Helped