# Short links endpoints

Call these REST routes to create, read, update and delete short links. They also manage a link's routing rules, other addresses, categories and tags. Each entry lists the permission, parameters, response fields and errors.
Page: https://docs.linkmanagerpro.com/developers/rest-api/links
Use when someone asks: link manager pro api create short link; list short links api; update short link rest api; delete short link api
Intent: Reference

Sign-in, the response envelope and paging work as 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`. Examples use `https://example.com` and an Application Password placeholder (`USER:APPLICATION_PASSWORD`). Inside WordPress, send the `X-WP-Nonce` header instead.

Click totals and charts for one link live on the reports page: `GET /links/{id}/report`, `GET /links/{id}/report/recent`, `GET /links/{id}/report/export` and `GET /links/{id}/clicks` are described in [Reports and stats endpoints](https://docs.linkmanagerpro.com/developers/rest-api/reports).

A short link that backs a Simple Redirect is hidden from the list and cannot be changed through the write routes here. Those routes return `404` for it. Manage it with the [Redirects endpoints](https://docs.linkmanagerpro.com/developers/rest-api/redirects).

A missing record returns `404` with the WordPress error code `not_found` and the message "The requested resource was not found." Validation failures return `422` with `message` and an `errors` object keyed by field name.

## The short link object

Three routes return a short link: the list rows, the single-link detail and the create or update response. They share the stored fields but differ in shape.

### List row, create response and update response

| Field | Type | What it holds |
|---|---|---|
| `id` | integer | The link ID. |
| `slug` | string | The last part of the short address. Up to 64 characters. |
| `target_url` | string | The destination URL. |
| `redirect_type` | integer | `301`, `302` or `307`. |
| `source` | string | Where the link came from: `manual`, `scan`, `editor`, `affiliate`, `health`, `ai`, `automatorwp`, `imported`, `prettylinks`, `betterlinks`, `thirstyaff` or `redirect`. |
| `url_hash` | string | The MD5 hash of the destination URL, 32 characters. |
| `keywords` | array of strings or null | Keywords used by auto-linking. |
| `enable_autolink` | boolean | Whether auto-linking uses this link. |
| `autolink_priority` | integer | Auto-link priority, `0` to `100`. |
| `link_attributes` | object or null | The `rel` tokens, each `1` or `0`: `nofollow`, `sponsored`, `ugc`, `noopener`, `noreferrer`. Only tokens that were saved appear. |
| `forward_params` | boolean | Whether the visitor's query string passes on to the destination. |
| `utm_params` | object or null | The saved UTM values, keyed `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`. Empty values are not stored. `null` when none are set. |
| `active_from` | string or null | When the link starts working, `Y-m-d H:i:s` in site time. |
| `expires_at` | string or null | When the link stops working, `Y-m-d H:i:s` in site time. |
| `click_count` | integer | Lifetime clicks. Not in the create response. |
| `created_at` | string | When the row was created, as `Y-m-d\TH:i:s.v` (for example `2026-10-20T08:30:00.000`). |
| `updated_at` | string | When the row last changed, same format. |
| `short_url` | string | The public address the plugin builds for this link: the site URL, a prefix and the slug. The prefix follows the affiliate prefix rule below. |

The password hash is never returned. The list rows add these fields:

| Field | Type | What it holds |
|---|---|---|
| `rules_count` | integer | How many routing rules the link has. |
| `_categories` | array of objects | Each has `id`, `name`, `slug` and `color` (a hex color, `#6B7280` when none is set). `name` is decoded, so `A & B` and not `A &amp; B`. |
| `_tags` | array of objects | Each has `id`, `name` and `slug`. |
| `_autolink_terms` | array of integers | WordPress term IDs that limit where auto-linking applies. Empty means everywhere. |
| `addresses_count` | integer | How many [other addresses](#other-addresses) the link has. |
| `address_conflicts` | integer | How many of those open something else and are not marked "kept". |

### Detail response

`GET /links/{id}` returns a fixed set of fields built for the edit form. Booleans are `"yes"` or `"no"` strings here, and relations come back inline.

| Field | Type | What it holds |
|---|---|---|
| `id` | integer | The link ID. |
| `slug` | string | The slug. |
| `target_url` | string | The destination URL. |
| `redirect_type` | integer | `301`, `302` or `307`. |
| `source` | string | The source, `manual` when none is stored. |
| `keywords` | array of strings | Keywords, an empty array when none. |
| `enable_autolink` | string | `"yes"` or `"no"`. |
| `autolink_priority` | integer | Auto-link priority. |
| `forward_params` | string | `"yes"` or `"no"`. |
| `link_attributes` | object | Always four keys, `nofollow`, `noopener`, `noreferrer` and `sponsored`, each `"yes"` or `"no"`. `ugc` is not returned. |
| `utm_params` | object | Always five keys, each a string, empty when not set. |
| `active_from` | string or null | The stored value, or `null`. |
| `expires_at` | string or null | The stored value, or `null`. |
| `has_password` | string | `"yes"` when the link has a password, else `"no"`. |
| `categories` | array of strings | Category slugs. |
| `tags` | array of strings | Tag slugs. |
| `autolink_terms` | array of integers | WordPress term IDs. |
| `rules` | array of objects | The link's [routing rules](#the-routing-rule-object) in the edit shape, ordered by priority then ID. |
| `imported` | object | `name` and `note`, strings kept from an import. Empty strings when none. |
| `addresses` | array of objects | The link's [other addresses](#the-address-object). |

`click_count`, `short_url`, `url_hash` and the timestamps are not part of the detail response.

## Short links

A short link is one row with a slug, a destination and settings. The public address is the site URL, a prefix and the slug.

The prefix in `short_url` follows one rule. When **Affiliate link prefix** is set and the destination is on one of your affiliates' domains, the address uses it, for example `https://example.com/recommends/amazon-deal`. A destination on your own site uses the internal prefix when **Mask internal links** is on. All other links use the **Short link prefix** (default `link`). A redirect's hidden link never uses the affiliate prefix. Every prefix you own opens every short link. See [General settings](https://docs.linkmanagerpro.com/guides/settings/general#affiliate-link-prefix).

### List short links

`GET /links`

Returns one page of short links, newest first, with each link's categories, tags and other-address counts.

**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 destination URL, the slug or a keyword. It also finds a link by one of its other addresses. |
| `filter` | string | No | none | `internal` or `external` compares the destination host with the site host. `address_conflicts` lists links whose other address opens something else. Any other value is ignored. |
| `category` | string | No | none | A category slug. A slug that does not exist returns an empty list. |
| `tag` | string | No | none | A tag slug. A slug that does not exist returns an empty list. |
| `page` | integer | No | `1` | The page number. |
| `per_page` | integer | No | `15` | Links per page. |

There is no sort parameter. The list is ordered by `id`, highest first. Links that back a redirect are never listed. A bad `page` or `per_page` falls back to its default.

**Response:** `200`. `data` is a page object ([Pagination](https://docs.linkmanagerpro.com/developers/rest-api/#pagination)). Each item in `data.data` is a short link row, fields in [the short link object](#list-row-create-response-and-update-response).

```bash
curl -u USER:APPLICATION_PASSWORD \
  "https://example.com/wp-json/link-manager-pro/v1/links?search=widget&filter=external&per_page=15"
```

```json
{
  "success": true,
  "data": {
    "current_page": 1,
    "data": [
      {
        "id": 42,
        "slug": "blue-widget",
        "target_url": "https://example.com/products/blue-widget",
        "redirect_type": 301,
        "source": "manual",
        "url_hash": "87a4e737bcad6e866e9c1c93a8fa4688",
        "keywords": ["blue widget"],
        "enable_autolink": false,
        "autolink_priority": 10,
        "link_attributes": { "nofollow": 1, "sponsored": 1 },
        "click_count": 128,
        "forward_params": false,
        "utm_params": null,
        "active_from": null,
        "expires_at": null,
        "created_at": "2026-10-20T08:30:00.000",
        "updated_at": "2026-10-21T10:05:00.000",
        "short_url": "https://example.com/link/blue-widget",
        "rules_count": 0,
        "_categories": [{ "id": 7, "name": "Widgets", "slug": "widgets", "color": "#6B7280" }],
        "_tags": [{ "id": 9, "name": "Spring sale", "slug": "spring-sale" }],
        "_autolink_terms": [],
        "addresses_count": 0,
        "address_conflicts": 0
      }
    ],
    "first_page_url": "/wp-json/link-manager-pro/v1/links?page=1",
    "from": 1,
    "last_page": 3,
    "last_page_url": "/wp-json/link-manager-pro/v1/links?page=3",
    "links": [],
    "next_page_url": "/wp-json/link-manager-pro/v1/links?page=2",
    "path": "/wp-json/link-manager-pro/v1/links",
    "per_page": 15,
    "prev_page_url": null,
    "to": 15,
    "total": 42
  }
}
```

The `path` and `*_url` fields come from the request path, so they carry no host. The `links` array is shortened here.

**Errors:** None beyond sign-in and permission errors. Bad parameter values fall back to their defaults.

### Get a short link

`GET /links/{id}`

Returns one link in the shape its edit form uses, with categories, tags, auto-link terms, rules and other addresses.

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

**Path parameters:**

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

**Response:** `200`. `data` is the link in the [detail response](#detail-response) shape.

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

```json
{
  "success": true,
  "data": {
    "id": 42,
    "slug": "blue-widget",
    "target_url": "https://example.com/products/blue-widget",
    "redirect_type": 301,
    "source": "manual",
    "keywords": ["blue widget"],
    "enable_autolink": "no",
    "autolink_priority": 10,
    "forward_params": "no",
    "link_attributes": {
      "nofollow": "yes",
      "noopener": "no",
      "noreferrer": "no",
      "sponsored": "yes"
    },
    "utm_params": {
      "utm_source": "",
      "utm_medium": "",
      "utm_campaign": "",
      "utm_term": "",
      "utm_content": ""
    },
    "active_from": null,
    "expires_at": null,
    "has_password": "no",
    "categories": ["widgets"],
    "tags": ["spring-sale"],
    "autolink_terms": [],
    "rules": [],
    "imported": { "name": "", "note": "" },
    "addresses": []
  }
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | No link has that ID. |

### Create a short link

`POST /links`

Creates a short link and returns it. Without a slug, the plugin generates a unique one.

**Permission:** `edit_posts` (or the capability the `link_manager_pro_picker_capability` filter returns) or `link_manager_pro_access` ([Permissions](https://docs.linkmanagerpro.com/developers/rest-api/#permissions)). This is the only link route that accepts editors without full plugin access. 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. Without it 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."

What the caller can set depends on the permission. A user with `link_manager_pro_access` can send every field below. A user with only post-editing rights can send `target_url` and `slug`. The plugin ignores every other field for them and sets `source` to `editor`.

**Body parameters:** (JSON)

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `target_url` | string | Yes | none | The destination. A full `http` or `https` URL with a host (international characters are allowed), 2048 characters or fewer. |
| `slug` | string | No | generated | The last part of the short address. 64 characters or fewer, no `/`, not `.` or `..`, no control characters. Must be unused. |
| `redirect_type` | integer | No | the affiliate's type, else the Redirection setting (default `301`) | `301`, `302` or `307`. |
| `enable_autolink` | string | Yes | none | `yes` or `no`. Turns keyword auto-linking on for this link. |
| `forward_params` | string | Yes | none | `yes` or `no`. Passes the visitor's query string on to the destination. |
| `link_attributes` | object | Yes | none | An object with at least one key. Each value is `yes` or `no`. Only `nofollow`, `sponsored`, `ugc`, `noopener` and `noreferrer` are kept. |
| `source` | string | No | `manual` | One of `manual`, `scan`, `editor`, `affiliate`, `health`, `ai`, `automatorwp`, `imported`, `prettylinks`, `betterlinks` or `thirstyaff`. |
| `keywords` | array of strings | No | `[]` | Up to 50 keywords, each 120 characters or fewer. |
| `autolink_priority` | integer | No | `10` | `0` to `100`. A number outside the range returns a `422` on this field. |
| `link_password` | string | No | none | 128 characters or fewer. Stored as a hash. |
| `active_from` | string | No | none | When the link starts working, in site time. |
| `expires_at` | string | No | none | When the link stops working, in site time. |
| `utm_params` | object | No | none | Any of `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`. Each 255 characters or fewer. Empty values are dropped. |
| `categories` | array of strings | No | none | Category slugs to attach. |
| `tags` | array of strings | No | none | Tag slugs to attach. |
| `rules` | array of objects | No | none | Routing rules to attach, each with the fields in [Rule body fields](#rule-body-fields). A rule error is keyed like `rules.0.target_url`. A value that is not an array returns `422` and nothing is created. |
| `autolink_terms` | array of integers | No | none | WordPress term IDs that limit where auto-linking applies. |

Date fields accept `Y-m-d H:i:s`, `Y-m-d` (midnight) or any other date string PHP can read, in the site time zone. A value that cannot be read is stored as empty, without an error.

If the destination host matches an affiliate, the plugin adds the affiliate's missing tag to `target_url`. A tag that belongs in the path goes into the path part, before any `?` or `#`, and only when its segments are not already there. It also takes the affiliate's redirect type when you send none.

**Response:** `200`. For a caller with `link_manager_pro_access`, `data` holds:

| Field | Type | What it holds |
|---|---|---|
| `link` | object | The saved link, fields in [the short link object](#list-row-create-response-and-update-response) without `click_count`. |
| `address_takeover` | array of objects | Other addresses of other links that the new short address now answers. Each item has `path` (string), `link_id` (integer), `slug` (string), `from` (string, the source plugin name or empty) and `live` (boolean). `live` is `false` when the saved link is scheduled or expired: it takes the address but opens nothing until it is live. Empty when none. |

A caller with only post-editing rights gets a smaller object:

| Field | Type | What it holds |
|---|---|---|
| `link.id` | integer | The new link ID. |
| `link.slug` | string | The slug. |
| `link.short_url` | string | The public address. |

```bash
curl -X POST -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{
    "target_url": "https://example.com/products/blue-widget",
    "slug": "blue-widget",
    "redirect_type": 301,
    "enable_autolink": "no",
    "forward_params": "no",
    "link_attributes": { "nofollow": "yes", "sponsored": "yes" },
    "keywords": ["blue widget"],
    "categories": ["widgets"],
    "tags": ["spring-sale"],
    "expires_at": "2026-12-31"
  }' \
  https://example.com/wp-json/link-manager-pro/v1/links
```

```json
{
  "success": true,
  "data": {
    "link": {
      "slug": "blue-widget",
      "target_url": "https://example.com/products/blue-widget",
      "url_hash": "87a4e737bcad6e866e9c1c93a8fa4688",
      "keywords": ["blue widget"],
      "enable_autolink": false,
      "autolink_priority": 10,
      "forward_params": false,
      "link_attributes": { "nofollow": 1, "sponsored": 1 },
      "redirect_type": 301,
      "source": "manual",
      "utm_params": null,
      "active_from": null,
      "expires_at": "2026-12-31 00:00:00",
      "updated_at": "2026-10-23T09:15:00.000",
      "created_at": "2026-10-23T09:15:00.000",
      "id": 43,
      "short_url": "https://example.com/link/blue-widget"
    },
    "address_takeover": []
  },
  "message": "Short link created successfully."
}
```

The editor-level response looks like this:

```json
{
  "success": true,
  "data": {
    "link": {
      "id": 43,
      "slug": "blue-widget",
      "short_url": "https://example.com/link/blue-widget"
    }
  },
  "message": "Short link created successfully."
}
```

**Errors:**

| Status | When |
|---|---|
| `403` | The caller has no full access and no create right. The code is `lmp_cannot_create_links`. |
| `422` | A required field is missing or has the wrong type. `errors` is keyed by `target_url`, `enable_autolink`, `forward_params` or `link_attributes`. |
| `422` | `target_url` is not a full `http` or `https` URL with a host. `errors.target_url` is "Enter a full URL, starting with https://". |
| `422` | `target_url` is over 2048 characters ("Use 2048 characters or fewer.") or `keywords` holds more than 50 items (`errors.keywords` is "Use 50 keywords or fewer."). |
| `422` | `slug` has more than 64 characters, contains `/`, equals `.` or `..`, holds control characters, or is not valid UTF-8. `errors.slug` explains it. |
| `422` | `slug` is already used by another link. `errors.slug` is "This short link slug is already in use." |
| `422` | `source` is not one of the allowed values. `errors.source` is "Choose a valid link source." |
| `422` | A keyword, the password or a UTM value is over its limit. `errors` is keyed by `keywords`, `link_password` or `utm_params.utm_source` and the other UTM fields. |
| `422` | `rules` is sent but is not a list. `errors.rules` is "Send rules as a list. Send an empty list to remove all rules." |
| `422` | A routing rule in `rules` is invalid. `errors` is keyed like `rules.0.priority`. On a PUT, the link is left unchanged. |

### Update a short link

`PUT /links/{id}`

Saves a link and returns it. This route replaces the link's fields. It is not a partial update.

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

**Path parameters:**

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

**Body parameters:** (JSON) The same fields and rules as [Create a short link](#create-a-short-link). The four required fields must be sent every time. The table shows what an omitted field does.

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `target_url` | string | Yes | none | The destination. |
| `enable_autolink` | string | Yes | none | `yes` or `no`. |
| `forward_params` | string | Yes | none | `yes` or `no`. |
| `link_attributes` | object | Yes | none | At least one key, each value `yes` or `no`. |
| `slug` | string | No | the current slug | A new slug. A value equal to the current slug is not checked again. |
| `redirect_type` | integer | No | the current type | `301`, `302` or `307`. |
| `source` | string | No | the current source | An allowed source, or the link's own stored value sent back unchanged. |
| `keywords` | array of strings | No | `[]` | Omitted means the keyword list is emptied. |
| `autolink_priority` | integer | No | `10` | `0` to `100`. |
| `utm_params` | object | No | none | Omitted means the UTM values are cleared. |
| `active_from` | string | No | none | Omitted means the start date is cleared. |
| `expires_at` | string | No | none | Omitted means the end date is cleared. |
| `link_password` | string | No | unchanged | A new password, 128 characters or fewer. It wins over `remove_password`. |
| `remove_password` | string | No | `no` | `yes` clears the password. |
| `categories` | array of strings | No | unchanged | When sent, replaces the link's categories. An empty array removes them all. |
| `tags` | array of strings | No | unchanged | When sent, replaces the link's tags. |
| `rules` | array of objects | No | unchanged | When sent, replaces all routing rules. An empty array removes them all. A value that is not an array returns `422` and nothing is saved. |
| `autolink_terms` | array of integers | No | unchanged | When sent, replaces the term list. An empty array clears it. |

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

| Field | Type | What it holds |
|---|---|---|
| `link` | object | The saved link with every column, including `click_count`. Fields in [the short link object](#list-row-create-response-and-update-response). |
| `address_takeover` | array of objects | Same as in [Create a short link](#create-a-short-link). |

```bash
curl -X PUT -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{
    "target_url": "https://example.com/products/blue-widget-2",
    "enable_autolink": "no",
    "forward_params": "yes",
    "link_attributes": { "nofollow": "yes" },
    "keywords": ["blue widget"]
  }' \
  https://example.com/wp-json/link-manager-pro/v1/links/43
```

```json
{
  "success": true,
  "data": {
    "link": {
      "id": 43,
      "slug": "blue-widget",
      "target_url": "https://example.com/products/blue-widget-2",
      "redirect_type": 301,
      "source": "manual",
      "url_hash": "5d1c7a0c1d6b0e4c2a8f3b9e7a4d6c21",
      "keywords": ["blue widget"],
      "enable_autolink": false,
      "autolink_priority": 10,
      "link_attributes": { "nofollow": 1 },
      "click_count": 0,
      "forward_params": true,
      "utm_params": null,
      "active_from": null,
      "expires_at": null,
      "created_at": "2026-10-23T09:15:00.000",
      "updated_at": "2026-10-23T09:30:00.000",
      "short_url": "https://example.com/link/blue-widget"
    },
    "address_takeover": []
  },
  "message": "Short link updated successfully."
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | The ID does not exist, or the link backs a Simple Redirect. |
| `422` | The same field errors as [Create a short link](#create-a-short-link). |
| `422` | `rules` is sent but is not a list. `errors.rules` is "Send rules as a list. Send an empty list to remove all rules." |
| `422` | A routing rule is invalid (`errors.rules.N.field`). Rules are checked first, so nothing is saved, the link's own fields included. |

### Delete a short link

`DELETE /links/{id}`

Deletes the link together with its click rows, routing rules, other addresses, category and tag assignments, and any redirect that points to it.

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

**Path parameters:**

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

**Query parameters:** None.

**Response:** `200`. `data` is `null`.

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

```json
{
  "success": true,
  "data": null,
  "message": "Short link deleted successfully."
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | The ID does not exist, or the link backs a Simple Redirect. |

## Routing rules

A routing rule sends some visitors of a short link to a different destination. A link can have many rules. The plugin tests the active ones in order of `priority` (lowest number first, then lowest rule ID) and uses the first rule that matches. When none matches, the visitor goes to the link's own `target_url`.

A rule matches when the current time is inside its window and its conditions pass. A rule with no conditions matches everyone in the window. With `match_mode` set to `all`, every condition must pass. With `any`, one passing condition is enough.

### The routing rule object

Two shapes exist. [Get a short link](#get-a-short-link) returns rules in the edit shape. The other rule routes return the stored row.

| Field | Type | What it holds |
|---|---|---|
| `id` | integer | The rule ID. |
| `link_id` | integer | The link the rule belongs to. Only in the stored row. |
| `name` | string or null | A label, up to 120 characters. |
| `is_active` | boolean or string | `true` or `false` in the stored row, `"yes"` or `"no"` in the edit shape. |
| `priority` | integer | `0` to `100`. Lower runs first. Default `100`. |
| `match_mode` | string | `all` or `any`. |
| `target_url` | string | Where a matching visitor goes. |
| `redirect_type` | integer | `301`, `302` or `307`. |
| `start_at` | string or null | Start of the window. The stored row gives `Y-m-d\TH:i:s.v`. The edit shape gives `Y-m-d` when the bound is a whole-day start (`00:00:00`), else the same date-time string. |
| `end_at` | string or null | End of the window. The edit shape gives `Y-m-d` for a whole-day end (`23:59:59`). |
| `conditions` | array of objects | The conditions, each `{ "type", "operator", "value" }`. Empty array when none. |
| `created_at`, `updated_at` | string | Timestamps, only in the stored row. |

A date without a time covers the whole day in site time. A start becomes `00:00:00` and an end becomes `23:59:59`, and the end day counts until the next day starts. Any other string must be a date PHP can read, and `start_at` must not be later than `end_at`.

### Condition types

Each condition has a `type`, an `operator` and a `value`. The allowed operators are `gte`, `gt`, `eq`, `lt`, `lte` and `in`, and each type accepts only some of them.

| `type` | Operators | `value` | What it tests |
|---|---|---|---|
| `total_clicks` | `gt`, `gte`, `eq`, `lt`, `lte` | A whole number, `0` or more. | The link's click count, counting the visit being handled. |
| `country_in` | `in` | A non-empty array of two-letter country codes such as `["US","CA"]`. Saved in upper case. | The visitor's country. |
| `date_after` | `gt`, `gte` | A date or date-time string. | The current time is after (`gt`) or at or after (`gte`) the value. |
| `date_before` | `lt`, `lte` | A date or date-time string. | The current time is before (`lt`) or at or before (`lte`) the value. |
| `device_in` | `in` | A non-empty array from `Desktop`, `Mobile`, `Tablet`, `Other`. | The visitor's device type. |
| `os_in` | `in` | A non-empty array from `Windows`, `macOS`, `iOS`, `Android`, `Linux`, `Other`. | The visitor's operating system. Crawlers resolve to `Other`. |
| `browser_in` | `in` | A non-empty array from `Chrome`, `Edge`, `Firefox`, `Safari`, `Opera`, `Samsung Internet`, `Other`. | The visitor's browser. |

For `device_in`, `os_in` and `browser_in`, the plugin fixes the letter case (`mobile` becomes `Mobile`) and rejects any value outside the list. For `date_after` and `date_before`, a date without a time means the whole day: `date_after` with `gt` starts the next day, and `date_before` with `lte` runs to the end of that day. A condition whose visitor data is missing, such as an unknown country, does not match.

```json
{
  "type": "country_in",
  "operator": "in",
  "value": ["US", "CA"]
}
```

### Rule body fields

The three write routes (create, update and save-all) take the same rule fields.

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `name` | string | No | none | A label, 120 characters or fewer. |
| `is_active` | string | Yes | none | `yes` or `no`. |
| `priority` | integer | Yes | none | A whole number from `0` to `100`. |
| `match_mode` | string | Yes | none | `all` or `any`. |
| `target_url` | string | Yes | none | A full `http` or `https` URL with a host, 2048 characters or fewer. A target the link's rules already have, such as an old `mailto:` rule, is accepted as it is. A new or changed non-http target is still refused. |
| `redirect_type` | integer | Yes | none | `301`, `302` or `307`. |
| `start_at` | string | No | none | Start of the window, site time. |
| `end_at` | string | No | none | End of the window, site time. |
| `conditions` | array of objects | No | none | The conditions. Each needs `type`, `operator` and a `value` key. |

### List the routing rules of a link

`GET /links/{id}/rules`

Returns every rule of one link, ordered by priority and then ID, active or not.

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

**Path parameters:**

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

**Query parameters:** None.

**Response:** `200`. `data` is an array of stored rule rows, fields in [the routing rule object](#the-routing-rule-object). It is empty when the link has no rules.

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

```json
{
  "success": true,
  "data": [
    {
      "id": 5,
      "link_id": 42,
      "name": "US visitors",
      "is_active": true,
      "priority": 10,
      "match_mode": "all",
      "target_url": "https://example.com/us/blue-widget",
      "redirect_type": 302,
      "start_at": null,
      "end_at": null,
      "conditions": [{ "type": "country_in", "operator": "in", "value": ["US"] }],
      "created_at": "2026-10-20T08:30:00.000",
      "updated_at": "2026-10-20T08:30:00.000"
    }
  ]
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | No link has that ID. |

### Create a routing rule

`POST /links/{id}/rules`

Adds one rule to a link and returns it.

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

**Path parameters:**

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

**Body parameters:** (JSON) The fields in [Rule body fields](#rule-body-fields).

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

| Field | Type | What it holds |
|---|---|---|
| `rule` | object | The saved rule, fields in [the routing rule object](#the-routing-rule-object). |

```bash
curl -X POST -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{
    "name": "US visitors",
    "is_active": "yes",
    "priority": 10,
    "match_mode": "all",
    "target_url": "https://example.com/us/blue-widget",
    "redirect_type": 302,
    "conditions": [{ "type": "country_in", "operator": "in", "value": ["US"] }]
  }' \
  https://example.com/wp-json/link-manager-pro/v1/links/42/rules
```

```json
{
  "success": true,
  "data": {
    "rule": {
      "id": 5,
      "link_id": 42,
      "name": "US visitors",
      "is_active": true,
      "priority": 10,
      "match_mode": "all",
      "target_url": "https://example.com/us/blue-widget",
      "redirect_type": 302,
      "start_at": null,
      "end_at": null,
      "conditions": [{ "type": "country_in", "operator": "in", "value": ["US"] }],
      "updated_at": "2026-10-20T08:30:00.000",
      "created_at": "2026-10-20T08:30:00.000"
    }
  },
  "message": "Rule created successfully."
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | The link does not exist or backs a Simple Redirect. |
| `422` | A required field is missing or invalid, `name` is over 120 characters, `type` or `operator` is not in the lists above, or a `value` does not fit its type. Condition errors are keyed `conditions.0.operator` or `conditions.0.value`, and their messages use the editor's labels. |
| `422` | `target_url` is not a full `http` or `https` URL with a host ("Enter a full URL, starting with https://") or is over 2048 characters ("Use 2048 characters or fewer."). An update accepts the rule's own current target unchanged. |
| `422` | The window is unreadable or `start_at` is later than `end_at`. `errors.start_at` explains it. |

### Update a routing rule

`PUT /links/{id}/rules/{ruleId}`

Replaces one rule's fields and returns it. Send every required field again.

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

**Path parameters:**

| Name | Type | What it is |
|---|---|---|
| `id` | integer | The short link ID. |
| `ruleId` | integer | The rule ID. It must belong to that link. |

**Body parameters:** (JSON) The fields in [Rule body fields](#rule-body-fields).

**Response:** `200`. `data.rule` is the saved rule, in the same shape as [Create a routing rule](#create-a-routing-rule), with `message` set to "Rule updated successfully."

```bash
curl -X PUT -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{
    "name": "US and Canada",
    "is_active": "yes",
    "priority": 10,
    "match_mode": "all",
    "target_url": "https://example.com/na/blue-widget",
    "redirect_type": 302,
    "conditions": [{ "type": "country_in", "operator": "in", "value": ["US", "CA"] }]
  }' \
  https://example.com/wp-json/link-manager-pro/v1/links/42/rules/5
```

**Errors:**

| Status | When |
|---|---|
| `404` | The link does not exist or backs a Simple Redirect, or the rule does not belong to the link. |
| `422` | The same field, condition and window errors as [Create a routing rule](#create-a-routing-rule). |

### Save all routing rules of a link

`PUT /links/{id}/rules/sync`

Replaces all of a link's rules with the list you send, in one step. Nothing changes if any rule is invalid.

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

**Path parameters:**

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

**Body parameters:** (JSON)

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `rules` | array of objects | Yes | none | The new rule set, each item with the fields in [Rule body fields](#rule-body-fields). Send `[]` to remove every rule. A missing or non-array value returns `422`. |

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

| Field | Type | What it holds |
|---|---|---|
| `rules` | array of objects | The saved rules, in the order sent. Fields in [the routing rule object](#the-routing-rule-object). |

```bash
curl -X PUT -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{
    "rules": [
      {
        "name": "Mobile",
        "is_active": "yes",
        "priority": 20,
        "match_mode": "any",
        "target_url": "https://example.com/m/blue-widget",
        "redirect_type": 302,
        "conditions": [{ "type": "device_in", "operator": "in", "value": ["Mobile", "Tablet"] }]
      }
    ]
  }' \
  https://example.com/wp-json/link-manager-pro/v1/links/42/rules/sync
```

```json
{
  "success": true,
  "data": {
    "rules": [
      {
        "id": 6,
        "link_id": 42,
        "name": "Mobile",
        "is_active": true,
        "priority": 20,
        "match_mode": "any",
        "target_url": "https://example.com/m/blue-widget",
        "redirect_type": 302,
        "start_at": null,
        "end_at": null,
        "conditions": [{ "type": "device_in", "operator": "in", "value": ["Mobile", "Tablet"] }],
        "updated_at": "2026-10-20T09:00:00.000",
        "created_at": "2026-10-20T09:00:00.000"
      }
    ]
  },
  "message": "Rules synced successfully."
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | The link does not exist or backs a Simple Redirect. |
| `422` | `rules` is missing or is not a list. `errors.rules` is "Send rules as a list. Send an empty list to remove all rules." |
| `422` | A rule is invalid. `errors` is keyed by the rule's position and field, such as `rules.0.target_url` or `rules.1.conditions.0.value`. A target the link's rules already have is accepted unchanged. |

### Delete a routing rule

`DELETE /links/{id}/rules/{ruleId}`

Deletes one rule of a link.

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

**Path parameters:**

| Name | Type | What it is |
|---|---|---|
| `id` | integer | The short link ID. |
| `ruleId` | integer | The rule ID. It must belong to that link. |

**Query parameters:** None.

**Response:** `200`. `data` is `null`.

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

```json
{
  "success": true,
  "data": null,
  "message": "Rule deleted successfully."
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | The link does not exist or backs a Simple Redirect, or the rule does not belong to the link. |

## Other addresses

An other address is an extra public path that also opens a short link, besides its own `/link/slug` address. An import keeps the old plugin's paths this way, such as `/recommends/deal`, and you can add more by hand. A link can have up to 25.

Each address is identified by its hash: the MD5 of the path in its comparable form (a leading `/`, each segment decoded once and lowercased). `/Recommends/Deal` and `/recommends/deal` share the hash `f28faceddcd1047b8c2057b3ccf62f4f`. The path is kept as typed for display.

An address opens the link only when nothing else answers it. The plugin checks these in order, and the first match wins:

1. The source plugin, while its code still runs.
2. Another short link's own address.
3. A Simple Redirect.
4. A published page or post that WordPress resolves at that path.
5. Another link that owns the same address, the one that has held it longest.

When something else answers, the address stays saved but dormant, and it is a conflict. It starts working when the other owner goes away. "Keep the other" marks a conflict as accepted, so it stops counting. The path is rejected if it is empty, longer than 2000 bytes, holds `.` or `..` or control characters, starts with a WordPress path (`wp-admin`, `wp-content`, `wp-includes`, `wp-json`, `wp-login.php`, `robots.txt` and similar), or is only a short-link prefix (link, internal or affiliate).

### The address object

| Field | Type | What it holds |
|---|---|---|
| `hash` | string | The 32-character address hash used in the route. |
| `path` | string | The path as typed, such as `/Recommends/Deal`. |
| `url` | string | The full address on this site, with the path encoded. |
| `from` | string | The source plugin that supplied it: `prettylinks`, `betterlinks`, `thirstyaff`, or an empty string when added by hand. |
| `from_name` | string | The source plugin's display name, empty when added by hand. |
| `added` | string | When it was added, `Y-m-d H:i:s` in UTC. |
| `status` | object | What a visit gets now, fields below. |
| `conflict` | boolean | `true` when something else answers it and the conflict is not marked kept. |
| `kept_other` | boolean | `true` when "Keep the other" was chosen for the current answer. |

The `status` object always has `code` (string) and `conflict` (boolean). It adds fields for some codes.

| `code` | Meaning | Extra fields |
|---|---|---|
| `opens` | The address opens this link. | None. |
| `same` | It is the link's own short address. | None. |
| `paused` | It would open the link, but the link is scheduled or expired. | None. |
| `source_active` | The source plugin still runs and answers it. | None. |
| `short_link` | Another short link's own address. A conflict. | `id`, `slug` |
| `redirect` | A Simple Redirect answers it. A conflict. | `id`, `target` |
| `page` | A published page or post answers it. A conflict. | `id`, `title`, `edit_url` |
| `other_link` | Another link's older address. A conflict. | `id`, `slug` |
| `invalid` | The stored path can no longer be used. | None. |

```json
{
  "hash": "f28faceddcd1047b8c2057b3ccf62f4f",
  "path": "/recommends/deal",
  "url": "https://example.com/recommends/deal",
  "from": "thirstyaff",
  "from_name": "ThirstyAffiliates",
  "added": "2026-10-20 08:30:00",
  "status": { "code": "redirect", "conflict": true, "id": 12, "target": "https://example.com/deals" },
  "conflict": true,
  "kept_other": false
}
```

### Count address conflicts

`GET /links/addresses/conflicts`

Returns how many other addresses open something else, and on how many links. The Dashboard's "Needs attention" row uses it.

**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 |
|---|---|---|
| `count` | integer | The number of conflicting addresses across all links. |
| `links` | integer | The number of links that have at least one. |

The numbers come from a stored check that the plugin refreshes when links, redirects and pages change, and once a day.

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

```json
{
  "success": true,
  "data": { "count": 3, "links": 2 }
}
```

**Errors:** None beyond sign-in and permission errors.

### Add an address to a link

`POST /links/{id}/addresses`

Adds an other address to a link. The address is saved even when something else answers it today.

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

**Path parameters:**

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

**Body parameters:** (JSON)

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `path` | string | Yes | none | A path on this site, such as `/deal` or `/recommends/deal`, or a full URL on this site's host. Control characters are removed and the ends trimmed. |

**Response:** `200`. All four address routes return the link's addresses after the change. `data` holds:

| Field | Type | What it holds |
|---|---|---|
| `addresses` | array of objects | Every address of the link, oldest first, fields in [the address object](#the-address-object). |

```bash
curl -X POST -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{ "path": "/recommends/deal" }' \
  https://example.com/wp-json/link-manager-pro/v1/links/42/addresses
```

```json
{
  "success": true,
  "data": {
    "addresses": [
      {
        "hash": "f28faceddcd1047b8c2057b3ccf62f4f",
        "path": "/recommends/deal",
        "url": "https://example.com/recommends/deal",
        "from": "",
        "from_name": "",
        "added": "2026-10-23 09:40:00",
        "status": { "code": "opens", "conflict": false },
        "conflict": false,
        "kept_other": false
      }
    ]
  },
  "message": "Address added."
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | The link does not exist or backs a Simple Redirect. |
| `422` | The path cannot be used (code `address_invalid`): empty, not on this site, a WordPress path, or a bare short-link prefix. |
| `422` | The path is the link's own short address (code `address_same`). |
| `422` | The link already has 25 other addresses (code `address_full`). |
| `409` | The link already has that address (code `address_exists`). |

### Remove an address from a link

`POST /links/{id}/addresses/{hash}/remove`

Removes one address. It stops opening the link.

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

**Path parameters:**

| Name | Type | What it is |
|---|---|---|
| `id` | integer | The short link ID. |
| `hash` | string | The address hash, 32 lowercase hex characters, from [the address object](#the-address-object). |

**Body parameters:** None.

**Response:** `200`. `data.addresses` is the link's remaining addresses, as in [Add an address to a link](#add-an-address-to-a-link), with `message` set to "Address removed."

```bash
curl -X POST -u USER:APPLICATION_PASSWORD \
  https://example.com/wp-json/link-manager-pro/v1/links/42/addresses/f28faceddcd1047b8c2057b3ccf62f4f/remove
```

**Errors:**

| Status | When |
|---|---|
| `404` | The link does not exist or backs a Simple Redirect, or the hash is not one of the link's addresses (code `address_unknown`). |

### Take an address for this link

`POST /links/{id}/addresses/{hash}/claim`

Makes a conflicting address open this link by removing whatever holds it. This is the "Send it here" action.

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

**Path parameters:**

| Name | Type | What it is |
|---|---|---|
| `id` | integer | The short link ID. |
| `hash` | string | The address hash. |

**Body parameters:** None.

What it does depends on the address `status.code`. For `redirect`, the Simple Redirect is deleted, with its backing link. For `other_link`, the address is removed from the other link. For other codes there is nothing to take and the call still succeeds. A short link's own address and a page are never taken.

**Response:** `200`. `data.addresses` is the link's addresses after the change, as in [Add an address to a link](#add-an-address-to-a-link), with `message` set to "The address now opens this link."

```bash
curl -X POST -u USER:APPLICATION_PASSWORD \
  https://example.com/wp-json/link-manager-pro/v1/links/42/addresses/f28faceddcd1047b8c2057b3ccf62f4f/claim
```

**Errors:**

| Status | When |
|---|---|
| `404` | The link does not exist or backs a Simple Redirect, or the hash is not one of the link's addresses (code `address_unknown`). |
| `409` | Another short link answers this address itself (code `address_short_link`). Change that link's slug first. |
| `409` | A page on the site uses this address (code `address_page`). Change the page's address first. |

### Keep the other owner of an address

`POST /links/{id}/addresses/{hash}/keep`

Marks a conflict as accepted. The address stays dormant and stops counting as a conflict until its answer changes. This is the "Keep the other" action.

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

**Path parameters:**

| Name | Type | What it is |
|---|---|---|
| `id` | integer | The short link ID. |
| `hash` | string | The address hash. |

**Body parameters:** None.

**Response:** `200`. `data.addresses` is the link's addresses after the change, as in [Add an address to a link](#add-an-address-to-a-link), with `message` set to "Kept as it is." The address then has `kept_other` set to `true` and `conflict` set to `false`.

```bash
curl -X POST -u USER:APPLICATION_PASSWORD \
  https://example.com/wp-json/link-manager-pro/v1/links/42/addresses/f28faceddcd1047b8c2057b3ccf62f4f/keep
```

**Errors:**

| Status | When |
|---|---|
| `404` | The link does not exist or backs a Simple Redirect, or the hash is not one of the link's addresses (code `address_unknown`). |
| `409` | The address is not a conflict, so there is nothing to keep (code `address_no_conflict`). |

## Categories and tags of a link

These routes read and replace the categories or tags of one short link. Both use term slugs. The list routes work for any link. The replace routes return `404` for a link that backs a Simple Redirect. Both replace routes hand the slugs to WordPress's `wp_set_object_terms`, which creates a term that does not exist yet.

### List categories of a link

`GET /links/{id}/categories`

Returns the categories attached to a link.

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

**Path parameters:**

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

**Query parameters:** None.

**Response:** `200`. `data` is an array of category objects, empty when none. Fields in [the category object](#the-category-object).

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

```json
{
  "success": true,
  "data": [
    { "id": 7, "name": "Widgets", "slug": "widgets", "color": "#6B7280", "count": 0 }
  ]
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | No link has that ID. |

### Set the categories of a link

`PUT /links/{id}/categories`

Replaces the link's categories with the slugs you send. An empty array removes them all.

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

**Path parameters:**

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

**Body parameters:** (JSON)

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `categories` | array of strings | No | `[]` | Category slugs. Each is cleaned like a WordPress slug and empty ones are dropped. A value that is not an array counts as empty. |

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

| Field | Type | What it holds |
|---|---|---|
| `categories` | array of objects | The link's categories after the change. Fields in [the category object](#the-category-object). |

```bash
curl -X PUT -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{ "categories": ["widgets"] }' \
  https://example.com/wp-json/link-manager-pro/v1/links/42/categories
```

```json
{
  "success": true,
  "data": {
    "categories": [
      { "id": 7, "name": "Widgets", "slug": "widgets", "color": "#6B7280", "count": 0 }
    ]
  },
  "message": "Categories updated successfully."
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | The link does not exist or backs a Simple Redirect. |

### List tags of a link

`GET /links/{id}/tags`

Returns the tags attached to a link.

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

**Path parameters:**

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

**Query parameters:** None.

**Response:** `200`. `data` is an array of tag objects, empty when none. Fields in [the tag object](#the-tag-object).

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

```json
{
  "success": true,
  "data": [
    { "id": 9, "name": "Spring sale", "slug": "spring-sale", "count": 0 }
  ]
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | No link has that ID. |

### Set the tags of a link

`PUT /links/{id}/tags`

Replaces the link's tags with the slugs you send. An empty array removes them all.

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

**Path parameters:**

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

**Body parameters:** (JSON)

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `tags` | array of strings | No | `[]` | Tag slugs. Each is cleaned like a WordPress slug and empty ones are dropped. A value that is not an array counts as empty. |

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

| Field | Type | What it holds |
|---|---|---|
| `tags` | array of objects | The link's tags after the change. Fields in [the tag object](#the-tag-object). |

```bash
curl -X PUT -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{ "tags": ["spring-sale"] }' \
  https://example.com/wp-json/link-manager-pro/v1/links/42/tags
```

```json
{
  "success": true,
  "data": {
    "tags": [
      { "id": 9, "name": "Spring sale", "slug": "spring-sale", "count": 0 }
    ]
  },
  "message": "Tags updated successfully."
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | The link does not exist or backs a Simple Redirect. |

## Categories

Categories group short links. Each has a name, a slug and a color. They are terms of the `link_manager_pro_category` taxonomy.

### The category object

| Field | Type | What it holds |
|---|---|---|
| `id` | integer | The term ID. |
| `name` | string | The category name. |
| `slug` | string | The slug, set when the category is created. |
| `color` | string | A hex color. `#6B7280` when none is set. |
| `count` | integer | The number of links, the same as `link_count` where that exists. |

### List categories

`GET /categories`

Returns categories alphabetically, as a page or as one full list.

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

**Query parameters:**

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `all` | string | No | none | Any value that is not empty or `0`, such as `1`, returns every category as a plain array with no paging. |
| `search` | string | No | none | Matches part of the name. Ignored with `all`. |
| `page` | integer | No | `1` | The page number. Ignored with `all`. |
| `per_page` | integer | No | `15` | Categories per page. Ignored with `all`. |

**Response:** `200`. With `all`, `data` is an array of [category objects](#the-category-object) with one more field, `link_count`.

| Field | Type | What it holds |
|---|---|---|
| `link_count` | integer | How many short links carry the category. |

Without `all`, `data` is a page object ([Pagination](https://docs.linkmanagerpro.com/developers/rest-api/#pagination)). Each row has `id`, `name`, `slug`, `color` and `link_count`, with no `count`.

```bash
curl -u USER:APPLICATION_PASSWORD \
  "https://example.com/wp-json/link-manager-pro/v1/categories?all=1"
```

```json
{
  "success": true,
  "data": [
    { "id": 7, "name": "Widgets", "slug": "widgets", "color": "#6B7280", "count": 0, "link_count": 12 }
  ]
}
```

**Errors:** None beyond sign-in and permission errors.

### Create a category

`POST /categories`

Creates a category and returns 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 |
|---|---|---|---|---|
| `name` | string | Yes | none | The category name. Cleaned as text and trimmed. |
| `color` | string | No | `#6B7280` | A hex color such as `#16A34A`. An invalid value falls back to the default. |

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

| Field | Type | What it holds |
|---|---|---|
| `category` | object | The new category. Fields in [the category object](#the-category-object). |

```bash
curl -X POST -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{ "name": "Widgets", "color": "#16A34A" }' \
  https://example.com/wp-json/link-manager-pro/v1/categories
```

```json
{
  "success": true,
  "data": {
    "category": { "id": 7, "name": "Widgets", "slug": "widgets", "color": "#16A34A", "count": 0 }
  },
  "message": "Category created successfully."
}
```

**Errors:**

| Status | When |
|---|---|
| `422` | `name` is empty. `errors.name` is "Category name is required." |
| `422` | A category with that name already exists. `errors.name` is "A category with this name already exists." |

### Update a category

`PUT /categories/{id}`

Changes the name, the color, or both. A rename changes only the name; the slug stays.

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

**Path parameters:**

| Name | Type | What it is |
|---|---|---|
| `id` | integer | The category (term) ID. |

**Body parameters:** (JSON)

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `name` | string | No | unchanged | The new name. An empty value leaves the name as it is. |
| `color` | string | No | unchanged | A hex color. An empty value removes the stored color, and the category then reads `#6B7280`. An invalid value leaves the color as it is. |

**Response:** `200`. `data.category` is the updated category, as in [Create a category](#create-a-category), with `message` set to "Category updated successfully."

```bash
curl -X PUT -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{ "color": "#2563EB" }' \
  https://example.com/wp-json/link-manager-pro/v1/categories/7
```

**Errors:**

| Status | When |
|---|---|
| `404` | The ID is not a category (code `category_not_found`). |
| `422` | Another category already has that name. `errors.name` explains it. |

### Delete a category

`DELETE /categories/{id}`

Deletes the category. Links that carried it stay and lose the category.

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

**Path parameters:**

| Name | Type | What it is |
|---|---|---|
| `id` | integer | The category (term) ID. |

**Query parameters:** None.

**Response:** `200`. `data` is `null`.

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

```json
{
  "success": true,
  "data": null,
  "message": "Category deleted successfully."
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | The ID is not a category (code `category_not_found`). |

## Tags

Tags are labels for short links. They have a name and a slug, and no color. They are terms of the `link_manager_pro_tag` taxonomy.

### The tag object

| Field | Type | What it holds |
|---|---|---|
| `id` | integer | The term ID. |
| `name` | string | The tag name. |
| `slug` | string | The slug. |
| `count` | integer | The number of links, the same as `link_count` where that exists. |

### List tags

`GET /tags`

Returns every tag alphabetically, with no paging.

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

**Query parameters:** None.

**Response:** `200`. `data` is an array of [tag objects](#the-tag-object) with one more field, `link_count`.

| Field | Type | What it holds |
|---|---|---|
| `link_count` | integer | How many short links carry the tag. |

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

```json
{
  "success": true,
  "data": [
    { "id": 9, "name": "Spring sale", "slug": "spring-sale", "count": 0, "link_count": 5 }
  ]
}
```

**Errors:** None beyond sign-in and permission errors.

### Create a tag

`POST /tags`

Creates a tag and returns it. When a tag with that name exists, it returns the existing tag instead of an error.

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

**Body parameters:** (JSON)

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `name` | string | Yes | none | The tag name. Cleaned as text and trimmed. |

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

| Field | Type | What it holds |
|---|---|---|
| `tag` | object | The new or existing tag. Fields in [the tag object](#the-tag-object). |
| `existing` | boolean | `true` when the tag already existed. Not sent for a new tag. |

For an existing tag, `message` is "Tag already exists."

```bash
curl -X POST -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{ "name": "Spring sale" }' \
  https://example.com/wp-json/link-manager-pro/v1/tags
```

```json
{
  "success": true,
  "data": {
    "tag": { "id": 9, "name": "Spring sale", "slug": "spring-sale", "count": 0 }
  },
  "message": "Tag created successfully."
}
```

**Errors:**

| Status | When |
|---|---|
| `422` | `name` is empty. `errors.name` is "Tag name is required." |

### Update a tag

`PUT /tags/{id}`

Renames a tag. The slug and the links that carry the tag stay the same.

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

**Path parameters:**

| Name | Type | What it is |
|---|---|---|
| `id` | integer | The tag (term) ID. |

**Body parameters:** (JSON)

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `name` | string | Yes | none | The new name. |

**Response:** `200`. `data.tag` is the renamed tag, as in [Create a tag](#create-a-tag), with `message` set to "Tag renamed."

```bash
curl -X PUT -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{ "name": "Autumn sale" }' \
  https://example.com/wp-json/link-manager-pro/v1/tags/9
```

**Errors:**

| Status | When |
|---|---|
| `404` | The ID is not a tag (code `tag_not_found`). |
| `422` | `name` is empty ("Tag name is required.") or another tag has that name ("A tag with this name already exists."). |

### Delete a tag

`DELETE /tags/{id}`

Deletes the tag. Links that carried it stay and lose the tag.

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

**Path parameters:**

| Name | Type | What it is |
|---|---|---|
| `id` | integer | The tag (term) ID. |

**Query parameters:** None.

**Response:** `200`. `data` is `null`.

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

```json
{
  "success": true,
  "data": null,
  "message": "Tag deleted successfully."
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | The ID is not a tag (code `tag_not_found`). |

## Related

- [REST API overview](https://docs.linkmanagerpro.com/developers/rest-api/)
- [Reports endpoints](https://docs.linkmanagerpro.com/developers/rest-api/reports)
- [Affiliates endpoints](https://docs.linkmanagerpro.com/developers/rest-api/affiliates)
- [Redirects endpoints](https://docs.linkmanagerpro.com/developers/rest-api/redirects)
- [All endpoints](https://docs.linkmanagerpro.com/developers/rest-api/endpoints)
