Appearance
Settings endpoints
Use these REST routes to read and save each settings group, test the AI connection, import SEO plugin redirects and manage the country database. Each entry lists the permission, parameters, response fields and errors.
Sign-in and the response envelope are in the REST API overview.
All paths are relative to /wp-json/link-manager-pro/v1. The examples use https://example.com and an Application Password placeholder (USER:APPLICATION_PASSWORD). Inside WordPress, send the X-WP-Nonce header instead. Every route here needs manage_options unless its entry says otherwise (Permissions). The backup routes and the import from other plugins are on Backup and import endpoints.
How settings requests work
Every settings group follows the same rules. Each group entry below lists only what is specific to it.
Reading. GET /settings/{group} returns the group as one flat JSON object in data: every key of the group with its saved value, or the default when nothing is saved. Keys that are not in the schema are never returned. The Default column is the schema default. A site that never had the plugin starts with three other saved values: anonymize_ips is "yes", params_on_site_redirects is "no" and health_scan_schedule is "weekly". Each is set once, and only for a group that was never saved.
Saving. POST /settings/{group} takes a JSON object of keys to change. Form-encoded fields work too.
- Partial updates: keys you leave out keep their saved value. You can send one key.
- Unknown keys: keys that are not in the group are dropped without an error. An unknown group is not a route.
- Sanitizing: every value is sanitized by its type. A number is cast to an integer and clamped to its range, not rejected. A select value outside the allowed list becomes the key's default. A
"yes"/"no"key given anything else becomes the key's default. A list or object sent for a single-value key is ignored and the saved value stays. Text keys go throughsanitize_text_field, and textarea keys throughsanitize_textarea_field. - Empty body: a body with no fields saves nothing and still answers
200. - Response: a save answers
200withdataset tonull, exceptPOST /settings/general, which returnswarnings. - Hook: after a save, the plugin runs the action
link_manager_pro_settings_updatedwith the group name, the new values and the old values, secrets masked as described under Secrets below (Action hooks). Plugins can also add keys to the schema with thelink_manager_pro_settings_schemafilter, so a site can return more keys than the tables below.
Booleans are the strings "yes" and "no", never true or false. Key names are snake_case.
Secrets. A key stored encrypted is returned masked, never in clear. Only the AI api_key is one. The masked form is lmpro_masked: followed by the last four characters of the key (the whole value when it is shorter than four), or an empty string when no key is saved. When you save a value that starts with lmpro_masked:, the plugin ignores it and keeps the saved secret. So you can send a group back as you read it without overwriting the key.
Errors. A failed validation answers 422 with message and an errors object of messages per field (REST API overview). POST /settings/general, POST /settings/advanced and POST /settings/tracking can fail this way. A request without manage_options gets 401 or 403.
General settings
The short link prefixes, the slug length and how links are masked.
Get the general settings
GET /settings/general
Returns the general settings.
Permission: manage_options
Query parameters: None.
Response: 200. data holds these keys:
| Key | Type | Default | Allowed values or range | What it does |
|---|---|---|---|---|
auto_mask_on_save | string | "yes" | "yes", "no" | Masks links in the content WordPress is about to save. |
link_prefix | string | "link" | A slug: lowercase letters, numbers, hyphens | The path in front of a short link's slug. |
internal_prefix | string | "go" | A slug | The path used for short links that point to this site. |
affiliate_prefix | string | "" | A slug, or empty | An optional path for short links whose destination is on one of your affiliates' domains, for example /recommends/amazon-deal. Empty means they follow the other rules: internal_prefix for your own site when mask_internal_links is on, else link_prefix. A redirect's hidden link never uses it. |
code_length | integer | 8 | 6 to 16 | The length of automatically generated slugs. |
mask_internal_links | string | "no" | "yes", "no" | Also masks links that point to this site. Those short links use internal_prefix. |
json
{
"success": true,
"data": {
"auto_mask_on_save": "yes",
"link_prefix": "link",
"internal_prefix": "go",
"affiliate_prefix": "",
"code_length": 8,
"mask_internal_links": "no"
}
}Save the general settings
POST /settings/general
Saves the general settings and checks the three prefixes first.
Permission: manage_options
Body parameters: any of the keys in the table above, as JSON. See How settings requests work.
Prefix rules. When the body holds at least one prefix, the plugin checks all three prefixes as they would be after the save. A prefix you did not change is not checked again, so an older value never blocks another change. A broken rule answers 422 and nothing is saved. The rules:
- A prefix must be a slug: lowercase letters, numbers and hyphens. Surrounding slashes and spaces are removed and capitals are lowercased before the check.
- A prefix cannot be a path WordPress answers itself:
wp-admin,wp-content,wp-includes,wp-json,feed,embed,page,comments,search,author,category,tagortrackback. - The three prefixes must differ from each other.
link_prefixandinternal_prefixcannot be empty.affiliate_prefixcan.
Side effects. When a prefix changes, the plugin remembers the old prefix so old short links keep working, clears the auto-link cache and flushes the rewrite rules. The old prefix keeps its full place for 90 days. After that it still opens its short link wherever WordPress has nothing at that address. The plugin remembers the last 5 replaced prefixes. Every prefix you own, and a remembered one, opens every short link. See Update links after a prefix change for the content update.
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
warnings | object | Messages that do not block the save, keyed by prefix name (link_prefix, internal_prefix, affiliate_prefix). An empty list ([]) when there are none. |
A warning appears when a published page or post sits at that path, or when ThirstyAffiliates is active and uses that prefix.
bash
curl -X POST -u USER:APPLICATION_PASSWORD \
-H "Content-Type: application/json" \
-d '{"link_prefix":"go-to","code_length":10}' \
"https://example.com/wp-json/link-manager-pro/v1/settings/general"json
{
"success": true,
"data": { "warnings": [] }
}Errors:
| Status | When |
|---|---|
422 | A prefix breaks a rule above. errors is keyed by the prefix name, for example link_prefix. |
Link Intelligence settings
How Link Health, traffic monitoring, the link audit, internal link optimization and link equity behave.
Get the Link Intelligence settings
GET /settings/link-intelligence
Returns the Link Intelligence settings.
Permission: manage_options
Query parameters: None.
Response: 200. data holds these keys:
| Key | Type | Default | Allowed values or range | What it does |
|---|---|---|---|---|
health_scan_schedule | string | "disabled" | "disabled", "daily", "weekly", "monthly" | How often the link health scan runs. |
health_post_types | string | "post,page" | Comma-separated post type names | The content types Link Health checks. |
traffic_monitor_mode | string | "sitemap" | "sitemap", "all" | Which visitor 404s the traffic monitor records. |
traffic_retention_days | integer | 0 | 0 to 3650 | Days a visitor 404 row is kept after it was last seen. 0 keeps it. |
audit_min_inbound | integer | 3 | 1 to 20 | The inbound internal links the link audit expects a post to have at least. |
audit_min_outbound | integer | 2 | 1 to 20 | The outbound internal links the link audit expects a post to have at least. |
optimization_enabled | string | "yes" | "yes", "no" | Turns the internal link optimization on or off. |
optimization_chars_per_link | integer | 500 | 50 to 5000 | Characters of content per recommended internal link. |
optimization_delta | integer | 1 | 0 to 20 | How far the internal link count may differ from the recommended count before it counts as low or high. |
equity_budget | integer | 100 | 1 to 10000 | The equity budget used in the link equity calculation. |
equity_position_penalty | integer | 50 | 0 to 100 | The position penalty, in percent, used in the link equity calculation. |
json
{
"success": true,
"data": {
"health_scan_schedule": "weekly",
"health_post_types": "post,page",
"traffic_monitor_mode": "sitemap",
"traffic_retention_days": 0,
"audit_min_inbound": 3,
"audit_min_outbound": 2,
"optimization_enabled": "yes",
"optimization_chars_per_link": 500,
"optimization_delta": 1,
"equity_budget": 100,
"equity_position_penalty": 50
}
}Save the Link Intelligence settings
POST /settings/link-intelligence
Saves the Link Intelligence settings and clears the cached Link Health counts, because the chosen content types change what they include.
Permission: manage_options
Body parameters: any of the keys in the table above. See How settings requests work.
Response: 200, data is null.
Notification settings
The email digest.
Get the notification settings
GET /settings/notifications
Returns the notification settings.
Permission: manage_options
Query parameters: None.
Response: 200. data holds these keys:
| Key | Type | Default | Allowed values or range | What it does |
|---|---|---|---|---|
digest_frequency | string | "disabled" | "disabled", "daily", "weekly", "monthly" | How often the email digest is sent. |
digest_recipient | string | "" | Email addresses separated by commas, semicolons or spaces | Who receives the digest. |
On save, each address is sanitized and checked, invalid ones are dropped, duplicates are removed, and the rest are stored as one comma-separated list ([email protected], [email protected]).
Save the notification settings
POST /settings/notifications
Saves the notification settings.
Permission: manage_options
Body parameters: any of the keys in the table above. See How settings requests work.
Response: 200, data is null.
Advanced settings
Access, report caching, proxy headers and what happens on uninstall.
Get the advanced settings
GET /settings/advanced
Returns the advanced settings.
Permission: manage_options
Query parameters: None.
Response: 200. data holds these keys:
| Key | Type | Default | Allowed values or range | What it does |
|---|---|---|---|---|
admin_capability | string | "manage_options" | "manage_options", "manage_link_manager_pro" | Who can use the plugin besides administrators. See Access for editors. |
report_cache_ttl | integer | 600 | 0 to 86400 | Seconds a report is cached. |
trust_proxy_headers | string | "no" | "yes", "no" | Reads the visitor IP from proxy headers. |
trusted_proxies | string | "" | IP addresses or CIDR ranges, separated by new lines or commas | The proxies whose forwarded headers are trusted for the visitor IP and country. Passed through the link_manager_pro_trusted_proxies filter. |
delete_data_on_uninstall | string | "no" | "yes", "no" | Deletes the plugin's data when the plugin is deleted. |
Save the advanced settings
POST /settings/advanced
Saves the advanced settings.
Permission: manage_options
Body parameters: any of the keys in the table above. See How settings requests work.
Response: 200, data is null.
Errors: 422 with errors.trusted_proxies when a line in trusted_proxies is not an IP address or CIDR range. The message names up to 10 such lines. Nothing is saved.
Integration settings
Caching, consent and SEO plugin behavior.
Get the integration settings
GET /settings/integrations
Returns the integration settings.
Permission: manage_options
Query parameters: None.
Response: 200. data holds these keys:
| Key | Type | Default | Allowed values or range | What it does |
|---|---|---|---|---|
clear_cache_on_change | string | "yes" | "yes", "no" | Clears a detected page cache when a short link or redirect changes. |
seo_404_cleanup | string | "manual" | "automatic", "manual" | Whether saving an active redirect also clears its entry from the Rank Math 404 log by itself. |
respect_consent | string | "yes" | "yes", "no" | Waits for visitor consent from a detected consent plugin before tracking. |
consent_category | string | "statistics" | "statistics", "marketing", "any" | The consent category that allows tracking. "any" accepts either of the other two. |
Save the integration settings
POST /settings/integrations
Saves the integration settings.
Permission: manage_options
Body parameters: any of the keys in the table above. See How settings requests work.
Response: 200, data is null.
Redirection settings
The default redirect type, extra parameters and rel attributes.
Get the redirection settings
GET /settings/redirection
Returns the redirection settings.
Permission: manage_options
Query parameters: None.
Response: 200. data holds these keys:
| Key | Type | Default | Allowed values or range | What it does |
|---|---|---|---|---|
global_params | string | "" | A query string, such as ref=newsletter&src=site | Parameters added to every short link redirect. Read with PHP parse_str(). The plugin rebuilds the value on save: it decodes each name and value, strips tags and encodes them again, so + becomes %20. A double-encoded value such as %2541 is kept as %2541, and the redirect sends it as is. A redirect of an old address to a page on your own site gets them only while params_on_site_redirects is "yes". |
params_on_site_redirects | string | "yes" | "yes", "no" | Adds the site-wide parameters to Simple Redirects whose target is this site. |
redirect_type | integer | 301 | 301, 302, 307 | The default redirect status for new short links. Any other value becomes 301. |
rel_rules | object | see below | Keys nofollow, noopener, noreferrer, sponsored | Which rel values the plugin adds to links. |
rel_rules holds one object per rel value, each with two fields: global ("yes" or "no", default "no") and domains (a textarea string, default ""). The plugin normalizes each listed line and the link's host when it matches them, so https://WWW.example.com/ matches example.com. Saved text is not rewritten. The defaults are:
json
{
"nofollow": { "global": "yes", "domains": "" },
"noopener": { "global": "yes", "domains": "" },
"noreferrer": { "global": "no", "domains": "" },
"sponsored": { "global": "no", "domains": "" }
}Save the redirection settings
POST /settings/redirection
Saves the redirection settings.
Permission: manage_options
Body parameters: any of the keys in the table above. See How settings requests work.
Send rel_rules whole. The plugin replaces the stored object with the entries you send, so a rel value you leave out is no longer saved. Names other than the four above are dropped, and an entry that is not an object is skipped.
Response: 200, data is null.
Tracking settings
Click tracking, bot filtering and IP handling.
Get the tracking settings
GET /settings/tracking
Returns the tracking settings.
Permission: manage_options
Query parameters: None.
Response: 200. data holds these keys:
| Key | Type | Default | Allowed values or range | What it does |
|---|---|---|---|---|
track_clicks | string | "yes" | "yes", "no" | Records clicks on short links. |
click_retention_days | integer | 0 | 0 to 3650 | Days a click is kept. 0 keeps clicks. |
blocked_ips | string | "" | One IP address per line, exact match | Clicks from these addresses are not counted. |
enable_bot_detection | string | "yes" | "yes", "no" | Leaves known bots out of the click data. |
blocked_bot_patterns | string | "" | One text per line, matched anywhere in the user agent, ignoring case | Your own bot list. Any entry replaces the 48 built-in patterns; it doesn't add to them. Used only while enable_bot_detection is "yes". |
anonymize_ips | string | "no" | "yes", "no" | Anonymizes stored IP addresses. |
track_internal_clicks | string | "no" | "yes", "no" | Records clicks on short links inside your own pages. |
internal_click_selector | string | ".entry-content" | Free text | The CSS selector for the area where internal clicks are tracked. A save over REST refuses a selector browsers always reject, with a 422 and errors.internal_click_selector (only while track_internal_clicks is "yes" and the value changed). WP-CLI, imports and restores store .entry-content instead. Some bad selectors still get through, and the tracker then falls back to .entry-content. |
Save the tracking settings
POST /settings/tracking
Saves the tracking settings. The plugin also records when tracking was changed, so report health verdicts that depend on tracking wait before they judge a link.
Permission: manage_options
Body parameters: any of the keys in the table above. See How settings requests work.
Response: 200, data is null.
Errors: 422 with errors.internal_click_selector ("Enter a valid CSS selector.") when the selector is one browsers always reject. Nothing is saved.
Auto-link settings
Automatic keyword links in post content.
Get the auto-link settings
GET /settings/autolink
Returns the auto-link settings.
Permission: manage_options
Query parameters: None.
Response: 200. data holds these keys:
| Key | Type | Default | Allowed values or range | What it does |
|---|---|---|---|---|
autolink_enabled | string | "yes" | "yes", "no" | Turns auto-linking on or off. |
autolink_max_per_keyword | integer | 1 | 1 to 50 | The most links one keyword gets in a post. |
autolink_max_per_page | integer | 0 | 0 to 500 | The most auto-links in a post. 0 means no limit. |
autolink_max_per_target | integer | 0 | 0 to 500 | The most links to the same destination (host and path, query kept) in a post. 0 means no limit. |
autolink_case_sensitive | string | "no" | "yes", "no" | Matches keywords with the same capital letters only. |
autolink_exclude_headings | string | "yes" | "yes", "no" | Skips headings. |
autolink_post_types | string | "post,page" | Comma-separated post type names | The content types that get auto-links. |
Save the auto-link settings
POST /settings/autolink
Saves the auto-link settings and clears the auto-link cache.
Permission: manage_options
Body parameters: any of the keys in the table above. See How settings requests work.
Response: 200, data is null.
Disclosure settings
The affiliate disclosure text added to posts.
Get the disclosure settings
GET /settings/disclosure
Returns the disclosure settings.
Permission: manage_options
Query parameters: None.
Response: 200. data holds these keys:
| Key | Type | Default | Allowed values or range | What it does |
|---|---|---|---|---|
disclosure_enabled | string | "no" | "yes", "no" | Adds the disclosure to posts. |
cookie_consent_acknowledged | string | "no" | "yes", "no" | Records that the site owner has seen the cookie consent note. |
disclosure_text | string | "Disclosure: This post contains affiliate links. If you purchase through these links, I may earn a commission at no additional cost to you." | Free text (textarea) | The disclosure text. |
disclosure_position | string | "before" | "before", "after" | Whether the disclosure comes before or after the content. |
disclosure_style | string | "boxed" | "minimal", "boxed", "custom" | The look of the disclosure. |
disclosure_custom_class | string | "" | Free text | The CSS class used with the "custom" style. |
disclosure_post_types | string | "post,page" | Comma-separated post type names | The content types that get the disclosure. |
Save the disclosure settings
POST /settings/disclosure
Saves the disclosure settings.
Permission: manage_options
Body parameters: any of the keys in the table above. See How settings requests work.
Response: 200, data is null.
AI settings
The provider, model and limits for the AI features. The key is a secret, so it comes back masked.
Get the AI settings
GET /settings/ai
Returns the AI settings, the model lists for the settings screen and the default model per provider.
Permission: manage_options
Query parameters: None.
Response: 200. data holds these keys:
| Key | Type | Default | Allowed values or range | What it does |
|---|---|---|---|---|
source | string | "auto" | "auto", "wordpress", "custom" | Where requests go: the AI connection set up in WordPress, your own key, or the first one available. |
provider | string | "none" | "none", "openai", "gemini", "anthropic" | The provider used with your own key. |
api_key | string | "" | Free text | Your provider key. Returned masked, see below. |
model | string | "" | Up to 120 characters | The model name. Empty uses the provider's default. Longer text is cut to 120 characters. |
max_tokens | integer | 2000 | 256 to 32000 | The most tokens one request may use. |
enable_suggestions | string | "no" | "yes", "no" | Turns the AI link suggestions on. |
enable_untracked_detection | string | "yes" | "yes", "no" | Turns the detection of untracked links on. |
rate_limit_max | integer | 30 | 0 to 100000 | Requests one user may make in the window. 0 turns the limit off. |
rate_limit_window | integer | 300 | 1 to 86400 | The window length in seconds. |
_models | object | none | Read-only | Model choices per provider (openai, gemini, anthropic), each a list of { "value", "label" }. |
_model_defaults | object | none | Read-only | The default model name per provider. Today openai is gpt-4o-mini, gemini is gemini-2.5-flash and anthropic is claude-haiku-4-5-20251001. |
The two _-prefixed keys are added to the response only. Sending them in a save does nothing.
json
{
"success": true,
"data": {
"source": "custom",
"provider": "openai",
"api_key": "lmpro_masked:a1b2",
"model": "",
"max_tokens": 2000,
"enable_suggestions": "yes",
"enable_untracked_detection": "yes",
"rate_limit_max": 30,
"rate_limit_window": 300,
"_models": {
"openai": [{ "value": "gpt-4o-mini", "label": "GPT-4o mini" }],
"gemini": [{ "value": "gemini-2.5-flash", "label": "Gemini 2.5 Flash" }],
"anthropic": [{ "value": "claude-haiku-4-5-20251001", "label": "Claude Haiku 4.5" }]
},
"_model_defaults": {
"openai": "gpt-4o-mini",
"gemini": "gemini-2.5-flash",
"anthropic": "claude-haiku-4-5-20251001"
}
}
}Save the AI settings
POST /settings/ai
Saves the AI settings. Send api_key with a new key to replace it. Send the masked value you read, or leave api_key out, to keep the saved key. Sending an empty string clears the key. When a request changes provider and has no model, a saved model that the new provider doesn't list is reset to empty, which means the provider's default. A model sent in the same request is kept as sent.
Permission: manage_options
Body parameters: any of the keys in the table above, except the _ keys. See How settings requests work.
Response: 200, data is null.
Test the AI connection
POST /settings/ai/test
Checks which AI connection the plugin would use. With your own key (source resolves to custom), it also sends a one-word test request to the provider.
Permission: manage_options
Body parameters: None. The route tests the saved settings.
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
configured | boolean | true when a connection is available. |
source | string or null | "custom" for your own key, "wordpress" for the WordPress connection. null when nothing is configured. |
provider | string or null | The provider name. "site-connector" for the WordPress connection. |
model | string or null | The model name. "site-default" for the WordPress connection with no model set. |
live | boolean | Whether the AI assistant is available on this site. |
live_note | string | A sentence that explains live when it is false. Empty otherwise. |
test_ok | boolean | true when the test request answered. Only present when source is "custom". |
test_error | string | The failure text, empty when the test worked. Only present with test_ok. |
No test request is sent for the WordPress connection or when nothing is configured, so test_ok and test_error are missing in those cases.
json
{
"success": true,
"data": {
"configured": true,
"source": "custom",
"provider": "openai",
"model": "gpt-4o-mini",
"live": true,
"live_note": "",
"test_ok": true,
"test_error": ""
}
}Errors:
| Status | When |
|---|---|
429 | More than 10 tests by this user in 5 minutes. The code is lmp_ai_rate_limited. The test sends a billed request, so it is limited more tightly than the AI features. |
Integrations and post types
Two read-only routes that fill the Integrations and settings screens.
Get the detected integrations
GET /settings/integrations/status
Returns which cache, consent and automation plugins the plugin sees, and whether a country database is available.
Permission: manage_options
Query parameters: None.
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
caching | array of strings | Names of the active page cache plugins: WP Rocket, LiteSpeed Cache, W3 Total Cache, WP Super Cache, WP Fastest Cache. Empty when none. |
automatorwp | boolean | true when AutomatorWP is active. |
consent | array of strings | Zero or one name: the consent plugin whose consent the plugin reads. Empty when none. |
consent_supported | array of strings | Every consent plugin the plugin can read: Complianz, CookieYes, Cookiebot. |
geoip | boolean | true when a country database file can be read. |
json
{
"success": true,
"data": {
"caching": ["WP Rocket"],
"automatorwp": false,
"consent": [],
"consent_supported": ["Complianz", "CookieYes", "Cookiebot"],
"geoip": true
}
}List the public post types
GET /settings/post-types
Returns the public post types a site can pick for Link Health, auto-links and the disclosure.
Permission: manage_options
Query parameters: None.
Response: 200. data is a list. Each item holds:
| Field | Type | What it holds |
|---|---|---|
name | string | The post type name, for example post. |
label | string | The plural label, for example Posts. |
Attachments and the block editor types wp_navigation, wp_block, wp_template, wp_template_part and wp_global_styles are left out.
json
{
"success": true,
"data": [
{ "name": "post", "label": "Posts" },
{ "name": "page", "label": "Pages" }
]
}Get the import prompt
GET /settings/migration/prompt
Returns whether the dashboard should offer to move data from Pretty Links, BetterLinks or ThirstyAffiliates.
Permission: link_manager_pro_access (Permissions)
Query parameters: None.
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
show | boolean | true when a plugin was detected that was not dismissed, migrated or already attempted. |
sources | array | Only when show is true. One item per detected plugin, fields below. |
total_records | integer | Only when show is true. All counts of all listed sources added together. |
recommendation | object | Only when show is true. method is "cli" from 10000 records, else "ui". reason is "large_dataset" or "standard". message is a sentence. |
Each item of sources holds:
| Field | Type | What it holds |
|---|---|---|
source | string | prettylinks, betterlinks or thirstyaff. |
name | string | The plugin name. |
version | string | The detected version, empty if unknown. |
counts | object | Integer counts per record kind, for example links, categories, tags, clicks. The keys depend on the plugin. |
cli_command | string | The WP-CLI command for this source, wp link-manager-pro import --source={source}. |
json
{
"success": true,
"data": { "show": false }
}SEO plugin redirect import
These routes copy redirects from another SEO plugin into the plugin's Simple Redirects. Rank Math (rankmath) and SEOPress (seopress) can be imported. Yoast SEO (yoast) and All in One SEO (aioseo) are only detected, so their analyze and import calls answer 400.
Only exact-path rules with a 301, 302 or 307 status and a target are imported. Other rules are skipped with a reason. The import creates each redirect the way the Simple Redirects screen does, so its checks apply. A source that already exists in any state is skipped, so running the import again is safe. analyze reports it as already_imported and counts it under skipped, not importable.
Flow: call status, call analyze for a dry run, then call import again and again, each time with the offset set to the previous processed value, until done is true. The plugin looks at no more than 5000 rules per call.
List the detected SEO plugins
GET /settings/integrations/seo/status
Returns the active SEO plugins and whether Rank Math's 404 log can be cleaned.
Permission: manage_options
Query parameters: None.
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
plugins | array | One item per active SEO plugin that the plugin knows. Inactive or missing plugins are not listed. |
reconcile_404_available | boolean | true when Rank Math's 404 Monitor module is on and its table is present. |
Each item of plugins holds:
| Field | Type | What it holds |
|---|---|---|
slug | string | rankmath, seopress, yoast or aioseo. This is the {source} value. |
name | string | Rank Math, SEOPress, Yoast SEO or All in One SEO. |
active | boolean | Always true in this list. |
store_available | boolean | true when the plugin's redirect store exists. Always false for Yoast SEO and All in One SEO. |
import_supported | boolean | false for Yoast SEO and All in One SEO. |
version | string | The plugin version, empty if unknown. |
redirect_count | integer | Rules in the plugin's store. 0 when the store is missing or import is not supported. |
json
{
"success": true,
"data": {
"plugins": [
{
"slug": "rankmath",
"name": "Rank Math",
"active": true,
"store_available": true,
"import_supported": true,
"version": "1.0.250",
"redirect_count": 12
}
],
"reconcile_404_available": true
}
}Analyze an SEO plugin's redirects
GET /settings/integrations/seo/{source}/analyze
Counts how many redirects can be imported and why the others cannot. Nothing is written.
Permission: manage_options
Path parameters:
| Name | Type | What it is |
|---|---|---|
source | string | rankmath or seopress. |
Query parameters: None.
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
total | integer | Rules examined. |
importable | integer | Rules that map to a Simple Redirect. |
skipped | integer | Rules that do not. |
reasons | object | Skip reason to count. The reasons are unsupported_match (not an exact match), empty_source, unsupported_type (not 301, 302 or 307), no_target and trashed (a rule in the SEO plugin's trash), plus already_imported for a source that already exists as a redirect. An empty list ([]) when nothing is skipped. |
sample | array | Up to 6 importable rules. Each item has source (string, absolute URL), target (string, absolute URL), method (integer, 301, 302 or 307) and status (string, active or inactive). |
truncated | boolean | true when the plugin holds more than 5000 rules and the rest were not examined. |
json
{
"success": true,
"data": {
"total": 12,
"importable": 10,
"skipped": 2,
"reasons": { "unsupported_match": 2 },
"sample": [
{
"source": "https://example.com/old-page",
"target": "https://example.com/new-page",
"method": 301,
"status": "active"
}
],
"truncated": false
}
}Errors:
| Status | When |
|---|---|
400 | The code is seo_source_invalid when source is not a known plugin. The code is seo_store_unavailable when the plugin is not active, has no redirect store, or cannot be imported. |
Import an SEO plugin's redirects
POST /settings/integrations/seo/{source}/import
Imports one batch of redirects as Simple Redirects.
Permission: manage_options
Path parameters:
| Name | Type | What it is |
|---|---|---|
source | string | rankmath or seopress. |
Body parameters:
| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
offset | integer | No | 0 | The first rule of the batch. A negative value counts as 0. |
limit | integer | No | 100 | Rules in the batch. Kept between 1 and 500. |
clean_404 | boolean | No | false | Also removes the matching entries from the Rank Math 404 log, for active redirects only. Does nothing without Rank Math's 404 Monitor. |
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
imported | integer | Redirects created in this batch. |
skipped | integer | Rules not imported: a skip reason, a source that already exists, or a rejected redirect. |
failed | integer | Rules that raised an unexpected error. |
processed | integer | Rules handled so far, counting from the first. Use it as the next offset. |
total | integer | Rules examined in all. |
done | boolean | true when processed has reached total. |
messages | array of strings | Up to 25 messages for rejected or failed rules, each starting with the source URL. |
cleared_404 | integer | Rank Math 404 log rows removed. 0 when clean_404 was not set. |
bash
curl -X POST -u USER:APPLICATION_PASSWORD \
-H "Content-Type: application/json" \
-d '{"offset":0,"limit":100,"clean_404":false}' \
"https://example.com/wp-json/link-manager-pro/v1/settings/integrations/seo/rankmath/import"json
{
"success": true,
"data": {
"imported": 10,
"skipped": 2,
"failed": 0,
"processed": 12,
"total": 12,
"done": true,
"messages": [],
"cleared_404": 0
}
}Errors: the same two 400 errors as the analyze route.
Country database
The country database is a file the plugin downloads once. It lets the plugin report the country of a click. The file is stored in the uploads folder under link-manager-pro/geoip, and the folder is protected from direct access.
Get the country database status
GET /settings/geoip/status
Returns whether the database file is installed and when it was downloaded.
Permission: manage_options
Query parameters: None.
Response: 200. data is the status object:
| Field | Type | What it holds |
|---|---|---|
installed | boolean | true when the downloaded file can be read. |
size | integer | The file size in bytes. 0 when not installed. |
sha256 | string | The file checksum. Empty when not installed. |
downloaded_at | string | The download time in UTC, YYYY-MM-DD HH:MM:SS. Empty when not installed. |
source | string | The download address. |
source_label | string | The name of the data source. |
php_supported | boolean | true on PHP 8.1 or later, which the database reader needs. |
downloading | boolean | true while a download holds its lock (up to 5 minutes). |
json
{
"success": true,
"data": {
"installed": true,
"size": 5242880,
"sha256": "9f2c0e5a4b1d8c7e6f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1e",
"downloaded_at": "2026-10-20 08:30:00",
"source": "https://cdn.jsdelivr.net/npm/@ip-location-db/[email protected]/geo-whois-asn-country.mmdb",
"source_label": "ip-location-db geo-whois-asn-country (CC0-1.0), release 2.3.2026061719",
"php_supported": true,
"downloading": false
}
}Download the country database
POST /settings/geoip/download
Downloads the database, checks its checksum and format, and replaces the installed file.
Permission: manage_options
Body parameters: None.
Response: 200. data is the status object from the status route.
Errors:
| Status | When |
|---|---|
502 | The download failed. The code is geoip_download_failed and message says why, for example a download already running, a missing download address, a checksum that does not match, a file that is not a valid database, or a folder that cannot be written. |
Remove the country database
POST /settings/geoip/remove
Deletes the downloaded file and its saved details.
Permission: manage_options
Body parameters: None.
Response: 200. data is the status object from the status route.
Errors:
| Status | When |
|---|---|
500 | The file could not be deleted. The code is geoip_remove_failed. |
License
The license screen uses these routes. They hold the license key's status and the buttons to activate, deactivate, refresh and remove it. Each route returns the same license object. The screen is described in Activate your license.
The license object
Each license route returns this object, as data.license or, for GET /settings/license, as data itself.
| Field | Type | What it holds |
|---|---|---|
status | string | active when the license counts as in use, else inactive. |
state | string | The raw state: active, trial, grace, expired, suspended, revoked or inactive. |
entitled | boolean | true for the active, trial and grace states. |
recoverable_via_refresh | boolean | true when the state is not entitled, a key is saved and the state is not terminal. |
recoverable_terminal | boolean | true for a revoked license. |
has_cached_key | boolean | true when a key is saved on this site. |
last_error | string | The last validation error code, empty if none. |
status_message | string | A sentence about the state. |
license_key_masked | string | The key with all but the last four characters replaced by *. Empty when no key is saved. |
license_type | string or null | lifetime, renewal or one_time when entitled, else null. |
valid_until | string or null | The end date, YYYY-MM-DD, when entitled and known, else null. |
renewal_date | string or null | The same date as valid_until. |
activated_on | string or null | The activation date, YYYY-MM-DD, when entitled and known, else null. |
is_expired | boolean | true when the state is expired. |
is_grace | boolean | true when the state is grace. |
is_trial | boolean | true when the state is trial. |
plan | string | The plan name, empty if unknown. |
activations_used | integer | Sites using the key. |
activations_limit | integer | Sites the key allows. |
manage_url | string | The address of the account page. |
renew_url | string | The same address as manage_url. |
message | string | The last message from the license service. |
When licensing is not available on the site, every route returns the inactive object: status and state are inactive, the booleans are false, the strings are empty, the integers are 0 and the nullable fields are null.
Get the license status
GET /settings/license
Returns the license object.
Permission: manage_options
Query parameters: None.
Response: 200. data is the license object.
json
{
"success": true,
"data": {
"status": "active",
"state": "active",
"entitled": true,
"recoverable_via_refresh": false,
"recoverable_terminal": false,
"has_cached_key": true,
"last_error": "",
"status_message": "Your license is active.",
"license_key_masked": "************ABCD",
"license_type": "renewal",
"valid_until": "2027-10-20",
"renewal_date": "2027-10-20",
"activated_on": "2026-10-20",
"is_expired": false,
"is_grace": false,
"is_trial": false,
"plan": "Pro",
"activations_used": 1,
"activations_limit": 3,
"manage_url": "https://example.com/account",
"renew_url": "https://example.com/account",
"message": ""
}
}Activate a license
POST /settings/license/activate
Activates a license key for this site.
Permission: manage_options
Body parameters:
| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
license_key | string | Yes | none | The license key. Required, 8 to 255 characters. |
Response: 200, with a top-level message from the license service. data holds:
| Field | Type | What it holds |
|---|---|---|
license | object | The license object after activation. |
json
{
"success": true,
"data": { "license": { "status": "active", "...": "..." } },
"message": "License activated successfully."
}Errors:
| Status | When |
|---|---|
422 | license_key is missing or outside 8 to 255 characters. |
422 | The service refused the key. errors.license_key holds its message, for example Invalid license key. or Licensing is unavailable on this site. |
Deactivate a license
POST /settings/license/deactivate
Deactivates the license for this site.
Permission: manage_options
Body parameters: None.
Response: 200, with a top-level message. data.license is the license object. A failed deactivation answers 424 with the code license_request_failed and the service's message.
Errors:
| Status | When |
|---|---|
424 | The code is license_unavailable. This site has no license client. |
424 | The code is license_request_failed. The license service refused the call. |
Refresh the license status
POST /settings/license/refresh
Asks the license service for the current state.
Permission: manage_options
Body parameters: None.
Response: 200, with a top-level message. data.license is the license object.
Errors:
| Status | When |
|---|---|
424 | The code is license_unavailable. Licensing is unavailable on this site. |
Remove the license
POST /settings/license/remove
Removes the license from this site and deletes the local license data. If the license server cannot be reached, the message says so and tells you to release the site in your account.
Permission: manage_options
Body parameters: None.
Response: 200, with a top-level message. data.license is the license object.
Errors:
| Status | When |
|---|---|
424 | The code is license_unavailable. Licensing is unavailable on this site. |
Update links after a prefix change
After you change a prefix, old short links keep working. These routes update the links inside your posts so they use the current prefix. The update works in three steps: check if there is anything to move, review what would change, then apply it in batches. A second pair of routes lists the posts one page at a time and applies the ones you pick.
What it changes. The apply route edits post content. It rewrites the href of <a> tags in posts, pages and other scanned content types that are published, scheduled or private. A link is rewritten only when it points to this site, has the exact shape /{previous-prefix}/{slug} and the slug belongs to an existing short link. With Affiliate link prefix set, a link that leads to an affiliate's domain moves straight to the affiliate prefix. Other links, and pages under the old prefix, stay as they are. In affiliate scope, the same rewrite moves affiliate links from your link prefix, internal prefix and previous prefixes to the affiliate prefix. With the affiliate prefix empty, it moves only links from previous affiliate prefixes. The plugin saves a revision of each post first, checks that the user can edit it, and keeps auto-masking from running during the save. Page builder data stored in post meta is listed, never changed. A second run finds nothing, because the rewritten links no longer match.
All five routes take the same two optional parameters in the query string (GET) or body (POST).
| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
from | string | No | all previous prefixes | Limits the update to one previous prefix. Slashes at both ends are removed. A prefix in use now returns nothing to do. |
scope | string | No | previous mode | affiliate moves only affiliate links to the affiliate prefix (Update affiliate links in posts). Any other value, or none, moves links from previous prefixes to the current ones. In affiliate scope, from is ignored. |
Check for links to update
GET /settings/prefix-links
Tells whether any post holds links under a previous prefix.
Permission: manage_options
Query parameters: from and scope, as above.
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
found | boolean | true when at least one post has a link to move. After 500 candidate posts without a match it answers true anyway, and the review gives the real count. |
prefixes | array | The prefixes it checks. Each item has from (string) and to (string, empty when the target depends on the link). |
json
{
"success": true,
"data": {
"found": true,
"prefixes": [{ "from": "link", "to": "go-to" }]
}
}Review links to update
GET /settings/prefix-links/review
Counts the posts and links that would change, with examples. Nothing is written. It reads up to 500 candidate posts per call.
Permission: manage_options
Query parameters:
| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
after | integer | No | 0 | Continue after this post ID. Use the next value of the previous call. |
from | string | No | all | As above. |
scope | string | No | previous mode | As above. |
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
posts | integer | Posts that a save would change. |
links | integer | Links in those posts. |
blocked | integer | Posts a save would skip, for example when the user cannot edit them. |
blocked_posts | array | Up to 20 skipped posts: the post fields below plus reason (string) and links (integer). |
sample | array | Up to 10 posts: the post fields below plus links (integer) and example (object with before and after link text, or null). |
found_in | array of strings | The previous prefixes the links use. |
moved_to | object | The new prefix to the number of links that move to it. An empty list ([]) when none. |
next | integer or null | The post ID to pass as after for the next call. null at the end. |
prefixes | array | The prefixes checked, as in the check route. |
builder | object | Only on the first call (after is 0) and when there are prefixes to check. posts lists up to 20 posts whose page builder data holds a previous prefix (post fields below). more is true if there are more. |
The post fields in these lists are:
| Field | Type | What it holds |
|---|---|---|
id | integer | The post ID. |
title | string | The post title, or (no title). |
type | string | The post type's singular label. |
status | string | The post status label. |
edit_url | string | The edit screen address. |
view_url | string | The permalink. |
json
{
"success": true,
"data": {
"posts": 3,
"links": 5,
"blocked": 0,
"blocked_posts": [],
"sample": [
{
"id": 12,
"title": "Best widgets",
"type": "Post",
"status": "Published",
"edit_url": "https://example.com/wp-admin/post.php?post=12&action=edit",
"view_url": "https://example.com/best-widgets/",
"links": 2,
"example": {
"before": "https://example.com/link/blue-widget",
"after": "https://example.com/go-to/blue-widget"
}
}
],
"found_in": ["link"],
"moved_to": { "go-to": 5 },
"next": null,
"prefixes": [{ "from": "link", "to": "go-to" }],
"builder": { "posts": [], "more": false }
}
}Apply links update
POST /settings/prefix-links/apply
Rewrites the links in the next 25 posts. Call it again with next as after until next is null.
Permission: manage_options. Each post also needs edit_post for the signed-in user. A post the user cannot edit is skipped, not failed.
Body parameters:
| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
after | integer | No | 0 | Continue after this post ID. Use the next value of the previous call. |
from | string | No | all | As above. |
scope | string | No | previous mode | As above. |
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
links | integer | Links rewritten in this batch. |
posts_changed | integer | Posts saved in this batch. |
skipped | array | Posts left unchanged: the post fields from the review plus reason (string) and links (integer). A post is skipped when the user cannot edit it, WordPress would remove markup on save, it changed during the run, the save would also change its status or address, or it could not be saved. |
next | integer or null | The post ID to pass as after for the next call. null when the last batch is done. |
bash
curl -X POST -u USER:APPLICATION_PASSWORD \
-H "Content-Type: application/json" \
-d '{"after":0}' \
"https://example.com/wp-json/link-manager-pro/v1/settings/prefix-links/apply"json
{
"success": true,
"data": {
"links": 5,
"posts_changed": 3,
"skipped": [],
"next": null
}
}Errors:
| Status | When |
|---|---|
409 | The update is already running in another window. The code is prefix_links_busy. The lock lasts at most 1 minute. |
List posts to update
GET /settings/prefix-links/page
Returns the next 20 posts that have links to move, each with up to 3 of its changes. The update dialog lists them for you to tick. Nothing is written.
Permission: manage_options
Query parameters:
| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
after | integer | No | 0 | Continue after this post ID. Use the next value of the previous call. |
from | string | No | all | As above. |
scope | string | No | previous mode | As above. |
The route reads at most 500 candidate posts per call, so a page can hold fewer than 20 posts.
Response: 200. data holds:
| Field | Type | What it holds |
|---|---|---|
posts | array | The posts: the post fields from the review plus links (integer), changes (up to 3 objects with before and after link text) and blocked (string, why a save would be refused, or null). |
next | integer or null | The post ID to pass as after for the next call. null at the end. |
json
{
"success": true,
"data": {
"posts": [
{
"id": 12,
"title": "Best widgets",
"type": "Post",
"status": "Published",
"edit_url": "https://example.com/wp-admin/post.php?post=12&action=edit",
"view_url": "https://example.com/best-widgets/",
"links": 2,
"changes": [
{ "before": "https://example.com/link/blue-widget", "after": "https://example.com/go-to/blue-widget" }
],
"blocked": null
}
],
"next": null
}
}Apply the update to chosen posts
POST /settings/prefix-links/apply-posts
Rewrites the links in the posts you send, up to 20.
Permission: manage_options. Each post also needs edit_post for the signed-in user. A post the user cannot edit is skipped, not failed.
Body parameters:
| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
ids | array of integers | Yes | none | Post IDs. Only the first 20 are used. A post outside the scanned content types or statuses, or with nothing to move, is left alone. |
from | string | No | all | As above. |
scope | string | No | previous mode | As above. |
Response: 200. data holds links, posts_changed and skipped, as in Apply links update, without next.
bash
curl -X POST -u USER:APPLICATION_PASSWORD \
-H "Content-Type: application/json" \
-d '{"ids":[12,15]}' \
"https://example.com/wp-json/link-manager-pro/v1/settings/prefix-links/apply-posts"json
{
"success": true,
"data": {
"links": 2,
"posts_changed": 1,
"skipped": []
}
}Errors:
| Status | When |
|---|---|
409 | The update is already running in another window. The code is prefix_links_busy. |