# 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.
Page: https://docs.linkmanagerpro.com/developers/rest-api/settings
Use when someone asks: link manager pro settings api; update link manager pro settings rest; settings keys and defaults
Intent: Reference

Sign-in and the response envelope are in the [REST API overview](https://docs.linkmanagerpro.com/developers/rest-api/).

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](https://docs.linkmanagerpro.com/developers/rest-api/#permissions)). The backup routes and the import from other plugins are on [Backup and import endpoints](https://docs.linkmanagerpro.com/developers/rest-api/backup-migration).

## 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 through `sanitize_text_field`, and textarea keys through `sanitize_textarea_field`.
- **Empty body:** a body with no fields saves nothing and still answers `200`.
- **Response:** a save answers `200` with `data` set to `null`, except `POST /settings/general`, which returns `warnings`.
- **Hook:** after a save, the plugin runs the action `link_manager_pro_settings_updated` with the group name, the new values and the old values, secrets masked as described under Secrets below ([Action hooks](https://docs.linkmanagerpro.com/developers/hooks/actions)). Plugins can also add keys to the schema with the `link_manager_pro_settings_schema` filter, 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](https://docs.linkmanagerpro.com/developers/rest-api/#responses)). `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](#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`, `tag` or `trackback`.
- The three prefixes must differ from each other.
- `link_prefix` and `internal_prefix` cannot be empty. `affiliate_prefix` can.

**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](#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](#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 (`a@example.com, b@example.com`).

### 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](#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](https://docs.linkmanagerpro.com/guides/access). |
| `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](#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](#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](#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](#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](#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](#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](#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](https://docs.linkmanagerpro.com/developers/rest-api/#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/geo-whois-asn-country-mmdb@2.3.2026061719/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](https://docs.linkmanagerpro.com/getting-started/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](#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](#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](#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](#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](#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](#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`. |

## Related

- [REST API overview](https://docs.linkmanagerpro.com/developers/rest-api/)
- [Short links endpoints](https://docs.linkmanagerpro.com/developers/rest-api/links)
- [All endpoints](https://docs.linkmanagerpro.com/developers/rest-api/endpoints)
- [Action hooks](https://docs.linkmanagerpro.com/developers/hooks/actions)
- [All settings](https://docs.linkmanagerpro.com/guides/settings)
