Skip to content

Webhooks

Webhooks let WPLM notify an external system in real time whenever something happens — a license is created, a device is revoked, a subscription renews, and so on. WPLM POSTs a small, HMAC-signed JSON payload to a URL you control and retries automatically if your endpoint is temporarily unreachable.

This is the most common point of confusion, so it’s worth being explicit:

Who starts itDirectionUsed by
Pullvalidate / activate / heartbeatthe client asksApp → WPLMyour Flutter app, your licensed plugin, a desktop app
Push — webhookWPLM tells youWPLM → your serveryour backend, CRM, Slack, Zapier…

Your customers’ apps do not receive webhooks. A Flutter app or a licensed WordPress plugin checks licensing by calling the REST API (pull). A webhook is the reverse — WPLM pushes an event to one of your own server endpoints so your systems can react automatically without polling.

Example (Flutter app): the app calls /validate on launch to unlock pro features — no webhook involved. But when a customer’s subscription payment fails, you want their cloud features on your server cut off instantly. WPLM fires subscription.payment_failed → POSTs it to your backend → your backend disables their access. The webhook URL is your server, never the phone.

Example (WordPress plugin you sell): customers’ sites call /validate to unlock features (pull). When you revoke a license for piracy, WPLM pushes license.revoked to your update server so it stops serving updates for that key. The customer’s site doesn’t receive it — your backend does.

Yes. The Target URL can be any publicly reachable HTTPS endpoint you own — including one on the same domain where WPLM is installed. It only has to accept a POST and return a 200. The URL must be a full address, for example (replacing your-domain.com with your site):

https://your-domain.com/wp-json/my-app/v1/wplm-webhook ← a custom REST route
https://your-domain.com/wplm-webhook.php ← a standalone PHP file
https://hooks.your-domain.com/incoming ← a separate subdomain/app

You create that endpoint — WPLM only sends to it. Two easy ways to make one on your own domain:

A. A standalone PHP file placed in your site’s web root (wplm-webhook.php):

<?php
$secret = 'PASTE_THE_WEBHOOK_SECRET_FROM_WPLM';
$body = file_get_contents( 'php://input' );
$sig = $_SERVER['HTTP_X_WPLM_SIGNATURE'] ?? '';
if ( ! hash_equals( 'sha256=' . hash_hmac( 'sha256', $body, $secret ), $sig ) ) {
http_response_code( 401 );
exit; // Not a genuine WPLM request.
}
$event = json_decode( $body, true ); // [ 'event' => ..., 'data' => ..., 'timestamp' => ... ]
// Do your thing, e.g. log it:
file_put_contents( __DIR__ . '/wplm-events.log', $body . "\n", FILE_APPEND );
http_response_code( 200 ); // Acknowledge.

Then set Target URL to https://your-domain.com/wplm-webhook.php.

B. A custom REST route in your own plugin/theme functions.php:

add_action( 'rest_api_init', function () {
register_rest_route( 'my-app/v1', '/wplm-webhook', array(
'methods' => 'POST',
'permission_callback' => '__return_true',
'callback' => function ( WP_REST_Request $req ) {
$secret = 'PASTE_THE_WEBHOOK_SECRET';
$body = $req->get_body();
$sig = $req->get_header( 'x-wplm-signature' );
if ( ! hash_equals( 'sha256=' . hash_hmac( 'sha256', $body, $secret ), (string) $sig ) ) {
return new WP_REST_Response( 'invalid signature', 401 );
}
$event = json_decode( $body, true );
// handle $event['event'] / $event['data'] …
return new WP_REST_Response( 'ok', 200 );
},
) );
} );

Then set Target URL to https://your-domain.com/wp-json/my-app/v1/wplm-webhook.

Reacting on the same WordPress site? If your receiver lives on the same WP install as WPLM, you don’t need a webhook at all — just hook the internal PHP actions directly (add_action( 'wplm_license_created', … )). Webhooks are for separate apps/servers, or no-code tools like Slack, Zapier, or Make.

  1. You register a webhook endpoint (a URL), choose which events it should receive, and WPLM generates a signing secret for it.
  2. When a subscribed event fires, WPLM builds a JSON body and signs it with HMAC-SHA256(secret).
  3. WPLM POSTs the body to your URL with the signature in the X-WPLM-Signature header.
  4. The delivery is logged. If it fails (network error or a non-2xx response), WPLM retries hourly (up to 5 attempts within 24 hours) via WP-Cron.

Internally, the flow is:

wplm_* action (e.g. wplm_license_created)
WebhookEventBridge ── maps the action to a dotted event slug (license.created)
WebhookService::dispatch( event, payload )
│ • filters payload through `wplm_webhook_payload`
│ • signs body with the webhook's secret
│ • POSTs to every active endpoint subscribed to that event
wp_wplm_webhook_log ── records response code + delivery time; the
`wplm_retry_webhooks` cron retries failures hourly

