# Webhooks endpoints

These REST routes manage the endpoints that receive Link Manager Pro events. You can create and edit a webhook, send a test, read its recent deliveries and reveal or regenerate its signing secret.
Page: https://docs.linkmanagerpro.com/developers/rest-api/webhooks
Use when someone asks: link manager pro webhooks api; create webhook rest api; webhook deliveries api; regenerate webhook secret
Intent: Reference

A webhook is an HTTPS address that receives an HTTP request when a Link Manager Pro event happens. Sign-in and the response envelope are in the [REST API overview](https://docs.linkmanagerpro.com/developers/rest-api/).

All paths are relative to `/wp-json/link-manager-pro/v1`. Examples use `https://example.com` and `USER:APPLICATION_PASSWORD`. Inside WordPress, send the `X-WP-Nonce` header instead.

Every route here needs an administrator (`manage_options`). The request your receiver gets, its headers and signature are described in [Events, webhooks and AutomatorWP](https://docs.linkmanagerpro.com/developers/hooks/events). The screen is explained in [Webhooks and AutomatorWP](https://docs.linkmanagerpro.com/guides/webhooks-automation).

A validation failure is a `422` with `message` and an `errors` object that lists messages per field. A missing webhook is a `404` with `code`, `message`, `data.status` and `status`.

```json
{
  "code": "not_found",
  "message": "Webhook not found.",
  "data": { "status": 404 },
  "status": 404
}
```

## The webhook object

Every route that returns a webhook uses these fields. The signing secret is never in the list. The list and `PUT` return only a masked form of it.

| Field | Type | What it holds |
|---|---|---|
| `id` | integer | The webhook ID. |
| `name` | string | A label for you. |
| `target_url` | string | The address that receives the request. |
| `events` | array of strings | The events it receives: `link_clicked`, `broken_link_found`, `link_created`, `broken_link_fixed`. |
| `status` | string | `active` or `paused`. A paused webhook receives nothing. |
| `method` | string | The HTTP method: `GET`, `POST`, `PUT`, `PATCH` or `DELETE`. |
| `ignore_signing` | string | `yes` sends requests without a signature. `no` signs them. |
| `headers` | object | Custom headers added to each request, as name and value strings. Empty as `[]` when there are none. |
| `has_secret` | string | `yes` or `no`. A secret is always made on create, so this is `yes` unless the stored secret cannot be read. |
| `secret_unreadable` | string | `yes` when a secret is stored but can't be decrypted, for example after the site's `AUTH_KEY` changed. Then `has_secret` is `no` and `secret_mask` is empty. Otherwise `no`. |
| `secret_mask` | string | The secret with every character but the last 4 replaced by `•`. Empty when there is no secret. |

**Signing.** With `ignore_signing` set to `no` and a secret stored, each request carries an `X-LMP-Signature` header. Its value is `sha256=` and the hex HMAC-SHA256 of `t=<timestamp>.<body>`, made with the raw secret. [Verify the signature](https://docs.linkmanagerpro.com/developers/hooks/events#verify-the-signature) has receiver code. Setting `ignore_signing` to `yes` keeps the secret but stops signing. With `ignore_signing` set to `no` and an unreadable secret, the delivery fails with `secret_unreadable` and is never sent unsigned. Regenerate the secret to fix it.

**The secret.** The plugin stores it encrypted. Create and regenerate return the raw value once in a `secret` field. `GET /webhooks/{id}/secret` reveals it at any time.

**Failures.** The plugin counts failed deliveries in a row for each webhook. After 15 in a row it sets `status` to `paused`. Failed deliveries with `secret_unreadable` count too. The count is not part of the object. Saving a new `secret` resets it to `0`. To resume a paused webhook, send `PUT /webhooks/{id}` with `status` set to `active`. Resuming does not reset the count. Only a delivered request or a new `secret` does.

## The delivery object

A delivery is one queued event for one webhook. The plugin sends it in the background, up to 5 tries per delivery.

| Field | Type | What it holds |
|---|---|---|
| `id` | integer | The delivery ID. |
| `webhook_id` | integer | The webhook it belongs to. |
| `event` | string | The event name, for example `link_clicked`. |
| `status` | string | `pending`, `sending`, `success` or `failed`. |
| `attempts` | integer | Tries made so far. |
| `response_code` | integer or null | The HTTP status of the last try. `0` when the receiver gave no answer. `null` before the first try. |
| `last_error` | string or null | Why the last try failed, for example `http_500` or `secret_unreadable`. `null` when there is no error. |
| `created_at` | string or null | When the event was queued, UTC `Y-m-d H:i:s`. |
| `delivered_at` | string or null | When the receiver accepted it, UTC `Y-m-d H:i:s`. `null` until then. |

**Status values.**

| Status | Meaning |
|---|---|
| `pending` | Queued, or waiting for the next try after a failed one. |
| `sending` | A background run has taken it and is sending it now. |
| `success` | The receiver answered with a status from `200` to `299`. |
| `failed` | The fifth try failed, or the webhook was deleted. |

A try that gets any other status, a timeout, or a blocked address fails. A failed try that is not the fifth puts the delivery back to `pending` with a wait of about 1 minute, then 5 minutes, then 30 minutes, then 2 hours. Details are in [Events, webhooks and AutomatorWP](https://docs.linkmanagerpro.com/developers/hooks/events#retries-and-pausing).

## Webhooks

### List webhooks

`GET /webhooks`

Returns every webhook, oldest first. There is no paging.

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

**Query parameters:** None.

**Response:** `200`. `data.data` is an array of [webhook objects](#the-webhook-object).

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

```json
{
  "success": true,
  "data": {
    "data": [
      {
        "id": 31,
        "name": "Zapier clicks",
        "target_url": "https://hooks.example.com/catch/123",
        "events": ["link_clicked", "link_created"],
        "status": "active",
        "method": "POST",
        "ignore_signing": "no",
        "headers": { "X-Source": "lmp" },
        "has_secret": "yes",
        "secret_mask": "••••••••••••••••••••••••••••••••••••a1b2"
      }
    ]
  }
}
```

### Create a webhook

`POST /webhooks`

Creates a webhook and returns it with its signing secret. A secret is generated when you send none.

**Permission:** `manage_options`

**Body parameters:**

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `name` | string | Yes | none | A label. 191 bytes or fewer. |
| `target_url` | string | Yes | none | Where requests go. A valid URL of 2048 bytes or fewer. It must use `https` and its host must resolve only to public addresses. Any port is allowed. |
| `events` | array of strings | Yes | none | At least one of `link_clicked`, `broken_link_found`, `link_created`, `broken_link_fixed`. |
| `status` | string | No | `active` | `active` or `paused`. |
| `method` | string | No | `POST` | `GET`, `POST`, `PUT`, `PATCH` or `DELETE`. A `GET` request has no body. |
| `ignore_signing` | boolean or string | No | `false` | `true`, `1`, `yes` or `on` turns signing off. Anything else leaves it on. |
| `secret` | string | No | generated | The signing secret, 255 bytes or fewer. Empty or missing generates a random 40 character one. |
| `headers` | object | No | none | Custom headers as name and value pairs. |

**Headers.** Names and values are cleaned as plain text. An entry with a non-string name or a value that is not a string, number or boolean is dropped. The code sets no limit on the number of headers. A header named `Content-Type`, `X-LMP-Event`, `X-LMP-Delivery`, `X-LMP-Timestamp` or `X-LMP-Signature` is stored but never sent, whatever its case.

**Response:** `200`, with the message "Webhook created." `data` is the [webhook object](#the-webhook-object) plus one field:

| Field | Type | What it holds |
|---|---|---|
| `secret` | string | The raw signing secret. |

```bash
curl -X POST -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Zapier clicks",
    "target_url": "https://hooks.example.com/catch/123",
    "events": ["link_clicked", "link_created"],
    "headers": { "X-Source": "lmp" }
  }' \
  https://example.com/wp-json/link-manager-pro/v1/webhooks
```

```json
{
  "success": true,
  "data": {
    "id": 31,
    "name": "Zapier clicks",
    "target_url": "https://hooks.example.com/catch/123",
    "events": ["link_clicked", "link_created"],
    "status": "active",
    "method": "POST",
    "ignore_signing": "no",
    "headers": { "X-Source": "lmp" },
    "has_secret": "yes",
    "secret_mask": "••••••••••••••••••••••••••••••••••••a1b2",
    "secret": "Qz7hT0w3Lk9mN2pR5vX8cB1dF4gJ6sUyaa1b2"
  },
  "message": "Webhook created."
}
```

**Errors:**

| Status | When |
|---|---|
| `422` | `name`, `target_url` or `events` is missing or empty. |
| `422` | `name` is over 191 bytes, `target_url` is not a URL or is over 2048 bytes, or `secret` is over 255 bytes. |
| `422` | An event is not one of the four. The error is keyed `events.0`, `events.1` and so on. |
| `422` | `status` or `method` is not an allowed value. |
| `422` | `target_url` does not use `https`, does not resolve, or resolves to a private or reserved address. `errors.target_url` says "The webhook URL must use https and point to a public host." |

### Update a webhook

`PUT /webhooks/{id}`

Changes the fields you send and leaves the others as they are. It returns the updated webhook without the secret.

**Permission:** `manage_options`

**Path parameters:**

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

**Body parameters:** the same fields as [Create a webhook](#create-a-webhook), all optional, with the same limits and allowed values.

| Name | Type | Required | Default | What it does |
|---|---|---|---|---|
| `name` | string | No | unchanged | A new label. |
| `target_url` | string | No | unchanged | A new address. It must pass the same `https` and public host check. |
| `events` | array of strings | No | unchanged | Replaces the list. A name that is not allowed is a `422`, as on create. |
| `status` | string | No | unchanged | `active` or `paused`. |
| `method` | string | No | unchanged | The new HTTP method. |
| `ignore_signing` | boolean or string | No | unchanged | Same values as on create. When the key is present, the value is applied. |
| `secret` | string | No | unchanged | A new secret. An empty value is ignored, and so is a value equal to the stored secret. A new secret resets the failure count. Setting `status` to `active` on a paused webhook resets it too. |
| `headers` | object | No | unchanged | Replaces all custom headers. Send `{}` or `null` to remove them. |

**Response:** `200`, with the message "Webhook updated." `data` is the [webhook object](#the-webhook-object), without `secret`.

```bash
curl -X PUT -u USER:APPLICATION_PASSWORD \
  -H "Content-Type: application/json" \
  -d '{ "status": "paused" }' \
  https://example.com/wp-json/link-manager-pro/v1/webhooks/31
```

```json
{
  "success": true,
  "data": {
    "id": 31,
    "name": "Zapier clicks",
    "target_url": "https://hooks.example.com/catch/123",
    "events": ["link_clicked", "link_created"],
    "status": "paused",
    "method": "POST",
    "ignore_signing": "no",
    "headers": { "X-Source": "lmp" },
    "has_secret": "yes",
    "secret_mask": "••••••••••••••••••••••••••••••••••••a1b2"
  },
  "message": "Webhook updated."
}
```

**Errors:**

| Status | When |
|---|---|
| `404` | No webhook has that ID. |
| `422` | A sent field breaks a limit or allowed value listed under [Create a webhook](#create-a-webhook). |
| `422` | `target_url` fails the `https` and public host check. `errors.target_url` holds the message. |

### Delete a webhook

`DELETE /webhooks/{id}`

Removes a webhook and all of its stored deliveries.

**Permission:** `manage_options`

**Path parameters:**

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

**Response:** `200`, with the message "Webhook deleted." `data` is `null`.

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

**Errors:**

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

### List a webhook's deliveries

`GET /webhooks/{id}/deliveries`

Returns the 50 most recent deliveries of one webhook, newest first.

**Permission:** `manage_options`

**Path parameters:**

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

**Query parameters:** None. There is no paging, and no way to ask for more than 50.

**Response:** `200`. `data.data` is an array of [delivery objects](#the-delivery-object). The queued payload is not included. The plugin keeps delivered rows for 7 days and failed rows for 30 days.

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

```json
{
  "success": true,
  "data": {
    "data": [
      {
        "id": 904,
        "webhook_id": 31,
        "event": "link_clicked",
        "status": "success",
        "attempts": 1,
        "response_code": 200,
        "last_error": null,
        "created_at": "2026-10-23 09:10:02",
        "delivered_at": "2026-10-23 09:10:07"
      }
    ]
  }
}
```

**Errors:**

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

### Reveal a webhook's secret

`GET /webhooks/{id}/secret`

Returns the raw signing secret of one webhook.

**Permission:** `manage_options`

**Path parameters:**

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

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

| Field | Type | What it holds |
|---|---|---|
| `secret` | string | The raw secret. Empty when none can be read. |

```json
{
  "success": true,
  "data": { "secret": "Qz7hT0w3Lk9mN2pR5vX8cB1dF4gJ6sUyaa1b2" }
}
```

**Errors:**

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

### Regenerate a webhook's secret

`POST /webhooks/{id}/regenerate-secret`

Replaces the signing secret with a new random one and returns it. The old secret stops working at once, so update your receiver.

**Permission:** `manage_options`

**Path parameters:**

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

**Body parameters:** None.

The new secret is 40 letters and digits. The webhook's failure count is reset to `0`.

**Response:** `200`, with the message "Signing secret regenerated." `data` is the [webhook object](#the-webhook-object) plus the raw `secret`, as in [Create a webhook](#create-a-webhook).

```json
{
  "success": true,
  "data": {
    "id": 31,
    "name": "Zapier clicks",
    "target_url": "https://hooks.example.com/catch/123",
    "events": ["link_clicked", "link_created"],
    "status": "active",
    "method": "POST",
    "ignore_signing": "no",
    "headers": { "X-Source": "lmp" },
    "has_secret": "yes",
    "secret_mask": "••••••••••••••••••••••••••••••••••••k3m9",
    "secret": "Bn4wR8yT2xQ6cV0zL5jH9sD1fG3aP7uEoo2k3m9"
  },
  "message": "Signing secret regenerated."
}
```

**Errors:**

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

### Send a test delivery

`POST /webhooks/{id}/test`

Sends one request to the webhook right now and returns how it went. The request has the event `ping`.

**Permission:** `manage_options`

**Path parameters:**

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

**Body parameters:** None.

The test goes out at once, signed like a real delivery, and does not use the queue. It is not stored, so it never shows in [the deliveries list](#list-a-webhooks-deliveries). It runs even when the webhook is paused. Its `data` holds `message` set to `Link Manager Pro webhook test`. The route answers `200` whether or not the receiver accepted the request. Read `ok`.

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

| Field | Type | What it holds |
|---|---|---|
| `ok` | boolean | `true` when the receiver answered with a status from `200` to `299`. |
| `response_code` | integer | The receiver's HTTP status. `0` when there was no answer or the address was blocked. |
| `error` | string or null | `null` on success. Otherwise `http_` and the status, a connection error text of up to 500 characters, or `blocked_url (private/reserved host or non-https)`. |
| `live` | boolean | Whether live events are sent. A test is sent either way. |
| `live_note` | string | A short note when `live` is `false`. Empty otherwise. |

The message is "Test delivered successfully." when `ok` is `true`. When it is `false`, the message is "Test delivery failed. See the response code and error." A delivered test resets the webhook's failure count. The test is sent either way. `live` is `false` when this site doesn't queue live events, and `live_note` then says why.

```bash
curl -X POST -u USER:APPLICATION_PASSWORD \
  https://example.com/wp-json/link-manager-pro/v1/webhooks/31/test
```

```json
{
  "success": true,
  "data": {
    "ok": true,
    "response_code": 200,
    "error": null,
    "live": true,
    "live_note": ""
  },
  "message": "Test delivered successfully."
}
```

**Errors:**

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

## Related

- [REST API overview](https://docs.linkmanagerpro.com/developers/rest-api/)
- [Events, webhooks and AutomatorWP](https://docs.linkmanagerpro.com/developers/hooks/events)
- [Webhooks and AutomatorWP](https://docs.linkmanagerpro.com/guides/webhooks-automation)
- [Action hooks](https://docs.linkmanagerpro.com/developers/hooks/actions)
- [All endpoints](https://docs.linkmanagerpro.com/developers/rest-api/endpoints)
