All posts

Webhooks for licence keys: get told when keys are made, used, banned or sold

PWF Auth calls your server, or posts in your Discord channel, when something happens to your keys: made, first used, banned, leaked, sold in a store order. Signed deliveries, automatic retries, and the code to check them

3 min read

Your licence keys don't live in isolation. Your website wants to know when a key is first used, your CRM when a customer signs up, your team's Discord channel when a key is banned or a leak is suspected. Webhooks do that: PWF Auth calls an address of yours the moment something happens, with the details as JSON

Two kinds of address

  • Your server: any https:// address that answers with a 2xx status within 5 seconds. Each one gets its own signing secret, shown once
  • A Discord channel: paste the channel's webhook address (channel settings → Integrations → Webhooks → Copy Webhook URL), and each event arrives as a short message. No code at all

Add up to 10 addresses per app on the Webhooks page, pick the events each one should get, and press the test button to send a test event straight away and see what your address answered

The events

  • Keys: key.created, key.activated (first use, its time starts), key.redeemed, key.banned and key.unbanned, key.paused and key.resumed, key.time_changed, key.hwid_reset, key.deleted
  • Protection: key.suspected_leak (a key used from far-apart places in a short time), key.decoy_hit (someone tried a decoy key you planted), app.revoked
  • Customers: account.registered, hwid_reset.requested, trial.created
  • Sales: order.completed and order.refunded from your connected stores
  • Releases: update.published

Every delivery says where a change came from in by: the panel, Telegram, Discord, a reseller, a store, or the API

What arrives

POST /your-webhook
Content-Type: application/json
X-Webhook-Id: whd_3f1c…
X-Webhook-Timestamp: 1760262000
X-Webhook-Signature-V2: t=1760262000,v1=<hex HMAC-SHA256(t + "." + body, secret)>

{"event": "key.created", "timestamp": "2026-10-12T10:00:00Z",
 "data": {"app_id": "…", "count": 2, "keys": ["…", "…"], "key_type": "days", "duration_value": 30, "by": "panel"}}

Check every delivery

Anyone can call your address, so check the signature before acting on a delivery. Use the V2 header: it signs the time too, so an old delivery can't be played back. Compare with a constant-time function, and skip an X-Webhook-Id you already handled

$body = file_get_contents('php://input');
[$t, $v1] = sscanf($_SERVER['HTTP_X_WEBHOOK_SIGNATURE_V2'] ?? '', 't=%d,v1=%s');
$good = hash_hmac('sha256', $t . '.' . $body, getenv('PWF_WEBHOOK_SECRET'));
if (!$v1 || !hash_equals($good, $v1) || abs(time() - $t) > 300) { http_response_code(401); exit; }
$event = json_decode($body, true);

The same in Node.js:

const crypto = require('crypto');
function verify(rawBody, header, secret) {
  const { t, v1 } = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const good = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex');
  return v1 && crypto.timingSafeEqual(Buffer.from(good), Buffer.from(v1)) && Math.abs(Date.now() / 1000 - t) < 300;
}

When your server is down

Deliveries leave within a minute. One that fails is tried again after 1 minute, 5 minutes, 30 minutes and 2 hours, with the same X-Webhook-Id. An address that fails 20 times in a row is paused for you until you fix it. The page lists the last 40 deliveries with what your address answered

Ideas

  • Send a welcome email when key.activated arrives
  • Post key.suspected_leak and key.decoy_hit to a private staff channel, so a leak is seen in minutes. See the leak tracer
  • Keep your own sales sheet up to date from order.completed and order.refunded

Webhooks tell you what happened. To change keys from your own code, use the Developer API. The full reference is in Webhooks

Protect your app for free

Licence keys, customer accounts and the kill switch, in every language. Make an account and sign in with your first key in a few minutes

Create a free account