# Redirects endpoints

These REST routes manage redirects that send an old address on your site to a new URL. They also review addresses that stopped matching after a site move.
Page: https://docs.linkmanagerpro.com/developers/rest-api/redirects
Use when someone asks: link manager pro redirects api; create 301 redirect rest api wordpress; list redirects api
Intent: Reference

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

All paths are relative to the base URL `/wp-json/link-manager-pro/v1`. The examples use `https://example.com` and the Application Password placeholder `USER:APPLICATION_PASSWORD`.

## The redirect object

A redirect is one row in `link_manager_pro_redirects` ([Database tables](https://docs.linkmanagerpro.com/developers/data/tables)). It holds the old address. The destination, the redirect type and the click count live on a hidden short link that the plugin creates for the redirect. That link never shows in the short links list. Its ID is `backing_link_id`.

When a visitor opens the old address, the plugin finds the redirect and sends the visitor through the hidden link. Every visit is a click on that link, so the redirect has a click report.

The redirect routes return the stored columns plus four fields taken from the hidden link:

| Field | Type | What it holds |
|---|---|---|
| `id` | integer | The redirect ID. |
| `source_url` | string | The old address in its saved form: `scheme://host/path`, with `?query` when the source has one. |
| `source_url_hash` | string | The `md5` of `source_url`, 32 characters. The plugin looks redirects up by this hash. |
| `target_link_id` | integer | The ID of the hidden short link. |
| `status` | string | `active` or `inactive`. Only an active redirect fires. |
| `created_at` | string | When the redirect was created, as `Y-m-d\TH:i:s.v`. |
| `updated_at` | string | When it was last saved, in the same format. |
| `redirect_type` | integer or `null` | `301`, `302` or `307`, read from the hidden link. `null` when that link is missing. |
| `target_url` | string or `null` | The destination, read from the hidden link. `null` when that link is missing. |
| `hits` | integer | The hidden link's click counter. `0` when the link is missing. |
| `backing_link_id` | integer | The same value as `target_link_id`. Use it with the `/links/{id}/report` and `/links/{id}/clicks` routes ([Short links endpoints](https://docs.linkmanagerpro.com/developers/rest-api/links)) to report on the redirect. |

### How a source is saved and matched

When you save a source, the plugin turns it into one saved form. The same rules apply when a visitor's address is compared to the saved sources.

- **Host.** A source must be on this site, with or without `www.`. The saved form always uses the site's current scheme and host and drops the port. A request over `http` or `https`, with or without `www.`, matches the same redirect.
- **Trailing slash.** It is dropped. `/old-page` and `/old-page/` are the same source.
- **Case.** The path keeps its letter case and is compared exactly, so `/Old-Page` and `/old-page` are different sources. A redirect imported from BetterLinks can be marked to match in any case. For such a redirect, the plugin retries the request with a lowercased path.
- **Encoded characters.** Each path segment is decoded once and encoded again with upper-case hex. A typed `/blog/привет`, a pasted `%d0%bf...` form and the browser's `%D0%BF...` form are one source.
- **Query string.** The saved form keeps the query with its keys sorted and empty values dropped, so `/?p=390` never turns into the home page. A request matches a source with a query only when its query is the same after the same clean-up. A source saved without a query matches its path with any query, such as `?utm_source=news`.
- **Hash.** A fragment (`#section`) is dropped when you save a source. Browsers never send it to the server, so it cannot take part in matching.
- **Home page.** A source that resolves to the home page is refused.

Redirects saved before an address move keep matching. The plugin also tries this site's other scheme and `www.` variants, and any earlier address it recorded. [Review old addresses](#review-old-addresses) explains the earlier addresses.

## Redirects

### List redirects

`GET /redirects`

Returns one page of redirects, newest first, with their destination, type and hit count.

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

**Query parameters:**

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `search` | string | No | none | Matches part of the source URL or of the destination URL. |
| `status` | string | No | none | `active` or `inactive`. Any other value shows both. |
| `page` | integer | No | `1` | The page number. |
| `per_page` | integer | No | `15` | Redirects per page. |

The list holds only redirects that own their hidden link. Rows that Link Health created to fix a broken link are left out.

**Response:** `200`. `data` is a page object ([Pagination](https://docs.linkmanagerpro.com/developers/rest-api/#pagination)). `data.data` holds [redirect objects](#the-redirect-object).

```bash
curl -u USER:APPLICATION_PASSWORD \
  "https://example.com/wp-json/link-manager-pro/v1/redirects?status=active"
```

```json
{
  "success": true,
  "data": {
    "current_page": 1,
    "data": [
      {
        "id": 5,
        "source_url": "https://example.com/old-page",
        "source_url_hash": "4b5c31ad44d7dec24d24f4e8ff46c481",
        "target_link_id": 61,
        "status": "active",
        "created_at": "2026-10-10T12:00:00.000",
        "updated_at": "2026-10-10T12:00:00.000",
        "redirect_type": 301,
        "target_url": "https://example.com/new-page",
        "hits": 17,
        "backing_link_id": 61
      }
    ],
    "first_page_url": "/wp-json/link-manager-pro/v1/redirects?page=1",
    "from": 1,
    "last_page": 1,
    "last_page_url": "/wp-json/link-manager-pro/v1/redirects?page=1",
    "links": [
      { "url": null, "label": "Previous", "active": false },
      { "url": "/wp-json/link-manager-pro/v1/redirects?page=1", "label": 1, "active": true },
      { "url": null, "label": "Next", "active": false }
    ],
    "next_page_url": null,
    "path": "/wp-json/link-manager-pro/v1/redirects",
    "per_page": 15,
    "prev_page_url": null,
    "to": 1,
    "total": 1
  }
}
```

**Errors:** None beyond the overview's sign-in and permission errors.

### Create a redirect

`POST /redirects`

Creates a redirect from an address on this site to a destination URL. It also creates the hidden short link and returns the redirect.

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

**Body parameters:** JSON.

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `source_url` | string | Yes | none | A path or a full URL on this site, 2048 bytes or fewer. The plugin saves the form described in [How a source is saved and matched](#how-a-source-is-saved-and-matched). |
| `target_url` | string | Yes | none | The destination. A full `http` or `https` URL, 2048 bytes or fewer. |
| `redirect_type` | integer | Yes | none | `301`, `302` or `307`. |
| `status` | string | No | `active` | `active` or `inactive`. |

**Response:** `200`, with `message` set to "Redirect created." `data` holds:

| Field | Type | What it holds |
|---|---|---|
| `redirect` | object | The new [redirect object](#the-redirect-object). |
| `address_takeover` | array | Short links whose other address this redirect now answers. Empty when none, and always empty when `status` is `inactive`. |

Each row of `address_takeover` holds:

| Field | Type | What it holds |
|---|---|---|
| `path` | string | The other address, such as `/deal`. |
| `link_id` | integer | The short link that has this address. |
| `slug` | string | That link's slug. |
| `from` | string | The plugin the address was imported from: `Pretty Links`, `BetterLinks` or `ThirstyAffiliates`. An empty string when someone added it by hand. |
| `live` | boolean | Always `true` for a redirect, because an active redirect answers the address at once. |

A redirect answers before a short link's other address, so the short link's address stops opening. [Other addresses and conflicts](https://docs.linkmanagerpro.com/guides/other-addresses) explains the order. The list is empty when no short link has other addresses.

```bash
curl -X POST -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{
    "source_url": "/old-page",
    "target_url": "https://example.com/new-page",
    "redirect_type": 301
  }' \
  https://example.com/wp-json/link-manager-pro/v1/redirects
```

```json
{
  "success": true,
  "data": {
    "redirect": {
      "source_url": "https://example.com/old-page",
      "source_url_hash": "4b5c31ad44d7dec24d24f4e8ff46c481",
      "target_link_id": 61,
      "status": "active",
      "updated_at": "2026-10-23T09:30:00.000",
      "created_at": "2026-10-23T09:30:00.000",
      "id": 5,
      "redirect_type": 301,
      "target_url": "https://example.com/new-page",
      "hits": 0,
      "backing_link_id": 61
    },
    "address_takeover": []
  },
  "message": "Redirect created."
}
```

**Errors:**

| Status | When |
|---|---|
| `422` | A required field is missing, `target_url` is not a valid URL, `redirect_type` is not `301`, `302` or `307`, `status` is not `active` or `inactive`, or a value is too long. The body is the standard `422` with `errors` keyed by field. |
| `422` | The plugin refuses the source or the destination. The body has `code` set to `lmp_redirect_invalid`, `data.status` set to `422`, `message` set to the first problem, and `errors` keyed by `source_url` or `target_url`. |

The plugin refuses these cases. Each field shows its first message only.

| Field | Message |
|---|---|
| `source_url` | "A source URL is required." |
| `source_url` | "The source URL must be on this site." |
| `source_url` | "You can’t redirect the home page." |
| `source_url` | "An active redirect for this source URL already exists." The check covers every saved form of the address, including earlier addresses of the site. |
| `target_url` | "Enter a full URL, starting with https://" |
| `target_url` | "The source and target URLs cannot be the same." |

### Get a redirect

`GET /redirects/{id}`

Returns one redirect.

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

**Path parameters:**

| Name | Type | What it is |
|---|---|---|
| `id` | integer | The redirect ID. |

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

| Field | Type | What it holds |
|---|---|---|
| `redirect` | object | The [redirect object](#the-redirect-object). |

```bash
curl -u USER:APPLICATION_PASSWORD \
  https://example.com/wp-json/link-manager-pro/v1/redirects/5
```

```json
{
  "success": true,
  "data": {
    "redirect": {
      "id": 5,
      "source_url": "https://example.com/old-page",
      "source_url_hash": "4b5c31ad44d7dec24d24f4e8ff46c481",
      "target_link_id": 61,
      "status": "active",
      "created_at": "2026-10-10T12:00:00.000",
      "updated_at": "2026-10-10T12:00:00.000",
      "redirect_type": 301,
      "target_url": "https://example.com/new-page",
      "hits": 17,
      "backing_link_id": 61
    }
  }
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | No redirect has that ID, or the row points to a short link that it does not own. The body is `{ "code": "lmp_redirect_not_found", "message": "Redirect not found.", "data": { "status": 404 } }`. |

### Update a redirect

`PUT /redirects/{id}`

Saves a redirect and returns it. The body needs the same required fields as create.

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

**Path parameters:**

| Name | Type | What it is |
|---|---|---|
| `id` | integer | The redirect ID. |

**Body parameters:** JSON. The same fields as [Create a redirect](#create-a-redirect). A missing `status` leaves the saved status as it is. When the redirect's hidden short link is gone, `target_url` is required and the plugin creates a new hidden link. The response then has a new `target_link_id` and `backing_link_id`, and `hits` is `0`. The source is saved again in its normal form, and the check for another active redirect with the same source skips this redirect. Nothing is saved when any field fails.

**Response:** `200`, with `message` set to "Redirect updated." `data` has the same shape as the create response: `redirect` and `address_takeover`. `address_takeover` is empty when the saved `status` is `inactive`.

```bash
curl -X PUT -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{
    "source_url": "/old-page",
    "target_url": "https://example.com/new-page",
    "redirect_type": 302,
    "status": "inactive"
  }' \
  https://example.com/wp-json/link-manager-pro/v1/redirects/5
```

```json
{
  "success": true,
  "data": {
    "redirect": {
      "id": 5,
      "source_url": "https://example.com/old-page",
      "source_url_hash": "4b5c31ad44d7dec24d24f4e8ff46c481",
      "target_link_id": 61,
      "status": "inactive",
      "created_at": "2026-10-10T12:00:00.000",
      "updated_at": "2026-10-23T09:35:00.000",
      "redirect_type": 302,
      "target_url": "https://example.com/new-page",
      "hits": 17,
      "backing_link_id": 61
    },
    "address_takeover": []
  },
  "message": "Redirect updated."
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | No redirect has that ID, or the row is a short link's address row. The body is `{ "code": "lmp_redirect_not_found", "message": "Redirect not found.", "data": { "status": 404 } }`. |
| `422` | The same field errors as create. |

### Delete a redirect

`DELETE /redirects/{id}`

Deletes the redirect, its hidden short link and that link's clicks. A row that points to a link it does not own loses only the row.

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

**Path parameters:**

| Name | Type | What it is |
|---|---|---|
| `id` | integer | The redirect ID. |

**Response:** `200`, with `message` set to "Redirect deleted." `data` holds:

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

```bash
curl -X DELETE -u USER:APPLICATION_PASSWORD \
  https://example.com/wp-json/link-manager-pro/v1/redirects/5
```

```json
{
  "success": true,
  "data": { "deleted": true },
  "message": "Redirect deleted."
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | No redirect has that ID. The body is the WordPress error `not_found`. |

## Review old addresses

Some redirects stop matching after a site move, a search-and-replace or an update from an older version. The review routes find them. Every review route returns the same object, described next, so you can call one and read the result.

The plugin keeps a list of the addresses the site had before, up to four. It saves them in the option `link_manager_pro_site_bases`, as `scheme://host` with no port. Redirects saved under one of these addresses keep matching. The list is the `site_bases` field below.

A finished review is kept for a day and an unfinished one for 10 minutes. It is refreshed whenever a redirect changes, and the next call continues from where the check stopped.

| Field | Type | What it holds |
|---|---|---|
| `shadowing` | array | Active redirects whose source is also a published page. Each row: `id` (integer), `source_url` (string) and `page` (the page title). Most come from an earlier version that saved a source without its encoded characters, so `/blog/привет/` became `/blog`. The redirect now sends visitors away from the page. The plugin cannot recover the original address. |
| `shadowing_complete` | boolean | `false` until every active redirect has been checked. The check resumes on the next call. |
| `other_address` | array | Active redirects whose source host is neither this site's nor a recorded earlier address, grouped by host. Each row: `base` (`scheme://host`) and `count` (integer). Such redirects do nothing today. |
| `key_mismatch` | array | Redirects whose `source_url_hash` no longer equals the `md5` of `source_url`, for example after a search-and-replace on the database. Each row: `id` and `source_url`. |
| `dangling` | array | Active redirects whose hidden short link is gone, for example after a partial restore. Each row: `id` and `source_url`. The plugin never turns them off. |
| `site_bases` | array of strings | The recorded earlier addresses, oldest first. |

`shadowing`, `key_mismatch` and `dangling` hold at most 50 rows each. The `shadowing` check stops after about 5 seconds per call, and `shadowing_complete` is `false` until it has covered every active redirect.

```json
{
  "shadowing": [
    { "id": 8, "source_url": "https://example.com/blog", "page": "Blog" }
  ],
  "shadowing_complete": true,
  "other_address": [
    { "base": "https://old.example.net", "count": 12 }
  ],
  "key_mismatch": [
    { "id": 9, "source_url": "https://example.com/moved" }
  ],
  "dangling": [],
  "site_bases": ["https://old.example.org"]
}
```

### List redirects to review

`GET /redirects/review/sources`

Returns the review object. It changes nothing.

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

**Query parameters:** None.

**Response:** `200`. `data` is [the review object](#review-old-addresses).

```bash
curl -u USER:APPLICATION_PASSWORD \
  https://example.com/wp-json/link-manager-pro/v1/redirects/review/sources
```

```json
{
  "success": true,
  "data": {
    "shadowing": [],
    "shadowing_complete": true,
    "other_address": [{ "base": "https://old.example.net", "count": 12 }],
    "key_mismatch": [],
    "dangling": [],
    "site_bases": []
  }
}
```

**Errors:** None beyond the overview's sign-in and permission errors.

### Adopt an earlier address

`POST /redirects/review/adopt`

Records an address from `other_address` as one of the site's own earlier addresses, so the redirects saved under it start to match.

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

**Body parameters:** JSON.

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `base` | string | Yes | none | An address, such as `https://old.example.net`. Only the scheme and host are used, in lower case. It must equal the `base` of a row in `other_address`. |

The plugin keeps at most four earlier addresses. Adding a fifth drops the oldest. It then clears the redirect caches.

**Response:** `200`. `data` is [the review object](#review-old-addresses), read again after the change. The adopted address is in `site_bases` and no longer in `other_address`.

```bash
curl -X POST -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{ "base": "https://old.example.net" }' \
  https://example.com/wp-json/link-manager-pro/v1/redirects/review/adopt
```

```json
{
  "success": true,
  "data": {
    "shadowing": [],
    "shadowing_complete": true,
    "other_address": [],
    "key_mismatch": [],
    "dangling": [],
    "site_bases": ["https://old.example.net"]
  }
}
```

**Errors:**

| Status | When |
|---|---|
| `422` | `errors.base`: "Choose an address." The value is empty, is not an `http` or `https` address, or is not listed in `other_address`. |

### Forget an earlier address

`POST /redirects/review/forget`

Removes an address from the recorded earlier addresses. Redirects saved under it stop matching.

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

**Body parameters:** JSON.

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `base` | string | Yes | none | An entry of `site_bases`, written exactly as the review shows it. A value that is not recorded changes nothing. |

**Response:** `200`. `data` is [the review object](#review-old-addresses), read again after the change.

```bash
curl -X POST -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{ "base": "https://old.example.net" }' \
  https://example.com/wp-json/link-manager-pro/v1/redirects/review/forget
```

```json
{
  "success": true,
  "data": {
    "shadowing": [],
    "shadowing_complete": true,
    "other_address": [{ "base": "https://old.example.net", "count": 12 }],
    "key_mismatch": [],
    "dangling": [],
    "site_bases": []
  }
}
```

**Errors:** None beyond the overview's sign-in and permission errors.

### Re-save a redirect's key

`POST /redirects/{id}/resave`

Fixes one row of `key_mismatch`. The plugin stores the hash that matches the saved address. The address text stays as it is.

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

**Path parameters:**

| Name | Type | What it is |
|---|---|---|
| `id` | integer | The redirect ID. |

**Body parameters:** None.

**Response:** `200`. `data` is [the review object](#review-old-addresses), read again after the change.

```bash
curl -X POST -u USER:APPLICATION_PASSWORD \
  https://example.com/wp-json/link-manager-pro/v1/redirects/9/resave
```

```json
{
  "success": true,
  "data": {
    "shadowing": [],
    "shadowing_complete": true,
    "other_address": [],
    "key_mismatch": [],
    "dangling": [],
    "site_bases": []
  }
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | No redirect has that ID. The body is the WordPress error `not_found`. |
| `422` | `errors.source_url`: "An active redirect for this source URL already exists." Another active redirect already holds that key. |

## Related

- [REST API overview](https://docs.linkmanagerpro.com/developers/rest-api/)
- [Short links endpoints](https://docs.linkmanagerpro.com/developers/rest-api/links)
- [Affiliates endpoints](https://docs.linkmanagerpro.com/developers/rest-api/affiliates)
- [Redirects](https://docs.linkmanagerpro.com/features/redirects)
- [Other addresses and conflicts](https://docs.linkmanagerpro.com/guides/other-addresses)
