# Editor, AI and setup endpoints

These routes serve the editor sidebar, the setup wizard and the support drawer. They also include the public internal click route and the two WordPress search types that list short links and affiliates.
Page: https://docs.linkmanagerpro.com/developers/rest-api/editor-ai
Use when someone asks: link manager pro ai link assistant api; internal click tracking endpoint; link picker search api
Intent: Reference

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

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

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

## AI Link Assistant

The four `/ai` routes serve the sidebar that finds plain URLs in a post and suggests internal links. They use the permission level that allows `edit_posts` (the capability the `link_manager_pro_picker_capability` filter returns) or `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions)).

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

### Find plain URLs in post content

`POST /ai/detect-urls`

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

**Permission:** `edit_posts` or `link_manager_pro_access`, and `edit_post` on `post_id` when it is set ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Body parameters:** (JSON)

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `content` | string | No | empty | The post content, as HTML or text. The route reads `href` links and bare `http` and `https` addresses. |
| `post_id` | integer | No | `0` | The post being edited. Addresses you dismissed for this post are left out. |

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

**Response:** `200`. `data` holds:

| Field | Type | What it holds |
|---|---|---|
| `enabled` | boolean | `false` when the setting `ai.enable_untracked_detection` is off. It is on by default. Then `urls` is empty. |
| `urls` | array | One object per address, fields below. |

Each object in `urls`:

| Field | Type | What it holds |
|---|---|---|
| `url` | string | The address. |
| `is_internal` | boolean | `true` for an address on this site (the `www.` prefix is ignored), `false` for any other host. |

```bash
curl -X POST \
  -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{"post_id":123,"content":"<p>See <a href=\"https://example.net/deal\">this deal</a>.</p>"}' \
  "https://example.com/wp-json/link-manager-pro/v1/ai/detect-urls"
```

```json
{
  "success": true,
  "data": {
    "enabled": true,
    "urls": [
      { "url": "https://example.net/deal", "is_internal": false }
    ]
  }
}
```

**Errors:**

| Status | When |
|---|---|
| `403` | The user cannot edit the post in `post_id`. The code is `lmp_ai_post_forbidden`. |

### Convert a URL to a short link

`POST /ai/convert-url`

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

**Permission:** `edit_posts` or `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions)). This route has no per-post check. To create a new short link, a caller without full access also needs the create right: `publish_posts` by default, or the capability the `link_manager_pro_picker_create_capability` filter returns.

**Body parameters:** (JSON)

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `url` | string | Yes | none | The destination. It must be a valid `http` or `https` address, at most 2048 characters. |

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

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

| Field | Type | What it holds |
|---|---|---|
| `link` | object | The link: `id` (integer) and `slug` (string). |
| `masked_url` | string | The public short URL to put in the post, under the prefix that fits the destination. |

```json
{
  "success": true,
  "data": {
    "link": { "id": 42, "slug": "blue-widget" },
    "masked_url": "https://example.com/link/blue-widget"
  }
}
```

**Errors:**

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

### Dismiss a URL for a post

`POST /ai/dismiss-url`

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

**Permission:** `edit_posts` or `link_manager_pro_access`, and `edit_post` on `post_id` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Body parameters:** (JSON)

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `url` | string | Yes | none | The address to hide. Case, a trailing slash and a default port do not matter. |
| `post_id` | integer | Yes | `0` | The post. It must be above 0. |

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

**Response:** `200`. `data` holds:

| Field | Type | What it holds |
|---|---|---|
| `dismissed` | boolean | Always `true`. |

**Errors:**

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

### Suggest internal links

`POST /ai/analyze`

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

**Permission:** `edit_posts` or `link_manager_pro_access`, and `edit_post` on `post_id` when it is set ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Body parameters:** (JSON)

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `content` | string | Yes | empty | The post content. Tags are removed and the text is cut to 8000 characters. Content that is empty once tags are removed returns `empty`, described below. |
| `post_id` | integer | No | `0` | The post. Used for the permission check only. |
| `selection` | string | No | empty | Text the user selected. Suggestions that match it rank first. Leave it empty to analyze the whole post. |

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

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

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

**Response:** `200`. A successful analysis holds:

| Field | Type | What it holds |
|---|---|---|
| `available` | boolean | `true`. |
| `enabled` | boolean | `true`. |
| `suggestions` | array | Up to 8 objects, fields below. |
| `total_candidates` | integer | How many short links matched and were offered to the provider. |
| `cached` | boolean | `true` when served from the one-hour cache. |
| `empty` | boolean | Only on the empty-content answer, where it is `true` and `suggestions` is empty. |

