Skip to content

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 firesPayload 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:

HeaderValue
X-LMP-EventThe event name, for example link_clicked.
X-LMP-DeliveryThe delivery ID. It is the same on every retry of one delivery.
X-LMP-TimestampThe Unix time in seconds, used in the signature.
X-LMP-Signaturesha256= and the hex signature. Absent when signing is off for the endpoint.
Content-Typeapplication/json, on requests with a body.
User-AgentLinkManagerPro-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 2xx code directly.
  • The request times out after 5 seconds. A code from 200 to 299 counts 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.

SettingValue
Attempts per delivery5
Waits between attempts60 s, 300 s, 1,800 s, 7,200 s, each plus or minus 20 percent
Auto-pauseAfter 15 failed deliveries in a row
Pending deliveries per endpoint500. New events for an endpoint that has 500 waiting are not queued.
Delivery log kept7 days for delivered requests, 30 days for failed ones
Deliveries per runUp 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)EventTags
A link is clickedlink_clickedevent, link_id, slug, target_url, click_count, country_code, referrer_domain
A broken link is foundbroken_link_foundevent, url, source_post_id, status_code, health_type
A link is createdlink_createdevent, link_id, slug, target_url, redirect_type
A broken link is fixedbroken_link_fixedevent, 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.

The action Create a link adds a short link. It has two fields, and both accept tags.

FieldRequiredWhat it does
Destination URL:YesWhere the short link redirects to. Only http and https addresses are accepted.
Slug (optional):NoThe 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.