Skip to content

Developers

How to Set Up Magic Link Webhooks

Want another tool to know the instant someone uses, or fails to use, a magic link — without anyone watching a screen? Magic Link PRO can send an outbound webhook for every login event. Open Magic Link → Webhooks, add the address of a service that should hear about activity, choose which events to send, and your site notifies that service as things happen. It works with Zapier, Make, n8n, Slack, and any endpoint that accepts an HTTP POST.

PRO feature

Outbound webhooks are available in Magic Link PRO. The free plugin records the same events in the Audit Log, which you check on demand rather than being notified.

A webhook is a message your site sends to another tool the moment something happens, so that tool can react without polling or checking a screen. Magic Link fires webhooks from its audit-log events, which turns magic link activity into something you can act on:

  • Connect to tools you already use. Zapier, Make, and n8n all accept webhooks, so from one event you can post to Slack, add a spreadsheet row, or update a CRM without writing code.
  • Notice trouble early. Repeated failed events are a sign of a problem worth investigating while it is happening.
  • Trigger your own follow-up. Someone used their link for the first time — send a welcome message, tag them, start onboarding.
  • Keep records outside WordPress. Send events straight to a helpdesk or data warehouse instead of asking people to log in and look.

The Audit Log answers "what happened?" when you go and look. Webhooks answer "tell me when it happens" without you looking.

Events you can send

Event When it fires
generated A magic link was created.
sent A magic link was emailed.
opened Someone opened a magic link.
used A magic link successfully logged someone in.
revoked A magic link was cancelled.
failed An attempt to use a magic link did not succeed.
expired Listed on the endpoint screen, but not sent by the current release (2.3.0).
delivered Listed on the endpoint screen, but not sent by the current release.
bounced Listed on the endpoint screen, but not sent by the current release.

Events that are listed but not sent yet

Webhooks are driven by the Audit Log, and the plugin does not record expired, delivered, or bounced entries yet. You can tick them (or use All events) without any harm, but nothing will arrive for them. WordPress does not report email delivery or bounces, so to chase "the member says they never got it", use your email provider's own logs.

An event also only fires for the flows that record it. For example, generated is sent for links created by the shortcode or block, the REST API, PRO bulk claim emails and the MemberPress or Paid Memberships Pro integrations, but not for links requested on the standard wp-login.php screen, where you get sent and used.

How to add a webhook

  1. In WordPress, go to Magic Link → Webhooks and click Add New.
  2. Destination URL — paste the address that receives the events, for example https://hooks.example.com/receive. In Zapier, Make, or n8n, create the "Webhook" trigger first and copy the address it shows you. HTTPS is strongly recommended.
  3. Signing Secret — a long random string used to sign every message. Leave it blank and Magic Link generates one for you. You will need it on the receiving side to verify messages (see below).
  4. Events — tick All events (wildcard) to receive everything, or tick only the events you need. See Events you can send for which events are actually sent today.
  5. Enabled — keep Deliver events to this endpoint ticked. Untick it to pause the endpoint without deleting it.
  6. Click Add Webhook. (When editing an existing endpoint the button reads Save Webhook.)
  7. Open the endpoint again and click Test Fire to send a sample message immediately. You will see "Test fired successfully (HTTP 200)" or "Test delivery failed (HTTP …). Check the receiver logs." The Test Fire button only appears on endpoints that have already been saved.

The list under Magic Link → Webhooks also lets you enable, disable, or delete several endpoints at once.

Test Fire is the fastest way to know your setup works — use it before you rely on the connection.

Payload and signature

Each webhook is an HTTP POST with a JSON body in this shape:

{
  "id": 1234,
  "id": 123,
  "event": "used",
  "occurred": "2026-09-04T10:15:00Z",
  "data": {
    "user_id": 42,
    "email": "alex@example.com",
    "ip": "203.0.113.10",
    "meta": {}
  }
}

Every request is signed with HMAC using your endpoint secret and carries these headers so the receiver can verify it is genuine and unaltered:

Header Meaning
X-KCML-Signature sha256= followed by the hex HMAC-SHA256 signature (see below).
X-KCML-Timestamp When the message was signed, as a Unix timestamp in seconds.
X-KCML-Event The event name, so you can route without parsing the body.
X-KCML-Attempt Which delivery attempt this is (see retries below).

How to verify the signature

The signature is an HMAC-SHA256 of the timestamp, a dot, and the raw request body, keyed with your Signing Secret:

signature = "sha256=" + hex( HMAC_SHA256( secret, timestamp + "." + raw_body ) )

Use the exact raw body as received (before any JSON parsing), take the timestamp from the X-KCML-Timestamp header, and compare in constant time. Also reject requests whose timestamp is more than a few minutes old (300 seconds is a good limit) so a captured request cannot be replayed later.

PHP receiver:

