Skip to content

Editor, AI and setup endpoints ​

These routes serve the editor sidebar, the setup wizard and the support drawer. They also include the public internal click route and the two WordPress search types that list short links and affiliates.

Sign-in, the envelope and errors are in the REST API overview.

Paths are relative to /wp-json/link-manager-pro/v1, except the search routes, which belong to WordPress core. These routes serve the plugin's own screens, so their fields can change.

Every short URL these routes return follows the prefix rules. A link to one of your affiliates' domains gets the Affiliate link prefix when one is set, for example https://example.com/recommends/amazon-deal. A link to your own site follows Mask internal links and the Internal link prefix. All others use the Short link prefix. See General settings.

The four /ai routes serve the sidebar that finds plain URLs in a post and suggests internal links. They use the permission level that allows edit_posts (the capability the link_manager_pro_picker_capability filter returns) or link_manager_pro_access (Permissions).

detect-urls, dismiss-url and analyze also check edit_post on the post you name. A user who cannot edit that post gets 403 with the code lmp_ai_post_forbidden. A missing or zero post_id skips that check, so an unsaved post works.

Find plain URLs in post content ​

POST /ai/detect-urls

Lists the web addresses in the content that are not yet short links, so the sidebar can offer to convert them.

Permission: edit_posts or link_manager_pro_access, and edit_post on post_id when it is set (Permissions)

Body parameters: (JSON)

NameTypeRequiredDefaultWhat it does
contentstringNoemptyThe post content, as HTML or text. The route reads href links and bare http and https addresses.
post_idintegerNo0The post being edited. Addresses you dismissed for this post are left out.

The route skips addresses of this site's own short links, core/embed blocks themselves (nested ones included), so the same address as a plain link elsewhere in the post is still returned, and addresses under its /wp-admin/, /wp-content/, /wp-includes/ and /wp-json/ paths. It returns at most 50 addresses, in the order found.

Response: 200. data holds:

FieldTypeWhat it holds
enabledbooleanfalse when the setting ai.enable_untracked_detection is off. It is on by default. Then urls is empty.
urlsarrayOne object per address, fields below.

Each object in urls:

FieldTypeWhat it holds
urlstringThe address.
is_internalbooleantrue for an address on this site (the www. prefix is ignored), false for any other host.
bash
curl -X POST \
  -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{"post_id":123,"content":"<p>See <a href=\"https://example.net/deal\">this deal</a>.</p>"}' \
  "https://example.com/wp-json/link-manager-pro/v1/ai/detect-urls"
json
{
  "success": true,
  "data": {
    "enabled": true,
    "urls": [
      { "url": "https://example.net/deal", "is_internal": false }
    ]
  }
}

Errors:

StatusWhen
403The user cannot edit the post in post_id. The code is lmp_ai_post_forbidden.

POST /ai/convert-url

Creates a short link for one address, or returns the one that already exists for it.

Permission: edit_posts or link_manager_pro_access (Permissions). This route has no per-post check. To create a new short link, a caller without full access also needs the create right: publish_posts by default, or the capability the link_manager_pro_picker_create_capability filter returns.

Body parameters: (JSON)

NameTypeRequiredDefaultWhat it does
urlstringYesnoneThe destination. It must be a valid http or https address, at most 2048 characters.

The new link is stored with the source ai. When a short link for the address exists, the route returns that link and creates nothing. Without the create right and with no existing link, the route returns 403 with the code lmp_cannot_create_links and the message "Your role can insert existing short links but can’t create new ones. Ask an editor or administrator."

Response: 200. data holds only the fields an editor may see:

FieldTypeWhat it holds
linkobjectThe link: id (integer) and slug (string).
masked_urlstringThe public short URL to put in the post, under the prefix that fits the destination.
json
{
  "success": true,
  "data": {
    "link": { "id": 42, "slug": "blue-widget" },
    "masked_url": "https://example.com/link/blue-widget"
  }
}

