REST routes and Abilities

Anansi registers three REST routes under the anansi/v1 namespace and two WordPress Abilities. The REST routes serve the block editor panels (link suggestions and Google Search Console data) and all need edit rights on the post they are about. The Abilities let AI tools and automation read and update a post’s SEO title, description, focus keyword and robots; a site owner can switch them off. Anansi’s _anansi_* meta is also registered with show_in_rest, in the edit context only.

REST routes

All routes check a real permission: the current user must be able to edit_post the post in the request. Use a nonce (X-WP-Nonce from wp_create_nonce( 'wp_rest' )) or an application password, as with any core route.

Internal link suggestions for a post or a draft. Registered by the links module.

ArgumentTypeRequiredDescription
post_idinteger (≥ 1)yesThe post being written.
titlestringnoDraft title. Default: the saved title.
contentstringnoDraft content (first 300,000 characters used). Default: the saved content.
tagsinteger[]noTag IDs. Default: the saved tags.
categoriesinteger[]noCategory IDs. Default: the saved categories.
focus_keywordstringnoDefault: the saved focus keyword.
limitinteger (1–20)noNumber of suggestions. Default: the “max suggestions” setting.
refreshbooleannoSkip the 5-minute result cache. Default false.

Permission: current_user_can( 'edit_post', $post_id ), else rest_forbidden (401 or 403).

Response:

KeyDescription
providerWhich engine answered (Ariadne, Voyage AI or the local index) and whether a meaning score was used.
itemsSuggestions, best first. Each: id, title, url, type (post type), score (0–100), why (reasons, strings), signals (semantic, words, taxonomy, keyword, title), pillar (bool), focus_keyword, anchor (suggested link text), anchor_source.
notesMessages for the user.
poolHow many candidate posts were scored.
cachedtrue when served from the cache.
tookMilliseconds (when not cached).
statusIndex status: published, indexed, embedded, Voyage usage and errors, Ariadne availability.
curl -u admin:APP_PASSWORD \
  "https://example.com/wp-json/anansi/v1/link-suggestions?post_id=123&limit=5"

When Voyage AI is set up, a request with draft content sends that text to Voyage AI. Each user can make at most 30 such draft requests in 10 minutes (filter anansi/links/voyage_user_rate).

GET|POST /wp-json/anansi/v1/schema/{post_id}

The post’s schema rows, used by the block editor’s schema builder.

  • Permission: edit_post on that post. POST also needs the wp_rest nonce in X-WP-Nonce (as wp.apiFetch sends it).
  • GET returns rows (each: id schema-{meta_id}, type, name, source anansi or rank_math, locked, spec, rich, custom, primary, missing and recommended field labels, and data, the stored row), types (all type specs) and post (published, url, testUrl for Google’s Rich Results Test).
  • POST body: { "rows": [ … ] }, the full list for the post. An item with only id keeps that row. An item with spec plus data (form mode, nested like the stored row) or json (custom JSON-LD, or "mode": "json") writes it; add id to replace an existing row, plus optional title and primary. Rows left out are removed. Default article and video rows (locked) are never changed. Rank Math rows are copy-on-write (see _anansi_schema_replaces in functions.md).
  • Values are cleaned by the type spec: unknown keys dropped, each value cleaned by its kind. Custom JSON-LD must parse, be one object with @type, and must not contain </script; otherwise the whole save is refused with 400 anansi_schema_invalid and nothing is written. Missing required fields only come back as warnings (row id => labels). A row id that no longer exists gives 409 anansi_schema_stale.
wp.apiFetch( {
    path: '/anansi/v1/schema/123',
    method: 'POST',
    data: { rows: [ { id: 'schema-456' }, { spec: 'person', title: 'Author', data: { name: '%title%', jobTitle: 'Singer' } } ] },
} );

Google Search Console

Registered by the google module. Both routes answer from a cache unless refresh=true, and return an error until Google is connected and a Search Console property is chosen.

GET /wp-json/anansi/v1/google/post/{id}

Search performance of the post’s URL: totals and its top queries.

POST /wp-json/anansi/v1/google/post/{id}/inspect

URL Inspection result for the post’s URL.

ArgumentTypeRequiredDescription
idinteger (≥ 1)yesPost ID (in the path).
refreshbooleannoSkip the cache. Default false.
daysintegernoDate range: 7, 28 (default), 90, 180, 365 or 480.

Permission: current_user_can( 'edit_post', $id ).

Errors:

CodeStatusWhen
anansi_google_not_ready409Google not connected or no property chosen.
anansi_google_unpublished409The post is not published.
anansi_google_outside409The post URL is not in the chosen property.
anansi_google_quota429Google’s quota is used up.
other Google errors502Google returned an error. data.detail has Google’s message; tokens are never included.

The success body is the report as the editor panel uses it. Its fields are not documented as stable yet.

Abilities

