Get an HTTPS callback the moment an affiliate link breaks, recovers, redirects, or a scan completes. HMAC-signed, retried with exponential backoff for 24 hours, with a delivery log you can replay from. Pipe into Slack, Discord, Zapier, or any HTTPS endpoint your team uses.
A scan tells you what's broken. A digest tells you once a week. A webhook tells you in the same minute it happens. For teams with editors, ops people, or revenue dashboards, that gap is where commission is lost. Webhooks close it.
Subscribe to all of them, or filter to the ones your workflow needs. Each event fires exactly once per state change (no flooding the same broken link every scan).
| Type | When it fires |
|---|---|
| link.broken | An affiliate link is newly detected as broken or timing out. Diff-based, so it won't re-fire while the same link stays broken. |
| link.recovered | A previously-broken affiliate link is no longer broken (fixed, removed from the post, or replaced). |
| link.redirect | An affiliate link is newly detected as redirecting. Often the first signal of a merchant URL change. |
| link.tag_stripped | Your affiliate tag was dropped somewhere in the redirect chain. You're losing commission on this link even though it works for the visitor. |
| scan.completed | A site scan finished. Includes full totals (broken, redirect, blocked, ok) plus delta vs the previous scan. |
| site.connected | A new site was added to your workspace. |
| tracking.sync_failed | A revenue-provider sync (e.g. WeCanTrack) errored out 3 times in a row. Caught by the connector health monitor. |
| revenue.alert | Revenue dropped past the alert threshold for a connected site. |
| webhook.test | Fired by the "Send test event" button in the dashboard. Always delivered, ignores event filters. |
Every event ships in a versioned envelope. The event_id is stable across delivery retries, so use it to dedupe on your side. delivery_id is unique per attempt.
{
"api_version": "2026-05-01",
"event_id": "9f4d2e1c-8b4a-4f2a-a1c0-b2e5d7c8f9a0",
"delivery_id": "1c5b9f3e-6a8d-4c2b-9e7f-a3b1d4c5e6f7",
"type": "link.broken",
"created_at": "2026-05-01T07:32:14.092Z",
"workspace_id": 5,
"site_id": 12,
"data": {
"url": "https://example.com/affiliate/123?ref=abc",
"status_code": 404,
"status_class": "broken",
"network": "amazon",
"days_failing": 3,
"is_affiliate": true,
"found_in_post_id": 4421,
"found_in_post_title": "Best wireless headphones 2026",
"found_in_post_url": "https://yoursite.com/best-headphones/",
"final_url": "https://example.com/404"
},
"_docs_url": "https://linkpulse.dev/connectors/webhooks/events#link-broken"
}Every request is signed with HMAC-SHA256 over ${timestamp}.${rawBody} using your webhook's secret. The header is X-LinkPulse-Signature: t=<ts>,v1=<hex>. Reject anything older than 5 minutes to defeat replay attacks.
const crypto = require('crypto');
app.post('/webhooks/linkpulse',
express.raw({ type: 'application/json' }), // raw body required
(req, res) => {
const sig = req.header('X-LinkPulse-Signature') || '';
const parts = Object.fromEntries(sig.split(',').map(s => s.split('=')));
const t = parseInt(parts.t, 10);
if (!t || Math.abs(Date.now()/1000 - t) > 300) return res.status(400).end();
const expected = crypto
.createHmac('sha256', process.env.LP_WEBHOOK_SECRET)
.update(`${t}.${req.body.toString()}`)
.digest('hex');
if (parts.v1 !== expected) return res.status(401).end();
const event = JSON.parse(req.body.toString());
handle(event); // dedupe by event.event_id
res.json({ ok: true });
});$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_LINKPULSE_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $sig), $parts);
$t = (int)($parts['t'] ?? 0);
if (!$t || abs(time() - $t) > 300) { http_response_code(400); exit; }
$expected = hash_hmac('sha256', $t . '.' . $raw, getenv('LP_WEBHOOK_SECRET'));
if (!hash_equals($expected, $parts['v1'] ?? '')) { http_response_code(401); exit; }
$event = json_decode($raw, true);
handle($event); // dedupe by $event['event_id']
http_response_code(200);Pre-built receivers you can drop in without writing code.
Post broken-link alerts to a channel.
link.broken + link.tag_strippedSame idea, dev-team channel.
/api/webhooks/...)/slack for Slack-compatible formattingFan out to anything (email, sheets, CRM).
Your own backend, your rules.
POST application/jsonevent_idWebhooks aren't fire-and-forget. We treat them like a queue, not a notification.
Failed deliveries retry at 5s, 30s, 2m, 10m, 1h, 6h, 24h. After 7 failed attempts the delivery moves to dead-letter, still replayable from the dashboard.
At most one delivery per webhook per 2 seconds. A site with 1,000 broken links won't drown your Slack channel; events spread out automatically.
Webhook destinations are DNS-resolved before each POST. Private IPs (10/8, 192.168/16, link-local, IPv6 ULA) are rejected at delivery time.
The last 30 days of attempts are visible per webhook: status, response code, response body, latency. Manual replay button on failed and dead deliveries.
It takes a minute. Pick a name, paste a URL, choose your events, save. Click Test to confirm your endpoint receives the event. Real events start flowing on the next scan.
Open the dashboard