Errors:

StatusWhen
422url is missing, not a valid address, over 2048 characters, or not http or https (the message is That is not a valid http or https URL.). The link could not be created. The body lists the field url.

Dismiss a URL for a post ​

POST /ai/dismiss-url

Remembers that you do not want one address offered again for one post.

Permission: edit_posts or link_manager_pro_access, and edit_post on post_id (Permissions)

Body parameters: (JSON)

NameTypeRequiredDefaultWhat it does
urlstringYesnoneThe address to hide. Case, a trailing slash and a default port do not matter.
post_idintegerYes0The post. It must be above 0.

The plugin keeps the last 200 dismissed addresses per post in the post meta key _lmp_ai_dismissed_urls.

Response: 200. data holds:

FieldTypeWhat it holds
dismissedbooleanAlways true.

Errors:

StatusWhen
422url is empty, or post_id is 0 or less. The message is A URL and post ID are required.
403The user cannot edit the post. The code is lmp_ai_post_forbidden.

POST /ai/analyze

Asks the configured AI provider which phrases in the content should link to which of your short links.

Permission: edit_posts or link_manager_pro_access, and edit_post on post_id when it is set (Permissions)

Body parameters: (JSON)

NameTypeRequiredDefaultWhat it does
contentstringYesemptyThe post content. Tags are removed and the text is cut to 8000 characters. Content that is empty once tags are removed returns empty, described below.
post_idintegerNo0The post. Used for the permission check only.
selectionstringNoemptyText the user selected. Suggestions that match it rank first. Leave it empty to analyze the whole post.

The route works in this order. Each early stop is a 200 answer with a plain reason, not an error status:

  1. The setting ai.enable_suggestions is off (the default): data is { "enabled": false }.
  2. The AI Link Assistant is not available on this site: { "available": false, "reason": "entitlement" }.
  3. No AI provider is set up: { "available": false, "reason": "provider" }.
  4. The content is empty once tags are removed: { "available": true, "enabled": true, "suggestions": [], "empty": true }. Nothing is cached and no rate-limit slot is used.
  5. The same content, selection, token budget, provider and model was analyzed in the last hour: the cached result, with cached set to true.
  6. No short link matches the content: the answer has no suggestions and total_candidates is 0. The plugin makes no provider call and uses no rate-limit slot. It caches this answer for one hour.
  7. Otherwise the plugin checks the rate limit, calls the provider, and caches the result for one hour.

The candidates are live, public short links that match the main words of the content. The plugin sends up to 50 of them to the provider and returns at most 8 suggestions, best first.

Response: 200. A successful analysis holds:

FieldTypeWhat it holds
availablebooleantrue.
enabledbooleantrue.
suggestionsarrayUp to 8 objects, fields below.
total_candidatesintegerHow many short links matched and were offered to the provider.
cachedbooleantrue when served from the one-hour cache.
emptybooleanOnly on the empty-content answer, where it is true and suggestions is empty.

Each suggestion:

FieldTypeWhat it holds
link_idintegerThe short link ID.
anchorstringA phrase copied from the content. The route keeps only phrases that appear in it.
slugstringThe link's slug.
target_urlstringThe link's destination.
masked_urlstringThe public short URL.
click_countintegerThe link's click total. Left out for users without link_manager_pro_access.
best_matchbooleantrue for the first suggestion only.

When the provider fails or its answer cannot be read, data is { "available": true, "error": "..." } with a plain message and no suggestions.

json
{
  "success": true,
  "data": {
    "available": true,
    "enabled": true,
    "suggestions": [
      {
        "link_id": 42,
        "anchor": "blue widget",
        "slug": "blue-widget",
        "target_url": "https://example.com/products/blue-widget",
        "masked_url": "https://example.com/link/blue-widget",
        "click_count": 128,
        "best_match": true
      }
    ],
    "total_candidates": 6,
    "cached": false
  }
}

Errors:

