Appearance
Events, webhooks and AutomatorWP
Link Manager Pro fires four events. Each one runs an action hook, can be sent to your webhook endpoints, and can start an AutomatorWP automation.
Link Manager Pro fires four events: link_clicked, broken_link_found, link_created and broken_link_fixed. You can react to them with an action hook, a signed webhook request, or an AutomatorWP trigger.
To set up an endpoint on screen, see Webhooks and AutomatorWP.
The four events
Each event runs a WordPress action named link_manager_pro_ plus the event name, with one flat array of values. The array also holds an event key with the event name, set by the plugin. Listener errors never break the page that fired the event, and a listener that fires the same event again is skipped.
| Event (action hook) | When it fires | Payload keys |
|---|---|---|
link_clicked (link_manager_pro_link_clicked) | A short link redirect is served to a visitor who is counted. Rate-limited and filtered visits, such as bots, do not fire it. | link_id (int), slug, target_url, click_count (int), country_code (empty string when unknown), referrer_domain (empty string when none) |
broken_link_found (link_manager_pro_broken_link_found) | A broken outbound link in your content is detected for the first time. | url, source_post_id (int), status_code (int), health_type (content), detected_at (UTC, Y-m-d H:i:s) |
link_created (link_manager_pro_link_created) | A short link is created. | link_id (int), slug, target_url, redirect_type (int: 301, 302 or 307), created_by (user ID, 0 when there is no user) |
broken_link_fixed (link_manager_pro_broken_link_fixed) | A broken link is marked fixed, or a redirect is created for it. | url, source_post_id (int), health_type, resolution (marked_fixed or redirect_created) |
The hook signatures are on Action hooks. The source is app/Support/Events.php.
Webhook requests
Each endpoint you add under Link Manager › Settings › Webhooks gets the events you ticked as an HTTP request. The queue sends it in the background, never during the visitor's redirect.
The method is one of GET, POST, PUT, PATCH or DELETE. It defaults to POST. Any other value is stored as POST.
JSON body. Every method except GET sends this body, with Content-Type: application/json:
json
{
"event": "link_clicked",
"delivery_id": "0b0e3c6e-5d1c-4f56-9d6e-0f1f7f6d2a11",
"timestamp": 1792756800,
"site": "https://example.com",
"data": {
"link_id": 123,
"slug": "my-link",
"target_url": "https://example.com/landing",
"click_count": 42,
"country_code": "US",
"referrer_domain": "google.com"
}
}data holds the event's payload keys from the table above, without event. timestamp is a Unix time in seconds, and delivery_id is a UUID.
GET requests. A GET request has no body. The event and delivery ID go in the query string as lmp_event and lmp_delivery. The event's data is not sent.
Headers. Every request carries these headers:
| Header | Value |
|---|---|
X-LMP-Event | The event name, for example link_clicked. |
X-LMP-Delivery | The delivery ID. It is the same on every retry of one delivery. |
X-LMP-Timestamp | The Unix time in seconds, used in the signature. |
X-LMP-Signature | sha256= and the hex signature. Absent when signing is off for the endpoint. |
Content-Type | application/json, on requests with a body. |
User-Agent | LinkManagerPro-Webhook/1.0 (+https://example.com), with your site address. |
Custom headers you add to an endpoint are sent too. A custom header named Content-Type, X-LMP-Event, X-LMP-Delivery, X-LMP-Timestamp or X-LMP-Signature is ignored, whatever its case.
Limits on the endpoint. These apply to every request:
- The address must use
https. Any port is allowed. - Every address the host name resolves to must be public. Private and reserved addresses are refused.
- Redirects are not followed. Answer with a
2xxcode directly. - The request times out after 5 seconds. A code from
200to299counts as delivered, and anything else counts as a failed attempt.
Test ping. Send test sends the event ping right away, signed like any delivery. Its data is { "message": "Link Manager Pro webhook test" }.
Verify the signature
The signature proves a request came from your site and the body was not changed. Compute an HMAC-SHA256 of the string t=, the X-LMP-Timestamp value, a dot, and the raw request body. Use the endpoint's signing secret as the key. Compare the result to the hex part of X-LMP-Signature, which has the form sha256=<hex>.
Sign the raw body exactly as received, not a re-encoded copy of the parsed JSON. On a GET request the body is empty, so the signed string ends after the dot.
The plugin does not enforce a time window. Your receiver chooses one, for example 5 minutes, and rejects older timestamps.
If X-LMP-Signature is missing, signing is off for that webhook: Send unsigned (ignore secret) is switched on under Request signing in the endpoint's form. A signed webhook whose secret can't be read sends nothing: the delivery fails with last_error secret_unreadable. So a missing header means only that Send unsigned is on. Treat unsigned requests as untrusted.
PHP receiver
This function checks the header and the timestamp, and answers 401 when either is wrong. It runs in a plain PHP file on your own server.
php
<?php
function myprefix_verify_lmp_webhook( string $secret ): array {
$body = file_get_contents( 'php://input' );
$timestamp = $_SERVER['HTTP_X_LMP_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_LMP_SIGNATURE'] ?? '';
if ( '' === $signature || ! ctype_digit( $timestamp ) || abs( time() - (int) $timestamp ) > 300 ) {
http_response_code( 401 );
exit;
}
$expected = 'sha256=' . hash_hmac( 'sha256', 't=' . $timestamp . '.' . $body, $secret );
if ( ! hash_equals( $expected, $signature ) ) {
http_response_code( 401 );
exit;
}
return json_decode( $body, true ) ?? array();
}
$event = myprefix_verify_lmp_webhook( 'YOUR_SIGNING_SECRET' );
http_response_code( 200 );Node.js receiver
This Express handler keeps the raw body with express.raw, so the signature is computed over the exact bytes.
js
const crypto = require('crypto');
const express = require('express');
const app = express();
const SECRET = process.env.LMP_SIGNING_SECRET;
app.post('/lmp-webhook', express.raw({ type: '*/*' }), (req, res) => {
const timestamp = req.get('X-LMP-Timestamp') || '';
const signature = req.get('X-LMP-Signature') || '';
if (!signature || !/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.sendStatus(401);
}
const expected = 'sha256=' + crypto
.createHmac('sha256', SECRET)
.update(`t=${timestamp}.`)
.update(req.body)
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
console.log(event.event, event.delivery_id);
res.sendStatus(200);
});
app.listen(3000);Retries and pausing
A delivery is tried up to 5 times. After a failed attempt, the next one waits about 1 minute, then 5 minutes, then 30 minutes, then 2 hours. Each wait varies by up to 20 percent, up or down. After the fifth failure the delivery is marked failed.
An endpoint is paused automatically after 15 failed deliveries in a row. One delivered request resets the count, and so does a delivered test. Resume a paused endpoint with its Active switch, which also resets the count.
| Setting | Value |
|---|---|
| Attempts per delivery | 5 |
| Waits between attempts | 60 s, 300 s, 1,800 s, 7,200 s, each plus or minus 20 percent |
| Auto-pause | After 15 failed deliveries in a row |
| Pending deliveries per endpoint | 500. New events for an endpoint that has 500 waiting are not queued. |
| Delivery log kept | 7 days for delivered requests, 30 days for failed ones |
| Deliveries per run | Up to 20, within a 15 second budget |
Delivery runs from WordPress cron, so it needs cron to run on your site. The hook link_manager_pro_webhook_drain runs every five minutes. When an event is queued, the plugin also schedules the single event link_manager_pro_webhook_nudge to start a run on the next request. Old delivery rows are removed once a day by link_manager_pro_webhook_purge.
A paused endpoint's waiting deliveries are moved back an hour without using an attempt. They continue when the endpoint is active again.
AutomatorWP
The integration loads only when AutomatorWP is active. It adds a Link Manager Pro integration with four triggers and one action. The click and broken link triggers are anonymous, so they run without a WordPress user.
Each trigger offers the event's payload keys as tags. Use a tag in any AutomatorWP field, for example {slug}.
| Trigger (as shown in AutomatorWP) | Event | Tags |
|---|---|---|
| A link is clicked | link_clicked | event, link_id, slug, target_url, click_count, country_code, referrer_domain |
| A broken link is found | broken_link_found | event, url, source_post_id, status_code, health_type |
| A link is created | link_created | event, link_id, slug, target_url, redirect_type |
| A broken link is fixed | broken_link_fixed | event, url, source_post_id, health_type, resolution |
The tag labels in AutomatorWP are friendlier, such as Destination URL, Total clicks and Resolved URL. The tag names above are the payload keys.
Action: Create a link
The action Create a link adds a short link. It has two fields, and both accept tags.
| Field | Required | What it does |
|---|---|---|
| Destination URL: | Yes | Where the short link redirects to. Only http and https addresses are accepted. |
| Slug (optional): | No | The short link's slug. Left blank, the plugin generates one. |
The new link is stored with the source automatorwp. The plugin never fires an event while the same event is still running, so an automation that starts from A link is created and creates a link cannot loop: that new link's own link_created is skipped. If the destination is empty or not http or https, the action does nothing, and a failed create never stops the automation.