Each suggestion:

| Field | Type | What it holds |
|---|---|---|
| `link_id` | integer | The short link ID. |
| `anchor` | string | A phrase copied from the content. The route keeps only phrases that appear in it. |
| `slug` | string | The link's slug. |
| `target_url` | string | The link's destination. |
| `masked_url` | string | The public short URL. |
| `click_count` | integer | The link's click total. Left out for users without `link_manager_pro_access`. |
| `best_match` | boolean | `true` for the first suggestion only. |

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

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

**Errors:**

| Status | When |
|---|---|
| `403` | The user cannot edit the post in `post_id` (`lmp_ai_post_forbidden`). |
| `429` | The user made too many uncached requests. The code is `lmp_ai_rate_limited`. |

A cached answer does not count toward the limit, and neither does a call with no matching short link. The limit is per user: by default 30 requests in 300 seconds, set by the settings `ai.rate_limit_max` (0 to 100000) and `ai.rate_limit_window` (1 to 86400 seconds). The [`link_manager_pro_ai_rate_max`](https://docs.linkmanagerpro.com/developers/hooks/filters#link-manager-pro-ai-rate-max) filter can change the limit. A limit of 0 turns it off.

## Setup

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

The state object that these routes return:

| Field | Type | What it holds |
|---|---|---|
| `status` | string | `pending`, `complete` or `skipped`. |
| `step` | integer | The current step, 0 to 3. |
| `steps` | object | `welcome`, `settings`, `privacy` and `scan`, each `pending` or `complete`. |
| `updated_at` | string | When the state was last saved, as an ISO 8601 date in UTC. |
| `completed_at` | string | Only after `POST /onboarding/complete`. |
| `skipped_at` | string | Only after `POST /onboarding/skip`. |

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

### Get the setup state

`GET /onboarding`

Returns the setup state and the settings the wizard shows.

**Permission:** `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Query parameters:** None.

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

| Field | Type | What it holds |
|---|---|---|
| `settings` | object | The groups `general`, `tracking` and `redirection`. An administrator gets every key of each group. Other users get only `general.link_prefix`, `redirection.redirect_type`, and `tracking.track_clicks`, `tracking.enable_bot_detection` and `tracking.anonymize_ips`. |

```json
{
  "success": true,
  "data": {
    "status": "pending",
    "step": 0,
    "steps": { "welcome": "pending", "settings": "pending", "privacy": "pending", "scan": "pending" },
    "updated_at": "2026-10-23T09:00:00+00:00",
    "settings": {
      "general": { "link_prefix": "link" },
      "tracking": { "track_clicks": "yes", "enable_bot_detection": "yes", "anonymize_ips": "yes" },
      "redirection": { "redirect_type": 301 }
    }
  }
}
```

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

### Reset the setup

`POST /onboarding/status`

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

**Permission:** `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Body parameters:** (JSON)

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `status` | string | Yes | empty | Must be `pending`. |

**Response:** `200`. `data` is the fresh [state object](#setup), with `status` `pending`, `step` 0 and every step `pending`.

**Errors:**

| Status | When |
|---|---|
| `422` | `status` is anything but `pending`. The body lists the field `status`. |

### Mark a step complete

`POST /onboarding/step`

Marks one step complete and moves `step` to it.

**Permission:** `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Body parameters:** (JSON)

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `step` | integer | Yes | none | The step number, 0 to 3: `0` welcome, `1` settings, `2` privacy, `3` scan. |
| `state` | string | Yes | empty | Must be `complete`. |

**Response:** `200`. `data` is the updated [state object](#setup). When all four steps are complete, `status` is `complete`.

**Errors:**

| Status | When |
|---|---|
| `422` | `step` is not a number from 0 to 3 (field `step`), or `state` is not `complete` (field `state`). |

### Scan posts in chunks

`POST /onboarding/scan`

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

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

**Permission:** `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Body parameters:** (JSON)

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `offset` | integer | No | `0` | How many posts to skip. Use `nextOffset` from the last answer. A negative value counts as 0. |
| `limit` | integer | No | `20` | Posts per chunk. A value above 100 counts as 100. |

**Response:** `200`. `data` holds:

| Field | Type | What it holds |
|---|---|---|
| `status` | string | `in_progress` or `completed`. |
| `processed` | integer | Posts scanned so far, counting from the first post. |
| `nextOffset` | integer | The `offset` for the next call. |
| `total` | integer | Posts the scan covers. |
| `hasMore` | boolean | `true` when another call is needed. |
| `time_limited` | boolean | `true` when the 20 second budget stopped the chunk early. |
| `linksCreated` | integer | New short links created in this chunk. |
| `linksConverted` | integer | Links rewritten into short links in this chunk. |
| `postsConverted` | integer | Posts where at least one link was converted. |
| `postsUpdated` | integer | Posts saved. |
| `postsSkipped` | integer | Posts left as they were, for example when WordPress would not allow the save. |
| `postsFailed` | integer | Posts that could not be processed. |
| `linksKeptForBlocks` | integer | Links left as written because their block keeps the address. |

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

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

### Save the setup settings

`POST /onboarding/settings`

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

**Permission:** `manage_options` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Body parameters:** (JSON)

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `general` | object | No | none | Keys of the `general` settings group, such as `link_prefix`. A group sent empty or left out is not saved. |
| `tracking` | object | No | none | Keys of the `tracking` group. |
| `redirection` | object | No | none | Keys of the `redirection` group. |

Each group follows the same schema as its settings screen. Values are sanitized, and keys the group does not define are dropped. `affiliate_prefix` is optional and empty by default. The prefix rules for `link_prefix`, `internal_prefix` and `affiliate_prefix` run first. A broken prefix rejects the whole request and saves nothing. The wizard pre-fills `affiliate_prefix` from `affiliate_prefix_suggestion` of [Detect other plugins](https://docs.linkmanagerpro.com/developers/rest-api/backup-migration#detect-other-plugins).

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

**Response:** `200`. `data` holds:

| Field | Type | What it holds |
|---|---|---|
| `state` | object | The updated [state object](#setup). |
| `warnings` | object | Prefix warnings keyed `general.<key>`, each a plain sentence. Empty when none. |

```json
{
  "success": true,
  "data": {
    "state": {
      "status": "pending",
      "step": 1,
      "steps": { "welcome": "pending", "settings": "complete", "privacy": "pending", "scan": "pending" },
      "updated_at": "2026-10-23T09:05:00+00:00"
    },
    "warnings": {}
  }
}
```

**Errors:**

| Status | When |
|---|---|
| `422` | A submitted prefix breaks a rule. The body lists each field as `general.link_prefix`, `general.internal_prefix` or `general.affiliate_prefix`. |

### Finish the setup

`POST /onboarding/complete`

Closes setup for the site, whichever steps were used.

**Permission:** `manage_options` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Body parameters:** None.

**Response:** `200`. `data` is the [state object](#setup) with `status` `complete`, `step` 3, the `scan` step `complete` and `completed_at` set.

### Skip the setup

`POST /onboarding/skip`

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

**Permission:** `manage_options` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Body parameters:** None.

**Response:** `200`. `data` is the [state object](#setup) with `status` `skipped` and `skipped_at` set.

## System and terms

### Get system information

`GET /system-info`

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

**Permission:** `manage_options` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Query parameters:** None.

**Response:** `200`. `data` holds three objects.

`environment`, all values strings:

| Field | Type | What it holds |
|---|---|---|
| `plugin_version` | string | The Link Manager Pro version. |
| `license_state` | string | The license state, `inactive` when none is known. |
| `wordpress_version` | string | The WordPress version. |
| `php_version` | string | The PHP version. |
| `mysql_version` | string | The database server version, or an empty string. |
| `memory_limit` | string | The WordPress memory limit, such as `256M`. |
| `max_upload_size` | string | The largest upload WordPress takes, such as `64 MB`. |
| `server_software` | string | The web server name from `SERVER_SOFTWARE`, or an empty string. |
| `multisite` | string | `Yes` or `No`. |
| `locale` | string | The site locale, such as `en_US`. |

`themes`:

| Field | Type | What it holds |
|---|---|---|
| `active` | string | The active theme's name and version. |
| `total` | integer | Installed themes. |
| `items` | array | Objects with `name` (string), `version` (string) and `active` (boolean). The active theme comes first, then the rest by name. |

`plugins`:

| Field | Type | What it holds |
|---|---|---|
| `active_count` | integer | Active plugins. On multisite this includes network-active ones. |
| `total` | integer | Installed plugins. |
| `items` | array | Objects with `name` (string), `version` (string) and `active` (boolean). Active plugins come first, then the rest by name. |

```json
{
  "success": true,
  "data": {
    "environment": {
      "plugin_version": "1.3.0",
      "license_state": "active",
      "wordpress_version": "6.8",
      "php_version": "8.2.12",
      "mysql_version": "8.0.36",
      "memory_limit": "256M",
      "max_upload_size": "64 MB",
      "server_software": "nginx/1.25.3",
      "multisite": "No",
      "locale": "en_US"
    },
    "themes": {
      "active": "Twenty Twenty-Five 1.0",
      "total": 2,
      "items": [ { "name": "Twenty Twenty-Five", "version": "1.0", "active": true } ]
    },
    "plugins": {
      "active_count": 3,
      "total": 4,
      "items": [ { "name": "Link Manager Pro", "version": "1.3.0", "active": true } ]
    }
  }
}
```

### List post categories and tags

`GET /wp-terms`

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

**Permission:** `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions))

**Query parameters:** None.

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

| Field | Type | What it holds |
|---|---|---|
| `id` | integer | The term ID. |
| `name` | string | The term name. |
| `taxonomy` | string | `category` or `post_tag`. |
| `group` | string | The group label, `Categories` or `Tags`, translated. |

```json
{
  "success": true,
  "data": [
    { "id": 3, "name": "Guides", "taxonomy": "category", "group": "Categories" },
    { "id": 9, "name": "Reviews", "taxonomy": "post_tag", "group": "Tags" }
  ]
}
```

## Internal click tracking

### Record an internal click

`POST /track/internal-click`

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

**Permission:** none. The route checks its own guards, below ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions)).

The route runs these checks in this order:

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

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

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

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

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `source_post_id` | integer | Yes | `0` | The post that holds the link. |
| `target_url` | string | Yes | empty | The clicked address, as the browser resolved it. |
| `anchor_text` | string | No | empty | The link text. The script sends up to 500 characters. |
| `lmp_nonce` | string | Logged-in visitors only | none | The post-scoped nonce from `lmpTrackerConfig.nonce`. |
| `_wpnonce` | string | No | none | The `wp_rest` nonce from `lmpTrackerConfig.restNonce`. Only for a logged-in visitor, and WordPress core checks it. |

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

**Response:** `200`. `data` holds:

| Field | Type | What it holds |
|---|---|---|
| `ok` | boolean | Always `true`. |

```json
{
  "success": true,
  "data": { "ok": true }
}
```

**Errors:**

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

## WordPress search

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

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

### Search short links

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

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

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

**Query parameters:**

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `type` | string | Yes | none | `link-manager-pro`. |
| `search` | string | No | empty | Matches the slug or a keyword. With no password, it matches the destination too. Empty returns every eligible link. |
| `page` | integer | No | `1` | The page number. |
| `per_page` | integer | No | `10` | Results per page. Core's default. |

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

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

| Field | Type | What it holds |
|---|---|---|
| `id` | integer | The short link ID. |
| `title` | string | The slug. |
| `url` | string | The public short URL. |
| `type` | string | `link-manager-pro`. |

```bash
curl -u USER:APPLICATION_PASSWORD \
  "https://example.com/wp-json/wp/v2/search?type=link-manager-pro&search=widget&per_page=5"
```

```json
[
  {
    "id": 42,
    "title": "blue-widget",
    "url": "https://example.com/link/blue-widget",
    "type": "link-manager-pro"
  }
]
```

### Search affiliates

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

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

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

**Query parameters:** the same as [Search short links](#search-short-links), with `type` set to `link-manager-pro-affiliate`. `search` matches an affiliate the way the Affiliates list does.

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

| Field | Type | What it holds |
|---|---|---|
| `id` | integer | The affiliate ID. |
| `title` | string | The site name, else the domain, else the affiliate link. |
| `url` | string | The tracked short URL for the affiliate. An affiliate with no link returns an empty string. |
| `type` | string | `link-manager-pro-affiliate`. |

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

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

The [`link_manager_pro_picker_item`](https://docs.linkmanagerpro.com/developers/hooks/filters#link-manager-pro-picker-item) filter can change each item before it is returned.

## Related

- [REST API overview](https://docs.linkmanagerpro.com/developers/rest-api/)
- [All endpoints](https://docs.linkmanagerpro.com/developers/rest-api/endpoints)
- [Filter hooks](https://docs.linkmanagerpro.com/developers/hooks/filters)
- [Short links endpoints](https://docs.linkmanagerpro.com/developers/rest-api/links)