StatusWhen
403The user cannot edit the post in post_id (lmp_ai_post_forbidden).
429The user made too many uncached requests. The code is lmp_ai_rate_limited.

A cached answer does not count toward the limit, and neither does a call with no matching short link. The limit is per user: by default 30 requests in 300 seconds, set by the settings ai.rate_limit_max (0 to 100000) and ai.rate_limit_window (1 to 86400 seconds). The link_manager_pro_ai_rate_max filter can change the limit. A limit of 0 turns it off.

Setup ​

The setup wizard keeps one state object in the option link_manager_pro_onboarding. Four steps make it up: welcome, settings, privacy and scan. Each is pending or complete. When all four are complete, status becomes complete.

The state object that these routes return:

FieldTypeWhat it holds
statusstringpending, complete or skipped.
stepintegerThe current step, 0 to 3.
stepsobjectwelcome, settings, privacy and scan, each pending or complete.
updated_atstringWhen the state was last saved, as an ISO 8601 date in UTC.
completed_atstringOnly after POST /onboarding/complete.
skipped_atstringOnly after POST /onboarding/skip.

The first four routes below need link_manager_pro_access. settings, complete and skip need an administrator (manage_options).

Get the setup state ​

GET /onboarding

Returns the setup state and the settings the wizard shows.

Permission: link_manager_pro_access (Permissions)

Query parameters: None.

Response: 200. data is the state object above plus:

FieldTypeWhat it holds
settingsobjectThe groups general, tracking and redirection. An administrator gets every key of each group. Other users get only general.link_prefix, redirection.redirect_type, and tracking.track_clicks, tracking.enable_bot_detection and tracking.anonymize_ips.
json
{
  "success": true,
  "data": {
    "status": "pending",
    "step": 0,
    "steps": { "welcome": "pending", "settings": "pending", "privacy": "pending", "scan": "pending" },
    "updated_at": "2026-10-23T09:00:00+00:00",
    "settings": {
      "general": { "link_prefix": "link" },
      "tracking": { "track_clicks": "yes", "enable_bot_detection": "yes", "anonymize_ips": "yes" },
      "redirection": { "redirect_type": 301 }
    }
  }
}

The settings values above are examples. Each group holds more keys for an administrator.

Reset the setup ​

POST /onboarding/status

Starts setup over. It replaces the state with a fresh pending one. Saved settings stay.

Permission: link_manager_pro_access (Permissions)

Body parameters: (JSON)

NameTypeRequiredDefaultWhat it does
statusstringYesemptyMust be pending.

Response: 200. data is the fresh state object, with status pending, step 0 and every step pending.

Errors:

StatusWhen
422status is anything but pending. The body lists the field status.

Mark a step complete ​

POST /onboarding/step

Marks one step complete and moves step to it.

Permission: link_manager_pro_access (Permissions)

Body parameters: (JSON)

NameTypeRequiredDefaultWhat it does
stepintegerYesnoneThe step number, 0 to 3: 0 welcome, 1 settings, 2 privacy, 3 scan.
statestringYesemptyMust be complete.

Response: 200. data is the updated state object. When all four steps are complete, status is complete.

Errors:

StatusWhen
422step is not a number from 0 to 3 (field step), or state is not complete (field state).

Scan posts in chunks ​

POST /onboarding/scan

Turns the external links of one chunk of posts into short links. It saves those posts.

This route changes post content. It edits published, scheduled and private posts, and leaves drafts alone. Call it again with the new offset until hasMore is false. A chunk stops after about 20 seconds of work.

Permission: link_manager_pro_access (Permissions)

Body parameters: (JSON)

NameTypeRequiredDefaultWhat it does
offsetintegerNo0How many posts to skip. Use nextOffset from the last answer. A negative value counts as 0.
limitintegerNo20Posts per chunk. A value above 100 counts as 100.

Response: 200. data holds:

