Agentimus exposes two distinct HTTP surfaces:

  1. A private admin REST API under the agentimus/v1 namespace, used by the Vue admin. Every route requires the manage_options capability and the standard WordPress REST nonce (X-WP-Nonce / apiFetch).
  2. A public, unauthenticated discovery surface — the generated /.well-known/* documents plus the front-end agent files (/llms.txt, .md twins, the fallback sitemap, security.txt). These are served for anonymous agents and crawlers, with permissive CORS.

This page documents both, with exact routes, parameters, response shapes, content types, and CORS behaviour. Everything below is derived from inc/Rest.php, inc/Endpoints.php, inc/Discovery/* and inc/Sitemap.php.

Admin REST API (agentimus/v1)

Base URL, auth and registration

All admin routes live under the agentimus/v1 namespace (Agentimus\Rest::NAMESPACE), so the base is:

/wp-json/agentimus/v1/<route>

Routes are registered on rest_api_init by Agentimus\Rest::register(). Every callback is gated by permission_callback => can_manage(), which is simply current_user_can( 'manage_options' ). Because the admin uses cookie authentication, requests must also carry a valid REST nonce. From the admin bundle this is transparent — apiFetch sends X-WP-Nonce automatically. From an external script you must send the nonce yourself:

curl -H "X-WP-Nonce: <nonce>" \
     --cookie "<wordpress-admin-cookies>" \
     https://example.com/wp-json/agentimus/v1/settings

The nonce is minted with wp_create_nonce( 'wp_rest' ).

GET / POST /settings

Read or persist the plugin settings.

GET /settings takes no parameters and returns both the current settings and a fresh readiness report:

{
  "settings":  { "enable_llms_txt": true, "enable_schema": true, "...": "..." },
  "readiness": [ { "id": "...", "label": "...", "status": "pass", "detail": "...", "fix": "...", "action": null } ]
}

settings is the full sanitized option array (Settings::all()). readiness is the same array returned by GET /readiness (see below).

POST /settings (WP_REST_Server::EDITABLE, i.e. POST/PUT/PATCH) saves settings. The handler is deliberately lenient about the request envelope — it accepts any of these bodies:

  • a bare JSON object of settings: { "enable_schema": false }
  • a wrapped object: { "settings": { "enable_schema": false } }
  • form params under a settings key

The input is passed through Settings::update(), which sanitizes and merges against defaults, then the response echoes the saved (re-read) values plus a fresh readiness report:

{ "settings": { "...": "..." }, "readiness": [ ... ], "saved": true }

Partial updates are supported for most flags, but note that boolean feature toggles have specific merge semantics in Settings::update() (some absent keys reset to default, others are treated as “off”); send the full settings object if you need deterministic results.

POST /settings/reset

Restores factory defaults via Settings::reset(). No parameters. The onboarding flag lives in its own option, so a reset does not re-trigger the setup wizard.

{ "settings": { "...factory defaults..." }, "readiness": [ ... ], "reset": true }

POST /onboarding

Marks the first-run setup wizard complete (or skipped) so it never shows again. It writes update_option( 'agentimus_onboarded', AGENTIMUS_VERSION ) — a separate option from the settings, so a factory reset will not resurrect the wizard. No parameters.

{ "onboarded": true }

GET /readiness

Returns the readiness report (Agentimus\Readiness::report()) — the same array embedded in the /settings responses. It is a flat list of check rows, each normalized to:

Field Type Meaning
id string Stable check id (also used as a UI anchor).
label string Human label.
status "pass" | "warn" | "fail" Outcome.
detail string One-line explanation of the current state.
fix string Guidance on how to resolve a warn/fail (may be empty).
action object | null Optional UI action descriptor (e.g. a deep link to Settings).

The set of checks is filterable via agentimus_readiness_checks (a Pro add-on can append its own rows), so integrators should treat the list as open-ended and key off id.

GET /preview/schema

Returns the exact JSON-LD @graph the front end would emit for the site, or for a chosen post — regardless of whether schema output is currently active. This is a preview tool: it shows what would ship even when schema is disabled or ceded to an SEO plugin.

Parameters

Name In Type Default Notes
post query integer 0 absint-sanitized. 0/absent → the site-wide identity graph. A non-zero id previews that post’s node.

A post id that does not exist, or whose post type is not in the agent-visible selection (Content::post_types()), returns WP_Error agentimus_preview_not_found with HTTP 404.

Response shape

{
  "active":       false,
  "reason":       "seo_plugin",
  "seoPlugin":    true,
  "target":       { "type": "post", "id": 42, "label": "Hello world", "url": "https://example.com/hello-world/" },
  "postIncluded": true,
  "postNote":     "",
  "livePublic":   true,
  "graph":        { "@context": "https://schema.org", "@graph": [ ... ] },
  "json":         "{ pretty, slash-unescaped JSON string }"
}

Key fields:

  • activetrue only when enable_schema is on and no SEO plugin owns schema output.
  • reason"disabled", "seo_plugin", or "ok"; lets the UI explain an empty live <head>.
  • targettype is "site" or "post"; carries the label and public URL.
  • postIncludedfalse for a password-protected post (its body is never exposed as schema — only the site-level nodes ship).
  • postNote — human note for draft/pending/scheduled (“not yet live”) or password-protected targets.
  • livePublic — whether the target is reachable at a public URL right now (published and non-gated), which gates URL-based validators.
  • graph — the JSON-LD document array (or null when there is nothing to emit).
  • json — the same graph as a pretty, slash-unescaped string for reading/validating (the live <head> escapes slashes for safe embedding; the data is identical).

An unpublished post is previewed with its would-be per-post node (built via Schema::build_document( $post, false, true )), flagged by postNote.

GET /preview/markdown

Returns the Markdown a page/post is served as — the .md twin / the Accept: text/markdown response. Markdown is per-page: the site target (post = 0) has no Markdown.

Parameters

Name In Type Default Notes
post query integer 0 absint-sanitized. 0/absent returns an empty site row.

A non-existent or out-of-scope post id returns WP_Error agentimus_preview_not_found (HTTP 404), same as the schema preview.

Response shape

{
  "active":       true,
  "reason":       "ok",
  "target":       { "type": "post", "id": 42, "label": "Hello world", "url": "https://example.com/hello-world/" },
  "postIncluded": true,
  "postNote":     "",
  "livePublic":   true,
  "markdown":     "# Hello world\n\n...",
  "mdUrl":        "https://example.com/hello-world.md"
}
  • active / reason — whether Markdown delivery (enable_markdown) is switched on ("ok" or "disabled").
  • Markdown is genuine served content (no “would-be” preview): a draft yields postIncluded: false and a postNote saying nothing is served yet; a password-protected post yields postIncluded: false and is never served as Markdown.
  • mdUrl mirrors the front-end resolution — the permalink with .md appended (untrailingslashit( $permalink ) . '.md').
  • For post = 0, the response returns the site target with postIncluded: false and a note that Markdown is per-page.

GET /preview/targets

Returns the pages and posts the preview can describe — the in-scope content, most-recently-modified first, optionally filtered by title.

Parameters

Name In Type Default Notes
search query string "" sanitize_text_field; passed to WP_Query’s s.

The query is limited to Content::post_types(), statuses publish, private, draft, pending, future, 50 rows, ordered by modified DESC.

Response shape

{
  "targets": [
    {
      "id": 42,
      "type": "post",
      "typeLabel": "Posts",
      "label": "Hello world",
      "status": "publish",
      "url": "https://example.com/hello-world/"
    }
  ]
}

Discovery read & validation routes

Registered separately in inc/Discovery/Module.php, still under agentimus/v1:

Route Method Auth Purpose
/discovery GET Public (__return_true) The live discovery envelope — the same array serialized to /.well-known/discovery.json. Records an activity hit (rest:discovery).
/discovery/hub GET manage_options The admin Discovery Hub payload (Hub::data()): resources, adapters, MCP surface, validation notices, counts.
/validate GET manage_options CI-friendly validation: { ok, resources, notices } where ok is false if any notice has level error.

GET /wp-json/agentimus/v1/discovery is the JSON-over-REST equivalent of the well-known document, useful for agents that prefer the WP REST root over /.well-known/.

Other admin route groups

Two further groups register under agentimus/v1; all require manage_options and the REST nonce. They are outside the scope of this page but listed for completeness:

  • Activity (inc/Activity/Module.php): GET/DELETE /activity, GET /activity/day (requires a date param, YYYY-MM-DD), POST /activity/block, POST /activity/allow.
  • AI Visibility (inc/Visibility/Rest.php, the opt-in monitoring add-on): GET/POST /visibility/config, GET /visibility/dashboard, POST /visibility/run, POST /visibility/test, POST /visibility/reveal-key, POST /visibility/clear.

Public discovery endpoints (/.well-known/*)

inc/Discovery/WellKnown.php is the single front controller for /.well-known/*. It is deliberately not greedy: it only routes the names it recognises. Anything else under /.well-known/ (ACME challenges, other plugins’ docs, unknown names) falls through to WordPress’s normal handling and 404s untouched.

How routing works

Two mechanisms bring a request to the router:

  1. Narrow rewrite rules (registered on init by WellKnown::add_rules(), flushed on activation) route only the exact names the plugin serves, so requests reach template_redirect even on servers that 404 the path at disk level. The routed names come from WellKnown::routed_names() — filterable via agentimus_well_known_routed.
  2. The 404-intercept path — a name that is not in the rewrite set (e.g. security.txt) still reaches WellKnown::route() through WordPress’s normal 404 template load, and the router serves it if it owns it.

Precedence inside route():

  • A real file on disk always wins. If <site-root>/.well-known/<name> exists (and resolves safely inside the .well-known directory), it is streamed as-is via readfile() — the plugin never shadows a real file.
  • Otherwise, one of the plugin’s generated documents is emitted.
  • Otherwise, a provider-registered document (via Registry::add_well_known()) is served — as a redirect, a streamed file, or a callback body.
  • If the plugin routed the request but produced nothing, maybe_clean_404() forces a clean 404 (rather than WordPress’s canonical redirect to the homepage).

Site-root resolution is handled by Agentimus\Paths::site_root() (the public document root that maps to home_url(), which is not always ABSPATH).

The routed documents

All generated documents are served by WellKnown::send(), which sets:

  • Content-Type: <type>; charset=UTF-8
  • X-Content-Type-Options: nosniff
  • Access-Control-Allow-Origin: *
  • Cache-Control: public, max-age=3600
Path (/.well-known/…) Content type Served when
discovery.json application/json Always (cached 1h).
agent-card.json application/json Always — the generated A2A agent card.
agent.json application/json Always — alias of agent-card.json.
mcp.json application/json Always — the experimental MCP/tools manifest.
openapi.json application/json Always — OpenAPI 3.1 description of the existing public REST read API.
api-catalog application/linkset+json Always — RFC 9727 API catalog as an RFC 9264 link set.
mcp/server-card.json application/json Only when a real MCP server is detected; otherwise 404.
mcp/{id}/server-card.json application/json Per-server card for a tool-bearing server matching {id}; else 404.
agent-skills/index.json application/json Only when executable skills exist; otherwise 404.
oauth-protected-resource application/json Only when an OAuth authorization server is configured (RFC 9728); else 404.
tdmrep.json application/json Only when enable_tdmrep is on and AI training is reserved; else 404.
http-message-signatures-directory application/json Only when response signing is enabled (the Web Bot Auth key directory); else 404.
security.txt text/plain Only when opted in, a Contact is configured, and no real file exists (see below).

discovery.json

The master index, built by Agentimus\Discovery\Envelope::build() and cached for an hour. It is a projection of the registry plus site identity — nothing is hand-maintained. The wire format is frozen at spec_version "1.0" and carries exactly these top-level keys, in order: $schema, spec_version, site, identity, documents, well_known, apis, agents, resources, capabilities, trust.

  • documents maps standard site documents (sitemap, robots, feed, openapi, and — when enabled — llms/llms_full and changes (the change feed), plus humans/security when present) to URLs. Filterable via agentimus_documents.
  • well_known indexes every /.well-known/* resource the site exposes (generated, provider-managed, and real files on disk), annotated with the governing spec label.
  • The whole envelope is filterable via agentimus_envelope; the JSON Schema URL via agentimus_schema_url.

Experimental surfaces (the MCP descriptor and tool list) are intentionally not in the frozen core — they are served at /.well-known/mcp.json and linked from well_known.

Change feed (/agentimus-changes.json)

A public JSON feed of recently changed content, served by the front controller (inc/Endpoints.php, not WellKnown) and advertised as documents.changes. Built by Agentimus\Changes, gated by the enable_changes setting (on by default), with a short Cache-Control: max-age=300.

  • GET /agentimus-changes.json returns the newest window of items (default 200, agentimus_changes_max), newest change first.
  • ?since=<ISO-8601 or Unix timestamp> filters to items changed strictly after that instant.
  • Each item is { id, type, title, url, modified, published, action, markdown, _links }, where action is created, updated or deleted and _links.rest points at the canonical wp/v2 resource. A deleted item — a tombstone recorded on unpublish/trash/delete and kept ~90 days (agentimus_tombstone_retain_days) — carries just the URL to evict and when.

agent-card.json / agent.json

The generated A2A agent card (Envelope::agent_card_json(), delegated to WellKnownDocs). agent.json is a straight alias for agents that look there first.

mcp.json and MCP server cards

Agentimus does not run an MCP server — it discovers and advertises the ones a shared MCP adapter library has registered (inc/Discovery/McpSurface.php). mcp.json carries the site identity, the MCP descriptor (available, source, endpoint, transport, auth, tools, servers, status: "experimental") and the deduped tool list.

When a live server is detected, standard single-server cards are served at mcp/server-card.json (the richest server, pinnable via agentimus_mcp_card_server) and, per server, at mcp/{id}/server-card.json. With no server, both return an empty body → a clean 404. The MCP descriptor is filterable via agentimus_mcp.

openapi.json

An OpenAPI 3.1 document describing the site’s existing public read API — WordPress’s REST API for the agent-indexed content types (inc/Discovery/OpenApi.php). It adds no endpoints and changes no behaviour; it is a machine-readable contract pointing at REST that already exists. One list + single-item read collection is generated per content type that is both agent-indexed and show_in_rest. It is also advertised from the HTML <head> as rel="service-desc".

api-catalog, oauth-protected-resource, tdmrep.json

  • api-catalog (RFC 9727, application/linkset+json) — the document complement to the rel="api-catalog" Link header; points agents at the site’s API descriptions.
  • oauth-protected-resource (RFC 9728) — served only when the owner has declared an auth server (oauth_auth_server); otherwise absent.
  • tdmrep.json — the W3C TDM Reservation Protocol opt-out file (inc/Tdmrep.php). Served only when enable_tdmrep is on and the site reserves its content from AI training (content_signal.ai_train is off). An “allow” site serves no file — absence already means “not reserved”.

Response signing (Web Bot Auth)

When signing is enabled (enable_signing, on by default but feature-detected against libsodium), the low-volume discovery JSON docs are signed with RFC 9421 Signature/Signature-Input headers over an RFC 9530 Content-Digest (inc/Discovery/Signer.php). The Ed25519 public key is published as a JWKS-style directory at /.well-known/http-message-signatures-directory. The signed surfaces default to discovery.json, agent-card.json, agent.json, mcp.json and are filterable via agentimus_signed_surfaces (keep it to JSON docs — never cached HTML/llms.txt).

security.txt

inc/Discovery/SecurityTxt.php is the reference example of the “extend, never override” contract. It generates an RFC 9116 security.txt (served text/plain) only when all of the following hold: the feature is opted in (enable_security_txt, off by default), at least one Contact is configured, no real /.well-known/security.txt file exists, and no other plugin already claimed the slot in the registry. It registers through the public wpdiscovery_register action at a late priority (99), so a first-class provider always wins. Because security.txt is intentionally absent from routed_names(), it is served via the 404-intercept path rather than a rewrite rule — which also means the CORS preflight (below) does not answer OPTIONS for it, though the actual GET still carries Access-Control-Allow-Origin: *.

CORS and preflight

Every generated document (and every streamed on-disk file) carries Access-Control-Allow-Origin: * on the GET/HEAD response — these docs are public by design, so browser-based agents can fetch them cross-origin.

For a strict cross-origin agent that sends a preflight, WellKnown::route() answers OPTIONSbut only for names the plugin actually serves (serves() = the routed names, the nested allow-list, or a per-server MCP card path). The preflight (WellKnown::preflight()) returns:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, HEAD, OPTIONS
Access-Control-Allow-Headers: *
Access-Control-Max-Age: 86400
Content-Length: 0

An OPTIONS for a name the plugin does not own is left entirely alone (never shadowed). Only GET/HEAD are otherwise handled; other methods return without action.

Advertising the discovery index

inc/Discovery/Module.php emits two Link headers on every front-end response so an agent can find the entry point from a single header:

Link: <https://example.com/.well-known/discovery.json>; rel="service-desc"; type="application/json"
Link: <https://example.com/.well-known/discovery.json>; rel="discovery"; type="application/json"

Front-end agent files

inc/Endpoints.php is the front controller for the non-.well-known agent files. It routes on template_redirect (priority 0), only for GET/HEAD and never for admin, feed, embed, REST, AJAX or XML-RPC requests. Bodies are emitted by Endpoints::send(), which sets X-Content-Type-Options: nosniff, Access-Control-Allow-Origin: *, and Vary: Accept.

llms.txt and llms-full.txt

Path Content type Gated by
/llms.txt text/plain enable_llms_txt
/llms-full.txt text/plain enable_llms_full

Both are stable URLs, so send() sets Cache-Control: public, max-age=3600. Content is assembled by Agentimus\LlmsText. The heavy full-text edition is re-warmed out-of-band after content changes via a debounced WP-Cron event (agentimus_warm_llms_full).

Markdown twins (.md and content negotiation)

Agentimus serves a Markdown representation of content two ways, both gated by enable_markdown and served as text/markdown:

  • Explicit .md URLs/<slug>.md, and /index.md (or a bare /.md) for the site index. The path is resolved back to a post via url_to_postid(); only posts whose type is in scope (Content::post_types()) are served, otherwise WordPress 404s normally.
  • Accept: text/markdown negotiation — on a resolved singular view (or the front page/archive/search, which map to the site index).

Because negotiated Markdown shares a URL with the HTML page, all Markdown responses are sent with Cache-Control: no-store, max-age=0 and Vary: Accept. The current page’s Markdown twin is advertised on singular, in-scope views as a Link header:

Link: <https://example.com/hello-world.md>; rel="alternate"; type="text/markdown"

Fallback sitemap

Agentimus never competes for the sitemap — inc/Sitemap.php detects the existing one (WordPress core, or a known SEO plugin) and links it. Only when nobody else provides one and enable_sitemap is on does it serve its own gap-filling sitemap:

Path Content type Notes
/agentimus-sitemap.xml application/xml The sitemap index.
/agentimus-sitemap-{type}-{page}.xml application/xml One sub-sitemap per content type, per page.

These are routed by Endpoints::route() and served only while Sitemap::detect()['source'] === 'agentimus'; any other /agentimus-sitemap* path 404s. As stable URLs they are cached (public, max-age=3600) and served with the standard Access-Control-Allow-Origin: *. Published, password-protected posts are excluded.

Endpoints also augments other standard surfaces without clobbering them:

  • robots.txt (via the robots_txt filter, priority 20) — injects a Content-Signal: directive into the existing User-agent: * group, appends a named model-training crawler blocklist, and adds a Sitemap: line if none is present. Gated by site visibility + enable_robots.
  • AI-usage headers (send_headers, priority 99) — when enable_ai_header is on and training is reserved, emits tdm-reservation: 1 (plus optional tdm-policy, and X-Robots-Tag: noai, noimageai when ai_noai_header is on). These are skipped on the “please read me” surfaces (llms.txt, llms-full.txt, robots.txt, .md, /.well-known/, feeds).
  • HTML <head> links (wp_head, priority 2) — mirrors rel="describedby" (/llms.txt) and rel="service-desc" (/.well-known/openapi.json) into the markup for crawlers that read HTML but not headers.
  • Discovery Link headers (send_headers, priority 99) — advertises rel="api-catalog" (the REST root), rel="describedby" (/llms.txt), and the current page’s rel="alternate" Markdown twin, each de-duplicated against any header a theme already set.

Coexistence: ceding a surface

Any of the front-end surfaces can be handed to another producer (a theme or plugin that emits its own llms.txt, Markdown, robots rules or Link headers) with a single filter, rather than sniffing for the plugin:

add_filter( 'agentimus_yield_surface', function ( $yield, $surface ) {
    // My theme already serves these — let it.
    return in_array( $surface, array( 'llms_txt', 'markdown' ), true ) ? true : $yield;
}, 10, 2 );

Valid $surface keys are llms_txt, llms_full, markdown, link_headers, and robots. When a surface is ceded, Agentimus stands down entirely for it — it neither serves the file nor advertises it.


Back to top

Built and maintained by Sheikh Heera. Agentimus is free software (GPL-2.0-or-later); this documentation is generated from the plugin source — if something here disagrees with the code, the code wins.

This site uses Just the Docs, a documentation theme for Jekyll.