Anansi registers two abilities with the WordPress Abilities API (WordPress 6.9 and later). On older versions the module does nothing. They are in the category anansi-seo (label “SEO”), are registered on wp_abilities_api_init, and have show_in_rest set, so WordPress exposes them through its own abilities REST endpoints (wp-abilities/v1) and tools such as the MCP Adapter can use them.

On/off switch

The abilities are on by default. A site owner turns them off under Anansi SEO → General → AI abilities (“Let AI tools read and edit SEO fields”). The switch is stored in the anansi_abilities option:

array( 'enabled' => 'on' ) // or 'off'

When it is off, the abilities are not registered at all. In code:

update_option( 'anansi_abilities', array( 'enabled' => 'off' ) );

Permission

Both abilities use the same permission check: the input must have a post_id of an existing post, and the current user must have edit_post on that post. Values are written only to Anansi’s own _anansi_* meta, never to another plugin’s keys.

anansi/get-post-seo

Returns the SEO fields saved for a post, the title and description search engines get after templates and variables are applied, and the post’s schema rows.

Annotations: readonly: true, destructive: false, idempotent: true.

Input schema:

{
  "type": "object",
  "properties": {
    "post_id": { "type": "integer", "minimum": 1 }
  },
  "required": [ "post_id" ],
  "additionalProperties": false
}

Output schema (shared by both abilities):

{
  "type": "object",
  "properties": {
    "post_id":              { "type": "integer" },
    "title":                { "type": "string" },
    "description":          { "type": "string" },
    "focus_keyword":        { "type": "string" },
    "robots":               { "type": "array", "items": { "type": "string" } },
    "resolved_title":       { "type": "string" },
    "resolved_description": { "type": "string" },
    "changed":              { "type": "array", "items": { "type": "string" } },
    "schema":               { "type": "array", "items": { "type": "object", "properties": {
                              "type": { "type": "string" }, "title": { "type": "string" },
                              "source": { "type": "string", "enum": [ "anansi", "rank_math" ] } } } }
  }
}
  • title, description, focus_keyword, robots: the stored values, read through as Anansi does (_anansi_*, then Rank Math’s keys, then anansi/meta/sources).
  • resolved_title, resolved_description: plain text after variables are replaced. When the post has no title of its own, the post type template is used (or %title% %sep% %sitename%).
  • changed: only in the update result.
  • schema: the schema rows stored on the post (read-only), as the front end reads them: Rank Math rows replaced in Anansi’s schema editor are left out. Writing rows through abilities is planned for a later release; use the REST route above.

Errors: anansi_no_post (404) when the post does not exist.

anansi/update-post-seo

Sets a post’s SEO title, meta description, focus keyword and/or robots. Only the fields sent are changed. An empty string clears a text field, so the site template is used again. Titles and descriptions may use variables such as %title%, %sep% and %sitename%.

Annotations: readonly: false, destructive: false, idempotent: true.

Input schema:

{
  "type": "object",
  "properties": {
    "post_id":       { "type": "integer", "minimum": 1 },
    "title":         { "type": "string", "maxLength": 300 },
    "description":   { "type": "string", "maxLength": 1000 },
    "focus_keyword": { "type": "string", "maxLength": 300 },
    "robots": {
      "type": "array",
      "items": { "type": "string", "enum": [ "index", "noindex", "nofollow", "noarchive", "nosnippet", "noimageindex" ] },
      "uniqueItems": true
    }
  },
  "required": [ "post_id" ],
  "additionalProperties": false
}

Rules:

  • focus_keyword is comma-separated.
  • robots: an empty array goes back to the site default. When the list has neither index nor noindex, index is added. nosnippet also keeps the page out of Google AI Overviews.
  • When the editor module is loaded, its save rules apply, so an untouched Rank Math value keeps being read through and a cleared field stops the read-through, as in the editor.

Output: the output schema above, with changed listing the field keys that changed.

Errors:

CodeStatusWhen
anansi_no_post404No post with that ID.
anansi_invalid_robots400robots is not a list of the allowed values.
anansi_invalid_field400A text field is not a string.

After a change Anansi clears the post cache and fires anansi/abilities/post_updated with the post ID and the changed keys.

Calling them from PHP

if ( function_exists( 'wp_get_ability' ) ) {
    $ability = wp_get_ability( 'anansi/update-post-seo' );
    if ( $ability ) {
        $result = $ability->execute(
            array(
                'post_id'     => 123,
                'description' => 'How to bake bread at home, step by step.',
                'robots'      => array( 'index', 'nofollow' ),
            )
        );
        if ( is_wp_error( $result ) ) {
            error_log( $result->get_error_message() );
        }
    }
}

The permission check runs for the current user, so run this in a request where a user who can edit the post is logged in.

Meta in the core REST API

The editor module registers each _anansi_* post, term and user key with register_meta() and show_in_rest in the edit context only. So the values appear under meta in /wp/v2/posts/{id}?context=edit (and for terms and users) for users who can edit that object, and they can be written through the same endpoints. They do not appear in public (view) responses. See functions.md for the keys and formats.