FieldTypeWhat it holds
statusstringin_progress or completed.
processedintegerPosts scanned so far, counting from the first post.
nextOffsetintegerThe offset for the next call.
totalintegerPosts the scan covers.
hasMorebooleantrue when another call is needed.
time_limitedbooleantrue when the 20 second budget stopped the chunk early.
linksCreatedintegerNew short links created in this chunk.
linksConvertedintegerLinks rewritten into short links in this chunk.
postsConvertedintegerPosts where at least one link was converted.
postsUpdatedintegerPosts saved.
postsSkippedintegerPosts left as they were, for example when WordPress would not allow the save.
postsFailedintegerPosts that could not be processed.
linksKeptForBlocksintegerLinks left as written because their block keeps the address.

When hasMore is false, the scan step is marked complete. When all four steps are complete, status of the setup becomes complete.

json
{
  "success": true,
  "data": {
    "status": "in_progress",
    "processed": 20,
    "nextOffset": 20,
    "total": 140,
    "hasMore": true,
    "time_limited": false,
    "linksCreated": 12,
    "linksConverted": 15,
    "postsConverted": 6,
    "postsUpdated": 6,
    "postsSkipped": 0,
    "postsFailed": 0,
    "linksKeptForBlocks": 1
  }
}

Save the setup settings ​

POST /onboarding/settings

Saves the General, Tracking and Redirection settings from the wizard.

Permission: manage_options (Permissions)

Body parameters: (JSON)

NameTypeRequiredDefaultWhat it does
generalobjectNononeKeys of the general settings group, such as link_prefix. A group sent empty or left out is not saved.
trackingobjectNononeKeys of the tracking group.
redirectionobjectNononeKeys of the redirection group.

Each group follows the same schema as its settings screen. Values are sanitized, and keys the group does not define are dropped. affiliate_prefix is optional and empty by default. The prefix rules for link_prefix, internal_prefix and affiliate_prefix run first. A broken prefix rejects the whole request and saves nothing. The wizard pre-fills affiliate_prefix from affiliate_prefix_suggestion of Detect other plugins.

Saving general or redirection marks the settings step complete. Saving tracking marks privacy complete.

Response: 200. data holds:

FieldTypeWhat it holds
stateobjectThe updated state object.
warningsobjectPrefix warnings keyed general.<key>, each a plain sentence. Empty when none.
json
{
  "success": true,
  "data": {
    "state": {
      "status": "pending",
      "step": 1,
      "steps": { "welcome": "pending", "settings": "complete", "privacy": "pending", "scan": "pending" },
      "updated_at": "2026-10-23T09:05:00+00:00"
    },
    "warnings": {}
  }
}

Errors:

StatusWhen
422A submitted prefix breaks a rule. The body lists each field as general.link_prefix, general.internal_prefix or general.affiliate_prefix.

Finish the setup ​

POST /onboarding/complete

Closes setup for the site, whichever steps were used.

Permission: manage_options (Permissions)

Body parameters: None.

Response: 200. data is the state object with status complete, step 3, the scan step complete and completed_at set.

Skip the setup ​

POST /onboarding/skip

Closes setup without finishing it. Saved settings stay, and defaults cover the rest.

Permission: manage_options (Permissions)

Body parameters: None.

Response: 200. data is the state object with status skipped and skipped_at set.

System and terms ​

Get system information ​

GET /system-info

Returns the support details the Help drawer shows: environment, themes and plugins. It holds no secrets and no personal data.

Permission: manage_options (Permissions)

Query parameters: None.

Response: 200. data holds three objects.

environment, all values strings:

FieldTypeWhat it holds
plugin_versionstringThe Link Manager Pro version.
license_statestringThe license state, inactive when none is known.
wordpress_versionstringThe WordPress version.
php_versionstringThe PHP version.
mysql_versionstringThe database server version, or an empty string.
memory_limitstringThe WordPress memory limit, such as 256M.
max_upload_sizestringThe largest upload WordPress takes, such as 64 MB.
server_softwarestringThe web server name from SERVER_SOFTWARE, or an empty string.
multisitestringYes or No.
localestringThe site locale, such as en_US.