License Manager → Webhooks → Add New. Provide:

FieldNotes
NameA label for your own reference.
Send toThe delivery preset — Custom endpoint, Slack, Discord, or Email (see below).
Delivery URL / addressThe URL or email address for the chosen preset.
EventsOne or more of the events below.
StatusActive webhooks receive deliveries; paused ones do not.

A signing secret is generated automatically (used by the Custom-endpoint preset).

You don’t have to write a receiver. Pick a preset and just paste a URL or address:

PresetWhat to pasteWhat happens
SlackYour Slack Incoming Webhook URL (https://hooks.slack.com/…)WPLM posts a short message to that Slack channel on each event. No code.
DiscordYour Discord channel Webhook URL (https://discord.com/api/webhooks/…)WPLM posts a message to that Discord channel. No code.
EmailAn email addressWPLM emails that address on each event. No code, no external service.
Custom endpointYour own HTTPS URLWPLM sends the signed JSON envelope (for your backend/SDK).

Creating a Slack/Discord incoming webhook takes a minute in their app settings and gives you a URL — that’s all you paste here.

After saving, open the webhook and click Send test delivery. WPLM sends a sample test.ping event to the destination and tells you whether it succeeded — so you can confirm Slack/Discord/Email/your endpoint works before relying on it.

EventFires when
license.createdA license is issued.
license.activatedA device activates a license.
license.deactivatedA device is deactivated / a seat is freed.
license.suspendedA license is put on temporary hold.
license.reinstatedA suspended/revoked license is restored.
license.revokedA license is revoked.
license.terminatedA license is permanently terminated.
license.expiredA license passes its expiry (+ grace).
license.renewedAn expired license is renewed back to active.
machine.revokedA single device is revoked.
subscription.createdA subscription is created.
subscription.renewedA renewal payment is processed.
subscription.payment_failedA renewal charge fails (includes the dunning attempt).
subscription.pausedA subscription goes on-hold.
subscription.resumedA paused subscription resumes.
subscription.cancelledA subscription is cancelled.
subscription.expiredA subscription reaches its end date.

Every delivery has the same envelope:

{
"event": "license.created",
"data": {
"id": 1042,
"status": 2,
"product_id": 17,
"order_id": 8841,
"user_id": 23,
"expires_at": "2027-01-01 00:00:00"
},
"timestamp": 1750000000
}

Headers:

Content-Type: application/json
X-WPLM-Signature: sha256=<hex hmac of the raw body>

The signature scheme matches the GitHub webhook style, so most existing client libraries verify it with minimal code.

Compute HMAC-SHA256 of the raw request body with your webhook’s secret and compare it (in constant time) to the value in X-WPLM-Signature.

// PHP receiver
$body = file_get_contents( 'php://input' );
$expected = 'sha256=' . hash_hmac( 'sha256', $body, $YOUR_WEBHOOK_SECRET );
$signature = $_SERVER['HTTP_X_WPLM_SIGNATURE'] ?? '';
if ( ! hash_equals( $expected, $signature ) ) {
http_response_code( 401 );
exit; // Reject — not from WPLM.
}
$payload = json_decode( $body, true );
// $payload['event'], $payload['data'], $payload['timestamp']
// Node.js receiver (Express, raw body)
import crypto from 'crypto';
const expected = 'sha256=' +
crypto.createHmac('sha256', YOUR_WEBHOOK_SECRET).update(rawBody).digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(401).end();
}

Respond with a 2xx status code to acknowledge receipt. Any other status (or a timeout) marks the delivery as failed and schedules a retry.

  • Failed deliveries are retried by the wplm_retry_webhooks cron, which runs hourly.
  • A delivery is retried up to 5 attempts, only within 24 hours of the original event. After that it is abandoned.
  • Each attempt is recorded in wp_wplm_webhook_log (event, response code, attempt count, delivered-at time).

Webhooks can also be managed through the REST API (requires an API key with the appropriate scope):

MethodEndpointPurpose
GET/wp-json/wplm/v1/webhooksList webhooks
POST/wp-json/wplm/v1/webhooksRegister a webhook
DELETE/wp-json/wplm/v1/webhooks/{id}Delete a webhook

Use the wplm_webhook_payload filter to add or remove fields before delivery — for example, to strip a sensitive field or attach your own metadata:

add_filter( 'wplm_webhook_payload', function ( array $payload, string $event ) {
if ( str_starts_with( $event, 'license.' ) ) {
unset( $payload['signature'] );
}
return $payload;
}, 10, 2 );