function verify_kcml( $raw_body, $sig_header, $ts_header, $secret ) {
	if ( abs( time() - (int) $ts_header ) > 300 ) {
		return false; // Too old.
	}
	$expected = 'sha256=' . hash_hmac( 'sha256', $ts_header . '.' . $raw_body, $secret );
	return hash_equals( $expected, $sig_header );
}

$ok = verify_kcml(
	file_get_contents( 'php://input' ),
	$_SERVER['HTTP_X_KCML_SIGNATURE'] ?? '',
	$_SERVER['HTTP_X_KCML_TIMESTAMP'] ?? '',
	'YOUR_SIGNING_SECRET'
);

Node.js receiver (use the raw body, for example express.raw({ type: 'application/json' })):

const crypto = require('crypto');

function verifyKcml(rawBody, sigHeader, tsHeader, secret) {
  if (Math.abs(Date.now() / 1000 - Number(tsHeader)) > 300) return false; // too old
  const expected = 'sha256=' + crypto.createHmac('sha256', secret)
    .update(`${tsHeader}.${rawBody}`).digest('hex');
  const a = Buffer.from(expected), b = Buffer.from(sigHeader || '');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

If verification fails, the usual causes are: hashing the parsed JSON instead of the raw body, leaving out the timestamp. prefix, forgetting the sha256= prefix, or using a different secret than the one saved on the endpoint.

Why the secret matters

Your webhook address is just a URL, and anyone who learns it could send messages pretending to be your site. The secret prevents that: every message is signed with it, so the receiving service can verify the message genuinely came from your site and was not altered in transit.

  • Use a long random string, not a word.
  • Use a different secret for each endpoint.
  • Treat it like a password — anyone with it can impersonate your site to that endpoint.

If the service you are connecting to cannot verify signatures itself (many no-code tools cannot), use a private, hard-to-guess endpoint address and treat the URL as a secret too.

Delivery and retries

Magic Link posts each event to your endpoint immediately. If the receiving service returns a 5xx error or the request fails at the transport level, delivery is retried with exponential backoff: after the first failure Magic Link waits 30 seconds, then 60, 120, and 240 seconds, for 5 attempts in total before giving up. Other responses, such as a 4xx error, are not retried. Each request waits up to 8 seconds for an answer. Retries are run by WP-Cron, so on a site with WP-Cron disabled they only happen if a real cron job triggers it, and on low-traffic sites they can run later than scheduled. The X-KCML-Attempt header tells the receiver which try it is looking at.

If messages are not arriving

  • Use Test Fire first. It isolates the connection from everything else.
  • Check the URL is exactly right. A trailing character, or an http that should be https, is enough to break it.
  • Check the receiving service is listening. Zapier and Make webhooks sometimes need the scenario switched on before they accept anything.
  • Check the event is subscribed. If you only subscribed to used, nothing happens when a link is merely generated.
  • Check whether your host blocks outbound requests. Some managed hosts restrict them; if Test Fire never arrives and everything else looks right, ask your host.

Conclusion

Magic Link PRO webhooks push every login event — from generated and sent to used, failed, and revoked — to Zapier, Make, n8n, Slack, or your own endpoint the moment it happens, signed with HMAC and retried on failure. Add an endpoint under Magic Link → Webhooks, confirm it with Test Fire, and you can automate follow-ups and monitoring outside WordPress. To create links from those same external systems, see the REST API.

FAQs

No. Outbound webhooks are a Magic Link PRO feature. The free plugin records the same events in the Audit Log, which you review on demand under Magic Link → Audit Log, but it does not push notifications to external services. Webhooks add the "tell me the moment it happens" layer on top of that log.

How do I verify a webhook really came from my site?

Every request is signed with HMAC using your endpoint secret and sent with an X-KCML-Signature header, plus X-KCML-Timestamp and X-KCML-Event. Recompute the HMAC of the payload with your secret and compare it to the signature header. A match confirms the message came from your site and was not altered in transit.

What happens if my endpoint is temporarily down?

Magic Link retries. If your endpoint returns a 5xx error or the connection fails, delivery is retried with exponential backoff (30, 60, 120, then 240 seconds), for 5 attempts in total. A brief outage on the receiving side usually will not lose the event, but an outage longer than about 7 minutes can. The X-KCML-Attempt header shows which attempt each request is.

Yes. Create a "Webhook" trigger in Zapier, Make, or n8n, copy the address it gives you, and paste it as the endpoint URL under Magic Link → Webhooks. Use the Test Fire button to send a sample so the platform captures the payload shape, then build your automation from the events you subscribed to.

What is the difference between the sent and used events?

sent means the email carrying the magic link was handed to WordPress for sending. It does not prove the email arrived. used means someone clicked the link and was logged in. Many sent events with few used events usually means people are not receiving the email or not finishing the sign-in. The delivered and bounced events shown on the endpoint screen are not sent by the current release.

Was this page helpful?