themes:

FieldTypeWhat it holds
activestringThe active theme's name and version.
totalintegerInstalled themes.
itemsarrayObjects with name (string), version (string) and active (boolean). The active theme comes first, then the rest by name.

plugins:

FieldTypeWhat it holds
active_countintegerActive plugins. On multisite this includes network-active ones.
totalintegerInstalled plugins.
itemsarrayObjects with name (string), version (string) and active (boolean). Active plugins come first, then the rest by name.
json
{
  "success": true,
  "data": {
    "environment": {
      "plugin_version": "1.3.0",
      "license_state": "active",
      "wordpress_version": "6.8",
      "php_version": "8.2.12",
      "mysql_version": "8.0.36",
      "memory_limit": "256M",
      "max_upload_size": "64 MB",
      "server_software": "nginx/1.25.3",
      "multisite": "No",
      "locale": "en_US"
    },
    "themes": {
      "active": "Twenty Twenty-Five 1.0",
      "total": 2,
      "items": [ { "name": "Twenty Twenty-Five", "version": "1.0", "active": true } ]
    },
    "plugins": {
      "active_count": 3,
      "total": 4,
      "items": [ { "name": "Link Manager Pro", "version": "1.3.0", "active": true } ]
    }
  }
}

List post categories and tags ​

GET /wp-terms

Lists WordPress post categories and tags. The auto-link targeting picker uses it.

Permission: link_manager_pro_access (Permissions)

Query parameters: None.

Response: 200. data is an array, not a page. It holds up to 500 categories, then up to 500 tags, each group ordered by name. Empty terms are included.

FieldTypeWhat it holds
idintegerThe term ID.
namestringThe term name.
taxonomystringcategory or post_tag.
groupstringThe group label, Categories or Tags, translated.
json
{
  "success": true,
  "data": [
    { "id": 3, "name": "Guides", "taxonomy": "category", "group": "Categories" },
    { "id": 9, "name": "Reviews", "taxonomy": "post_tag", "group": "Tags" }
  ]
}

Internal click tracking ​

Record an internal click ​

POST /track/internal-click

Records one click on an ordinary link inside a post, from a visitor's browser. It is public and answers { "ok": true } even when it records nothing.

Permission: none. The route checks its own guards, below (Permissions).

The route runs these checks in this order:

  1. The settings tracking.track_clicks and tracking.track_internal_clicks must both be yes. The first is on by default, the second is off. When either is off, the route answers { "ok": true } and stops.
  2. A source_post_id of 0 or less gives 403 with the code invalid_nonce. For a logged-in visitor, lmp_nonce must also be a valid nonce for this post, created for the action lmp_internal_click_{source_post_id}; a missing or wrong one gives the same 403. A logged-out visitor sends no nonce.
  3. The request must come from this site. The route reads the Origin header, or Referer when there is no Origin. Its host must equal the site host, ignoring a leading www.. A missing header or another host gives 403 with the code invalid_origin.
  4. One visitor address may send 30 requests per minute for one post. Past that, the route answers { "ok": true } and records nothing.
  5. An empty target_url is ignored.

After the guards, the plugin still skips the click when the visitor is a filtered bot, the address is not on this site, the address is one of this site's own short links or a link's other address that opens its link, or the source post is not published.

The front-end script gets the nonce from the page. On a single post or page of an enabled post type, the plugin loads lmp-tracker.js and sets the object lmpTrackerConfig with endpoint, postId, siteHost, homePath, selector and maskedPrefixes. Only for a logged-in visitor it also holds restNonce and nonce; the nonce value is the post-scoped nonce.

Body parameters: (form data, as navigator.sendBeacon sends it, or JSON)

