Appearance
Link Health, Audit and Equity endpoints
These REST routes list and fix broken links, audit internal links, report link equity and mask links in content. Each entry lists the parameters, the response fields and the errors.
Sign-in, the response envelope and paging are in the REST API overview.
All paths are relative to /wp-json/link-manager-pro/v1. Examples use https://example.com and USER:APPLICATION_PASSWORD. Inside WordPress, send the X-WP-Nonce header instead.
A validation failure is a 422 with message and an errors object that lists messages per field. A missing record is a 404. The scans run in batches, so a caller repeats one route until the response says it is finished.
The Link Health item
One Link Health item is one broken address. A content item is a broken link found in a post. A traffic item is a visitor 404 on your own site.
The list and the single-item routes use these fields. Dates are in UTC.
| Field | Type | What it holds |
|---|---|---|
id | integer | The item ID. |
url | string | The broken address. |
source_post_id | integer or null | The post the link was found in. null for most traffic items. |
health_type | string | content or traffic. |
status_code | integer or null | The HTTP status of the last check, or null when there was no answer. |
referrer | string or null | The page a visitor came from. Set on traffic items. |
hits | integer | How many times the problem was seen. Starts at 1. |
last_detected | string | When it was last seen, for example 2026-10-20T08:30:00.000. |
status | string | active, fixed or ignored. |
created_at | string | When the item was first stored. |
updated_at | string | When the item last changed. |
post_title | string | The title of the source post. Empty when there is none. |
post_url | string | The permalink of the source post. Empty when there is none. |
post_edit_url | string | The editor link of the source post. Empty when there is none. |
referrer_host | string | The host of referrer without www.. Empty when there is no referrer. |
via_links | array | Up to 5 of your short links whose destination is this address. Each has id, slug and short_path, the address under the prefix that link uses (the affiliate prefix for a link to an affiliate's domain, when it is set). Content items only. |
redirect_link_id | integer | The short link a redirect for this address leads to. Only present when a redirect exists. |
redirect_status | string | The redirect's status. Only present when a redirect exists. |
redirect_slug | string | The slug of that short link. Only present when a redirect exists. |
redirect_url | string | The full short address. Only present when a redirect exists. |
redirect_path | string | The short address without the host. Only present when a redirect exists. |
redirect_target_url | string | Where that short link leads. Only present when a redirect exists. |
Items of unpublished or trashed posts, and items of content types you did not choose for scanning, are hidden from every list, count and clear. They come back when the post is published again.
Link Health
Everything the Link Health screen does is a route here. The scan checks published posts one batch at a time.
How a scan runs. Call POST /link-health/scan with offset 0. The response carries a scanId and a nextOffset. Call the route again with that nextOffset as offset and the same scanId as scan_id. Repeat until nextOffset is null. The last batch records the finished scan, which then shows as last_scan in the list. To stop, do not ask for the next batch.
Each batch checks up to 25 posts, within a 15 second budget. A batch can scan fewer posts than the limit when time runs out, so always continue from nextOffset. A rerun within an hour of a scan that left links unchecked reuses the links that scan already checked.
List Link Health items
GET /link-health
Returns one page of broken-link items, newest detection first, with the counts that drive the filters.
Permission: link_manager_pro_access (Permissions)
Query parameters:
| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
type | string | No | all | content or traffic. Any other value, or all, returns both. |
status | string | No | active | active, fixed, ignored or all. Any other value counts as active. |
search | string | No | none | Matches the address, the referrer, or the title of the source post. |
page | integer | No | 1 | The page number. |
per_page | integer | No | 15 | Items per page. |
Response: 200. data is a page object (Pagination) whose data.data rows are Link Health items. It carries four more fields:
| Field | Type | What it holds |
|---|---|---|
counts | object | Counts across all pages, fields below. |
last_scan | object or null | The last finished content scan, or null before the first. |
scan_types | array | The content types a scan checks, each { "name": "post", "label": "Posts" }. |
sitemap | object | How visitor 404s are filtered, fields below. |
counts holds these integers:
| Field | What it counts |
|---|---|
needs_attention | Active items of both types. |
content | Active content items. |
traffic | Active traffic items. |
fixed | Fixed items of both types. |
ignored | Ignored items of both types. |
all | Every item, whatever its status. |
scope | An object { "type": "", "all": 12 }: every item in the tab chosen by type, any status, search not applied. This is what Clear Link Health items removes. |
status | An object { "active": 9, "fixed": 2, "ignored": 1 }: items in the tab, honoring search. |
last_scan holds posts (integer, posts in the scan), finished_at (string, UTC Y-m-d H:i:s), checked (integer, links checked), unchecked (integer, links left over by a cap or the time limit) and started_at (integer, Unix seconds, 0 for a scheduled scan). A scan with any unchecked links was not fully checked.
sitemap holds mode (string, sitemap or all, from the Track visitor 404s setting), status (object or null) and listed (boolean). status has state (ok, missing or unreadable), checked_at (integer, Unix seconds), and for ok also url and count. listed is false when no sitemap list is cached.
bash
curl -u USER:APPLICATION_PASSWORD \
"https://example.com/wp-json/link-manager-pro/v1/link-health?type=content&status=active"json
{
"success": true,
"data": {
"current_page": 1,
"data": [
{
"id": 17,
"url": "https://example.com/old-guide",
"source_post_id": 42,
"health_type": "content",
"status_code": 404,
"referrer": null,
"hits": 2,
"last_detected": "2026-10-20T08:30:00.000",
"status": "active",
"created_at": "2026-10-18T06:00:00.000",
"updated_at": "2026-10-20T08:30:00.000",
"post_title": "Choosing a widget",
"post_url": "https://example.com/choosing-a-widget/",
"post_edit_url": "https://example.com/wp-admin/post.php?post=42&action=edit",
"referrer_host": "",
"via_links": []
}
],
"per_page": 15,
"total": 1,
"counts": {
"needs_attention": 1,
"content": 1,
"traffic": 0,
"fixed": 0,
"ignored": 0,
"all": 1,
"scope": { "type": "content", "all": 1 },
"status": { "active": 1, "fixed": 0, "ignored": 0 }
},
"last_scan": {
"posts": 120,
"finished_at": "2026-10-20 08:31:12",
"checked": 640,
"unchecked": 0,
"started_at": 1792483800
},
"scan_types": [
{ "name": "post", "label": "Posts" },
{ "name": "page", "label": "Pages" }
],
"sitemap": {
"mode": "sitemap",
"status": { "state": "ok", "checked_at": 1792483500, "url": "https://example.com/wp-sitemap.xml", "count": 118 },
"listed": true
}
}
}The page object also holds first_page_url, from, last_page, last_page_url, links, next_page_url, path, prev_page_url and to, shortened here.
Scan posts for broken links
POST /link-health/scan
Checks the links in one batch of published posts, logs the broken ones and returns where the next batch starts.
Permission: link_manager_pro_access
Body parameters:
| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
offset | integer | No | 0 | Posts to skip. 0 starts a new scan. A negative number counts as 0. |
limit | integer | No | 25 | Posts per batch, kept between 1 and 25. |
scan_id | string | No | none | The scanId of the scan this batch belongs to. 8 to 32 lowercase letters and digits. Any other value is ignored. |
The scan checks the content types chosen under Content types to check in the Link Intelligence settings (default post,page), published posts only, ordered by ID. With offset above 0 and a scan_id the site no longer knows (expired after a day, or lost from an object cache), the scan still runs. It records no finished scan, because its earlier counts are lost.
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
scanId | string | The scan's ID. Send it back as scan_id. A new one is made when offset is 0. |
scanned | integer | Posts scanned in this batch. |
found | integer | Broken links found in this batch. |
checked | integer | Links checked in this batch. |
unchecked | integer | Links left unchecked by a cap or the time limit. |
skipped | integer | Links never requested because they point to a private or internal address. |
total | integer | Published posts of the chosen types. |
nextOffset | integer or null | The offset for the next batch, or null when the scan is finished. |
bash
curl -X POST -u USER:APPLICATION_PASSWORD \
-H "Content-Type: application/json" \
-d '{ "offset": 0 }' \
https://example.com/wp-json/link-manager-pro/v1/link-health/scanjson
{
"success": true,
"data": {
"scanId": "q3k9x0m2b7a1c8de",
"scanned": 25,
"found": 3,
"checked": 140,
"unchecked": 0,
"skipped": 2,
"total": 120,
"nextOffset": 25
}
}Errors:
| Status | When |
|---|---|
422 | No content type is chosen for scanning. errors.scan_types holds the message. |
Check the sitemap again
POST /link-health/sitemap
Reads the site's sitemap again now and returns how that went. It decides which visitor 404s are logged in Smart mode.
Permission: link_manager_pro_access
Body parameters: None.
Only one read runs at a time for the site. A call that arrives while another read runs skips the read and returns the current state.
Response: 200. data is the sitemap object described under List Link Health items: mode, status and listed.
json
{
"success": true,
"data": {
"mode": "sitemap",
"status": { "state": "ok", "checked_at": 1792483500, "url": "https://example.com/wp-sitemap.xml", "count": 118 },
"listed": true
}
}Change a Link Health item's status
PUT /link-health/{id}/status
Marks an item active, fixed or ignored. Marking an active item fixed fires the link_manager_pro_broken_link_fixed action.
Permission: link_manager_pro_access
Path parameters:
| Name | Type | What it is |
|---|---|---|
id | integer | The Link Health item ID. |
Body parameters:
| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
status | string | No | active | active, fixed or ignored. |
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
id | integer | The item ID. |
status | string | The new status. |
previous_status | string | The status before the change, so a client can offer Undo. |
json
{
"success": true,
"data": { "id": 17, "status": "fixed", "previous_status": "active" }
}The action link_manager_pro_broken_link_fixed fires once, with resolution set to marked_fixed, when the item was not fixed before and is now. Saving a fixed item as fixed again does not fire it.
Errors:
| Status | When |
|---|---|
404 | No item has that ID. |
422 | status is not active, fixed or ignored. errors.status holds the message. |
Fix a broken link with a redirect
POST /link-health/{id}/redirect
Creates a short link and a redirect for the broken address, then marks the item fixed.
Permission: link_manager_pro_access
Path parameters:
| Name | Type | What it is |
|---|---|---|
id | integer | The Link Health item ID. |
Body parameters:
| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
target_url | string | Yes | none | Where visitors go instead. A full http or https URL of 2048 bytes or fewer. It cannot be the broken address. destination_url is accepted as an older name. |
slug | string | No | generated | The short link's slug, 64 characters or fewer. It must be unused. Empty lets the plugin generate one. |
redirect_type | integer | No | 301 | 301, 302 or 307. |
link_attributes | object | No | none | Keys nofollow, noopener, noreferrer and sponsored, each yes, no, true or false. Other keys are dropped. |
When the broken address is on this site, the short link is hidden from the short link list and counts, and its source is redirect. When it is on another site, the short link stays visible with source health. That short link is the fix to put into the post.
A redirect that already exists for the broken address is pointed at the new short link instead of being duplicated.
Response: 200, with the message "Redirect created successfully." data holds:
| Field | Type | What it holds |
|---|---|---|
redirect | object | The redirect row: id, source_url (the broken address), source_url_hash (32-character string), target_link_id (integer), status (active), created_at, updated_at. |
link | object | The new short link, with the fields shown in Create a short link. |
bash
curl -X POST -u USER:APPLICATION_PASSWORD \
-H "Content-Type: application/json" \
-d '{ "target_url": "https://example.com/new-guide", "redirect_type": 301 }' \
https://example.com/wp-json/link-manager-pro/v1/link-health/17/redirectjson
{
"success": true,
"data": {
"redirect": {
"source_url": "https://example.com/old-guide",
"source_url_hash": "4b5c31ad44d7dec24d24f4e8ff46c481",
"target_link_id": 61,
"status": "active",
"updated_at": "2026-10-23T09:30:00.000",
"created_at": "2026-10-23T09:30:00.000",
"id": 5
},
"link": {
"id": 61,
"slug": "k7d2p9",
"target_url": "https://example.com/new-guide",
"redirect_type": 301,
"source": "redirect",
"short_url": "https://example.com/link/k7d2p9"
}
},
"message": "Redirect created successfully."
}The link object is shortened here to the main fields.
The action link_manager_pro_broken_link_fixed fires once, with resolution set to redirect_created, unless the item was already fixed.
Errors:
| Status | When |
|---|---|
404 | No item has that ID. |
422 | errors.target_url: empty, over 2048 characters, not an http or https URL, or the same as the broken address. |
422 | errors.slug: over 64 characters, already used by another short link, or not made of letters, numbers or dashes. |
422 | errors.redirect_type: not 301, 302 or 307. |
422 | errors.link_attributes: not an object, or a value other than yes, no, true or false. |
Delete a Link Health item
DELETE /link-health/{id}
Removes one item. If the link is still broken, the next scan or visit logs it again.
Permission: link_manager_pro_access
Path parameters:
| Name | Type | What it is |
|---|---|---|
id | integer | The Link Health item ID. |
Response: 200. data is null and the message is "Link health issue deleted."
json
{
"success": true,
"data": null,
"message": "Link health issue deleted."
}Errors:
| Status | When |
|---|---|
404 | No item has that ID. |
Clear Link Health items
DELETE /link-health
Removes every Link Health item of one type, or of both types when type is empty, all or left out, whatever its status, including fixed and ignored items.
WARNING
This deletes the items for good. Send type to clear only one kind.
Permission: link_manager_pro_access
Query parameters:
| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
type | string | No | all | content or traffic. Empty or all removes both. Leaving it out removes both. |
Search and status filters do not apply. Hidden items of unpublished posts stay. The number in counts.scope.all of the list is what this removes.
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
removed | integer | How many items were removed. |
type | string | The tab cleared: content, traffic, or an empty string for both. |
json
{
"success": true,
"data": { "removed": 12, "type": "content" }
}Errors:
| Status | When |
|---|---|
422 | type is not empty, all, content or traffic. errors.type holds the message. |
Link Audit
Link Audit counts how many links point to each post and from it. Each audited post is one row. A post with no incoming internal link is orphaned. A post below the minimum incoming or outgoing links is weak. The minimums come from the Link Intelligence settings: 3 incoming and 2 outgoing by default. Every other post is healthy.
The Link Audit row
| Field | Type | What it holds |
|---|---|---|
id | integer | The row ID. |
post_id | integer | The audited post. |
post_type | string | The post type name, for example post. |
post_title | string | The post title when it was last scanned. |
inbound_internal | integer | Posts that link to this post. |
outbound_internal | integer | Internal links in this post. |
outbound_external | integer | External links in this post. |
status | string | orphaned, weak or healthy. |
scanned_at | string or null | When the post was last scanned, Y-m-d H:i:s. |
created_at | string | When the row was stored. |
updated_at | string | When the row last changed. |
The list adds two fields. post_type_label is the post type's singular label, and edit_url is the editor link, or null when the user cannot edit the post.
How a full scan runs. Call POST /link-audit/scan with offset 0. The server keeps the position of the pass. The pass first reads every audited post, then counts the links to each. Call the route again with the nextOffset from the last response until status is completed. A call with any other offset changes nothing and returns the position the server expects. A call with no pass running, or one idle for over an hour, returns 409.
List audited posts
GET /link-audit
Returns one page of audited posts, fewest incoming links first.
Permission: link_manager_pro_access
Query parameters:
| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
search | string | No | none | Matches part of the post title. |
filter | string | No | none | orphaned, weak or healthy. Any other value is ignored. |
sort | string | No | inbound_internal | One of post_title, inbound_internal, outbound_internal, outbound_external, scanned_at, status. Any other value sorts by inbound_internal, ascending. |
order | string | No | asc | asc or desc. |
page | integer | No | 1 | The page number. |
per_page | integer | No | 20 | Rows per page, kept between 1 and 100. |
Only published posts of the audited types are listed.
Response: 200. data is a page object (Pagination) whose data.data rows are Link Audit rows.
json
{
"success": true,
"data": {
"current_page": 1,
"data": [
{
"id": 8,
"post_id": 42,
"post_type": "post",
"post_title": "Choosing a widget",
"inbound_internal": 0,
"outbound_internal": 3,
"outbound_external": 1,
"status": "orphaned",
"scanned_at": "2026-10-20 08:30:00",
"created_at": "2026-10-18T06:00:00.000",
"updated_at": "2026-10-20T08:30:00.000",
"post_type_label": "Post",
"edit_url": "https://example.com/wp-admin/post.php?post=42&action=edit"
}
],
"per_page": 20,
"total": 1
}
}The page object also holds the other paging fields, shortened here. When the audit table does not exist yet, the route returns an empty page: data [], total 0, per_page 20, current_page 1, last_page 1.
Get the Link Audit totals
GET /link-audit/stats
Returns the audit counts, the thresholds behind them and the coverage score.
Permission: link_manager_pro_access
Query parameters: None.
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
total | integer | Audited posts. |
healthy | integer | Posts that are neither orphaned nor weak. |
weak | integer | Posts with some incoming links, but below a minimum. |
orphaned | integer | Posts with no incoming internal link. |
avg_inbound | number | Average incoming links per post, rounded to one decimal. 0 with no rows. |
thresholds | object | min_inbound and min_outbound, integers. |
coverage_score | integer | The coverage score, 0 to 100. |
coverage_grade | string | The grade letter for the score. |
published | integer | Published posts a full scan covers. |
rescan_needed | boolean | true when stored rows were counted under older rules, or the last full scan stopped before the end, and a full scan is due. |
rescan_reason | string | "unfinished" after a stopped full scan, otherwise an empty string. |
json
{
"success": true,
"data": {
"total": 120,
"healthy": 80,
"weak": 25,
"orphaned": 15,
"avg_inbound": 3.4,
"thresholds": { "min_inbound": 3, "min_outbound": 2 },
"coverage_score": 76,
"coverage_grade": "B",
"published": 120,
"rescan_needed": false,
"rescan_reason": ""
}
}List slugs to review
GET /link-audit/slugs-to-review
Lists short links whose slug breaks the current slug rules, for you to rename. It changes nothing.
Permission: link_manager_pro_access
Query parameters: None.
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
items | array | Up to 50 short links. Each has id (integer), slug (string) and reason (string, why the slug is flagged). |
total | integer | All short links that break the rules, even when items stops at 50. |
json
{
"success": true,
"data": {
"items": [
{ "id": 12, "slug": "summer/deals", "reason": "This slug can’t be used in a link. Remove \"/\", line breaks and special control characters." }
],
"total": 1
}
}Get the coverage score
GET /link-audit/coverage-score
Returns a 0 to 100 score for how well the audited posts link to each other, with its weekly history.
Permission: link_manager_pro_access
Query parameters: None.
The score is 100 minus three penalties, never below 0. Each penalty is the share of audited posts affected times a cap. Orphaned posts take up to 40 points, weak posts up to 30 (orphaned posts count here too), and posts with an open broken link up to 30. The grade is A from 90, B from 75, C from 60, D from 40, and F below that.
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
score | integer | The score, 0 to 100. |
grade | string | A, B, C, D or F. |
breakdown | object | orphan_penalty, weak_penalty and broken_penalty, integers: the points each takes off. |
suggestions | array | What to fix first. Each has type (orphaned, weak or broken), message (string) and link (string, a screen path such as /link-audit?filter=orphaned). |
counts | object | orphaned, weak, total, broken and broken_posts, integers. broken is open broken links in content. broken_posts is audited posts with at least one. |
history | array | Up to 12 weekly snapshots, oldest first. Each has week (string, for example week_2026_42), score (integer), grade (string) and date (string, Y-m-d). |
json
{
"success": true,
"data": {
"score": 76,
"grade": "B",
"breakdown": { "orphan_penalty": 5, "weak_penalty": 13, "broken_penalty": 6 },
"suggestions": [
{ "type": "orphaned", "message": "Fix 15 orphaned posts to improve your score by up to 12 points.", "link": "/link-audit?filter=orphaned" }
],
"counts": { "orphaned": 15, "weak": 25, "total": 120, "broken": 9, "broken_posts": 7 },
"history": [
{ "week": "week_2026_42", "score": 74, "grade": "C", "date": "2026-10-19" }
]
}
}Run a full Link Audit scan
POST /link-audit/scan
Reads one batch of a full audit pass and returns where the next batch starts. See how a full scan runs under Link Audit.
Permission: link_manager_pro_access
Body parameters:
| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
offset | integer | No | 0 | 0 starts a new pass. Later calls send the nextOffset of the last response. |
limit | integer | No | 10 | Posts read per batch, kept between 1 and 20. It does not apply to the counting phase, which handles 500 rows per batch. |
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
status | string | in_progress or completed. |
scanned | integer | Posts read, or rows counted, in this batch. |
offset | integer | The step the pass is at. Same as nextOffset. |
nextOffset | integer | The offset to send next. |
total | integer | Total steps: published posts plus audited rows. |
has_more | boolean | true until the pass is completed. |
hasMore | boolean | The same value as has_more. |
time_limited | boolean | true when the 20 second budget stopped the batch early. Keep going from nextOffset. |
phase | string | reading while posts are read, then counting. |
phase_done | integer | Steps done in the current phase. |
phase_total | integer | Steps in the current phase. |
bash
curl -X POST -u USER:APPLICATION_PASSWORD \
-H "Content-Type: application/json" \
-d '{ "offset": 0, "limit": 10 }' \
https://example.com/wp-json/link-manager-pro/v1/link-audit/scanjson
{
"success": true,
"data": {
"status": "in_progress",
"scanned": 10,
"offset": 10,
"nextOffset": 10,
"total": 240,
"has_more": true,
"hasMore": true,
"time_limited": false,
"phase": "reading",
"phase_done": 10,
"phase_total": 120
}
}Errors:
| Status | When |
|---|---|
409 | No pass is running, or the last one was idle for over an hour. The code is link_audit_scan_expired. Start again with offset 0. |
Scan one post for Link Audit
POST /link-audit/scan/{id}
Rescans one post and returns its audit row. A post that is no longer published or audited loses its row.
Permission: link_manager_pro_access
Path parameters:
| Name | Type | What it is |
|---|---|---|
id | integer | The post ID. |
Body parameters: None.
Response: 200, with the message "Post scanned successfully." data.audit is the Link Audit row, without the two list-only fields.
json
{
"success": true,
"data": {
"audit": {
"id": 8,
"post_id": 42,
"post_type": "post",
"post_title": "Choosing a widget",
"inbound_internal": 2,
"outbound_internal": 3,
"outbound_external": 1,
"status": "weak",
"scanned_at": "2026-10-23 09:40:00",
"created_at": "2026-10-18T06:00:00.000",
"updated_at": "2026-10-23T09:40:00.000"
}
},
"message": "Post scanned successfully."
}Errors:
| Status | When |
|---|---|
404 | The post does not exist, is not published, or is not a type Link Audit covers. The code is post_not_found. |
Get link suggestions for a post
GET /link-audit/{id}/suggestions
Suggests other posts that could link to this post, each with the text to link.
Permission: link_manager_pro_access
Path parameters:
| Name | Type | What it is |
|---|---|---|
id | integer | The post that should receive the links. |
Query parameters:
| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
limit | integer | No | 10 | Suggestions to return, kept between 1 and 20. |
The route matches words from this post's title against the title and text of up to 300 published posts that contain a title keyword. Words match as whole words. A missing post, or a title with no usable words, returns an empty array.
Response: 200. data is an array, best match first. Each item holds:
| Field | Type | What it holds |
|---|---|---|
source_post_id | integer | The post that would carry the link. |
source_title | string | Its title. |
post_type | string | Its post type. |
score | integer | The raw match score: for each shared title word, its title hits times 3 plus its text hits (at most 3), added up. |
anchor_text | string | Text found in that post that can become the link. |
relevance | integer | The score as a percentage of the best score, 0 to 100. |
json
{
"success": true,
"data": [
{
"source_post_id": 15,
"source_title": "Widget buying guide",
"post_type": "post",
"score": 7,
"anchor_text": "choosing a widget",
"relevance": 100
}
]
}Insert a link into a post
POST /link-audit/insert-link
Turns the first plain mention of the anchor text in one post into a link to another post.
Permission: link_manager_pro_access. The signed-in user also needs edit_post on the source post.
Body parameters:
| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
source_post_id | integer | Yes | none | The post to edit. |
target_post_id | integer | Yes | none | The post to link to. The link uses its permalink. |
anchor_text | string | Yes | none | Text that appears in the source post and is not yet a link. Trimmed. |
The change touches only that one link. Both posts are rescanned for Link Audit afterward.
Response: 200, with the message "Link inserted successfully." data is null.
bash
curl -X POST -u USER:APPLICATION_PASSWORD \
-H "Content-Type: application/json" \
-d '{ "source_post_id": 15, "target_post_id": 42, "anchor_text": "choosing a widget" }' \
https://example.com/wp-json/link-manager-pro/v1/link-audit/insert-linkjson
{
"success": true,
"data": null,
"message": "Link inserted successfully."
}Errors:
| Status | When |
|---|---|
422 | anchor_text is empty, or the text is not in the post any more, or is already a link. errors.anchor_text holds the message. |
422 | source_post_id or target_post_id is missing. The error is keyed to the missing field. |
422 | Saving would also change more than the content, or WordPress would strip content on save, or the post changed during the save. errors.anchor_text says which. |
404 | A post does not exist. The code is lmp_post_not_found. |
403 | The user cannot edit the source post. The code is lmp_forbidden. |
500 | WordPress could not save the post. The code is lmp_save_failed. |
Get the editor optimization for a post
GET /editor/optimization/{id}
Counts a post's internal links and compares them with a recommended number, for the editor panel.
Permission: edit_posts (or the capability the link_manager_pro_picker_capability filter returns) or link_manager_pro_access. The user must also be able to edit this post (edit_post).
Path parameters:
| Name | Type | What it is |
|---|---|---|
id | integer | The post ID. |
Query parameters: None.
The recommendation is one internal link per N characters of text, where N is the optimization_chars_per_link setting (default 500, at least 1). The optimization_delta setting (default 1) is the allowed difference. Your own short links count by where they lead.
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
text_length | integer | Characters of text in the post, without tags. |
internal_links | integer | Internal links in the post. |
recommended | integer | The text length divided by chars_per_link, rounded down, and at least 1 when the post has text. 0 when it has none. |
delta | integer | The allowed difference. |
chars_per_link | integer | The characters per link used. |
status | string | low below recommended - delta, high above recommended + delta (never for a post with no text), else optimal. |
json
{
"success": true,
"data": {
"text_length": 2400,
"internal_links": 2,
"recommended": 4,
"delta": 1,
"chars_per_link": 500,
"status": "low"
}
}Errors:
| Status | When |
|---|---|
403 | The user cannot edit this post. The code is lmp_forbidden. |
404 | The post does not exist. The code is lmp_post_not_found. |
Link Equity
Link Equity spreads a fixed budget of equity over the outbound links of each published post, with earlier links getting more. It adds up the result per address and per domain. The report is stored when a scan finishes. The list routes read the stored report and never recompute it.
How a scan runs. Call POST /link-equity/scan with offset 0, which clears any scan in progress. Call it again with the nextOffset of each response until status is completed. Send the batches in order. The stored report is replaced only when the last batch completes.
List Link Equity rows
GET /link-equity
Returns one page of the stored report, highest equity first.
Permission: link_manager_pro_access
Query parameters:
| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
basis | string | No | url | url or domain. Any other value counts as url. |
scope | string | No | all | all, internal, external or affiliate. Any other value counts as all. |
search | string | No | none | Matches part of the address, or of the domain when basis is domain. |
page | integer | No | 1 | The page number. |
per_page | integer | No | 10 | Rows per page, kept between 1 and 50. |
The report keeps the 500 addresses with the most equity.
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
basis | string | The basis used. |
scope | string | The scope used. |
summary | object | The totals, fields under Get the Link Equity totals. |
rows | array | The rows of this page, fields below. |
total | integer | Rows after filtering, across all pages. |
page | integer | The page returned. |
per_page | integer | Rows per page. |
max_equity | number | The highest equity in the filtered list, rounded to one decimal. Use it as the scale for a bar. |
Each row holds:
| Field | Type | What it holds |
|---|---|---|
label | string | The address, or the domain. Internal addresses show as a path such as /pricing. |
count | integer | How many links point to it. |
equity | number | The equity it receives, rounded to one decimal. |
internal | boolean | true when it leads to this site. |
clicks | integer | Recorded clicks on short links that lead there. |
affiliate | boolean | true when the host matches one of your affiliates. |
type | string | affiliate, internal or external. An affiliate row is never internal or external. |
json
{
"success": true,
"data": {
"basis": "url",
"scope": "all",
"summary": {
"generated_at": "2026-10-23 08:00:00",
"posts_scanned": 120,
"total_links": 940,
"internal_links": 610,
"external_links": 330,
"distinct_urls": 410,
"distinct_domains": 38,
"urls_listed": 410
},
"rows": [
{ "label": "/pricing", "count": 42, "equity": 380.5, "internal": true, "clicks": 0, "affiliate": false, "type": "internal" }
],
"total": 410,
"page": 1,
"per_page": 10,
"max_equity": 380.5
}
}Get the Link Equity totals
GET /link-equity/stats
Returns the totals of the stored report.
Permission: link_manager_pro_access
Query parameters: None.
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
generated_at | string or null | When the report was made, UTC Y-m-d H:i:s. null before the first scan. |
posts_scanned | integer | Posts in the report. |
total_links | integer | Links counted. |
internal_links | integer | Links that lead to this site. |
external_links | integer | Links that lead elsewhere. |
distinct_urls | integer | Different addresses, before the cut to 500. |
distinct_domains | integer | Different domains. |
urls_listed | integer | Addresses kept in the By URL list, at most 500. |
A report made under older counting rules is not shown. Every field is then 0, and generated_at is null.
json
{
"success": true,
"data": {
"generated_at": "2026-10-23 08:00:00",
"posts_scanned": 120,
"total_links": 940,
"internal_links": 610,
"external_links": 330,
"distinct_urls": 410,
"distinct_domains": 38,
"urls_listed": 410
}
}Run a Link Equity scan
POST /link-equity/scan
Adds one batch of published posts to the report and returns where the next batch starts.
Permission: link_manager_pro_access
Body parameters:
| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
offset | integer | No | 0 | Posts to skip. 0 starts a new scan. |
limit | integer | No | 50 | Posts per batch, kept between 1 and 100. |
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
status | string | in_progress or completed. |
scanned | integer | Posts scanned in this batch. |
offset | integer | Where the next batch starts. Same as nextOffset. |
nextOffset | integer | The offset to send next. |
total | integer | Published posts of the scanned types. |
has_more | boolean | true while posts remain. |
hasMore | boolean | The same value as has_more. |
time_limited | boolean | true when the 20 second budget stopped the batch early. Keep going from nextOffset. |
json
{
"success": true,
"data": {
"status": "in_progress",
"scanned": 50,
"offset": 50,
"nextOffset": 50,
"total": 120,
"has_more": true,
"hasMore": true,
"time_limited": false
}
}Content scan
The content scan is the Scan that masks external links. It rewrites links in saved posts into short links, so it edits your content. It covers published, scheduled and private posts of the configured post types, minus the excluded types. Drafts are left alone. A rewritten link to an affiliate's domain gets its short address under the Affiliate link prefix when that setting is set. Other links use the Short link prefix, or the internal prefix for your own site. See General settings.
How a scan runs. Call POST /scan with offset 0. Call it again with the nextOffset of each response until status is completed. A batch can end early when its 20 second budget runs out, so always continue from nextOffset. Call GET /scan/preview first to show how many posts the scan will check.
Start or continue a content scan
POST /scan
Masks the external links of one batch of posts and returns the tallies and where the next batch starts.
Permission: link_manager_pro_access. A post the signed-in user cannot edit is skipped and counted in postsSkipped.
Body parameters:
| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
offset | integer | No | 0 | Posts to skip. A negative number counts as 0. |
limit | integer | No | 20 | Posts per batch, at most 100. |
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
status | string | in_progress or completed. |
processed | integer | Posts passed so far in the whole scan. Same as nextOffset. |
nextOffset | integer | The offset to send next. |
total | integer | Posts the scan checks. |
hasMore | boolean | true while posts remain. |
time_limited | boolean | true when the 20 second budget stopped this batch early. |
linksCreated | integer | New short links made in this batch, including ones restored for an existing post. |
linksConverted | integer | Links rewritten into short links in this batch, new or reused. |
postsConverted | integer | Posts where at least one link was converted. |
postsUpdated | integer | Posts saved. |
postsSkipped | integer | Posts left alone: the user cannot edit them, or WordPress would strip their content. |
postsFailed | integer | Posts that could not be masked or saved. |
linksKeptForBlocks | integer | Links left as written because their block keeps the address. |
The tallies cover the batch you just ran, not the whole scan. Add them across batches if you need totals.
bash
curl -X POST -u USER:APPLICATION_PASSWORD \
-H "Content-Type: application/json" \
-d '{ "offset": 0, "limit": 20 }' \
https://example.com/wp-json/link-manager-pro/v1/scanjson
{
"success": true,
"data": {
"status": "in_progress",
"processed": 20,
"nextOffset": 20,
"total": 120,
"hasMore": true,
"time_limited": false,
"linksCreated": 14,
"linksConverted": 18,
"postsConverted": 9,
"postsUpdated": 9,
"postsSkipped": 1,
"postsFailed": 0,
"linksKeptForBlocks": 2
}
}Preview a content scan
GET /scan/preview
Returns how many posts a content scan will check, by status. It reads only.
Permission: link_manager_pro_access
Query parameters: None.
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
total | integer | Posts the scan checks. |
by_status | object | publish, future (scheduled) and private, integers. |
json
{
"success": true,
"data": {
"total": 124,
"by_status": { "publish": 120, "future": 1, "private": 3 }
}
}