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.
GET|POST /wp-json/anansi/v1/link-suggestions
Internal link suggestions for a post or a draft. Registered by the links module.
| Argument | Type | Required | Description |
|---|---|---|---|
post_id | integer (≥ 1) | yes | The post being written. |
title | string | no | Draft title. Default: the saved title. |
content | string | no | Draft content (first 300,000 characters used). Default: the saved content. |
tags | integer[] | no | Tag IDs. Default: the saved tags. |
categories | integer[] | no | Category IDs. Default: the saved categories. |
focus_keyword | string | no | Default: the saved focus keyword. |
limit | integer (1–20) | no | Number of suggestions. Default: the “max suggestions” setting. |
refresh | boolean | no | Skip the 5-minute result cache. Default false. |
Permission: current_user_can( 'edit_post', $post_id ), else rest_forbidden (401 or 403).
Response:
| Key | Description |
|---|---|
provider | Which engine answered (Ariadne, Voyage AI or the local index) and whether a meaning score was used. |
items | Suggestions, 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. |
notes | Messages for the user. |
pool | How many candidate posts were scored. |
cached | true when served from the cache. |
took | Milliseconds (when not cached). |
status | Index 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_poston that post.POSTalso needs thewp_restnonce inX-WP-Nonce(aswp.apiFetchsends it). - GET returns
rows(each:idschema-{meta_id},type,name,sourceanansiorrank_math,locked,spec,rich,custom,primary,missingandrecommendedfield labels, anddata, the stored row),types(all type specs) andpost(published,url,testUrlfor Google’s Rich Results Test). - POST body:
{ "rows": [ … ] }, the full list for the post. An item with onlyidkeeps that row. An item withspecplusdata(form mode, nested like the stored row) orjson(custom JSON-LD, or"mode": "json") writes it; addidto replace an existing row, plus optionaltitleandprimary. Rows left out are removed. Default article and video rows (locked) are never changed. Rank Math rows are copy-on-write (see_anansi_schema_replacesin 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 with400 anansi_schema_invalidand nothing is written. Missing required fields only come back aswarnings(row id => labels). A row id that no longer exists gives409 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.
| Argument | Type | Required | Description |
|---|---|---|---|
id | integer (≥ 1) | yes | Post ID (in the path). |
refresh | boolean | no | Skip the cache. Default false. |
days | integer | no | Date range: 7, 28 (default), 90, 180, 365 or 480. |
Permission: current_user_can( 'edit_post', $id ).
Errors:
| Code | Status | When |
|---|---|---|
anansi_google_not_ready | 409 | Google not connected or no property chosen. |
anansi_google_unpublished | 409 | The post is not published. |
anansi_google_outside | 409 | The post URL is not in the chosen property. |
anansi_google_quota | 429 | Google’s quota is used up. |
| other Google errors | 502 | Google 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, thenanansi/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_keywordis comma-separated.robots: an empty array goes back to the site default. When the list has neitherindexnornoindex,indexis added.nosnippetalso 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:
| Code | Status | When |
|---|---|---|
anansi_no_post | 404 | No post with that ID. |
anansi_invalid_robots | 400 | robots is not a list of the allowed values. |
anansi_invalid_field | 400 | A 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.