NameTypeRequiredDefaultWhat it does
source_post_idintegerYes0The post that holds the link.
target_urlstringYesemptyThe clicked address, as the browser resolved it.
anchor_textstringNoemptyThe link text. The script sends up to 500 characters.
lmp_noncestringLogged-in visitors onlynoneThe post-scoped nonce from lmpTrackerConfig.nonce.
_wpnoncestringNononeThe wp_rest nonce from lmpTrackerConfig.restNonce. Only for a logged-in visitor, and WordPress core checks it.

The script sends a click only for links on this site inside the content selector (default .entry-content), skips links under this site's own prefixes (link, internal and affiliate) and the previous prefixes the router still answers, and skips links with the attribute data-lmp-notrack.

Response: 200. data holds:

FieldTypeWhat it holds
okbooleanAlways true.
json
{
  "success": true,
  "data": { "ok": true }
}

Errors:

StatusWhen
403The nonce of a logged-in visitor is missing or wrong (invalid_nonce), or the origin is not this site (invalid_origin).

The block editor and the classic link dialog find short links and affiliates through the WordPress core search route, GET /wp-json/wp/v2/search. The plugin adds two values for its type parameter. These are core routes, so core's rules apply: the route is public, and the results come back as a plain JSON array without the Link Manager Pro envelope.

Both types return nothing, an empty array, unless the visitor is signed in and has the capability the link_manager_pro_picker_capability filter returns (edit_posts by default). The link_manager_pro_picker_enabled filter can turn them off. Search is matched on the slug and keywords. A link's destination matches only for links without a password.

GET /wp-json/wp/v2/search?type=link-manager-pro

Lists short links for the editor's link picker, newest first.

Permission: a signed-in user who can edit_posts, by default. The route is open to every visitor, and it returns an empty list to anyone else.

Query parameters:

NameTypeRequiredDefaultWhat it does
typestringYesnonelink-manager-pro.
searchstringNoemptyMatches the slug or a keyword. With no password, it matches the destination too. Empty returns every eligible link.
pageintegerNo1The page number.
per_pageintegerNo10Results per page. Core's default.

The route leaves out links that back a Simple Redirect, and expired links by default (the link_manager_pro_picker_excluded_statuses filter changes that).

Response: 200. A JSON array. The headers X-WP-Total and X-WP-TotalPages give the counts. Each item:

FieldTypeWhat it holds
idintegerThe short link ID.
titlestringThe slug.
urlstringThe public short URL.
typestringlink-manager-pro.
bash
curl -u USER:APPLICATION_PASSWORD \
  "https://example.com/wp-json/wp/v2/search?type=link-manager-pro&search=widget&per_page=5"
json
[
  {
    "id": 42,
    "title": "blue-widget",
    "url": "https://example.com/link/blue-widget",
    "type": "link-manager-pro"
  }
]

Search affiliates ​

GET /wp-json/wp/v2/search?type=link-manager-pro-affiliate

Lists affiliates for the editor's link picker, newest first.

Permission: the same as short link search. It also returns nothing when the link_manager_pro_picker_include_affiliates filter returns false.

Query parameters: the same as Search short links, with type set to link-manager-pro-affiliate. search matches an affiliate the way the Affiliates list does.

Response: 200. A JSON array with the same headers. Each item:

FieldTypeWhat it holds
idintegerThe affiliate ID.
titlestringThe site name, else the domain, else the affiliate link.
urlstringThe tracked short URL for the affiliate. An affiliate with no link returns an empty string.
typestringlink-manager-pro-affiliate.

The url is always the tracked short link, never the raw affiliate address. When the affiliate has no short link yet, the route creates one while it builds the result, but only for a user who can create short links. For a user who can't, that affiliate is left out of the results.

json
[
  {
    "id": 7,
    "title": "Example Shop",
    "url": "https://example.com/link/example-shop",
    "type": "link-manager-pro-affiliate"
  }
]

The link_manager_pro_picker_item filter can change each item before it is returned.