Webhooks
Webhooks
License events delivered to your server, so you don't have to poll. Configure
one endpoint URL per product — in the dashboard (/admin → product → Webhooks)
or via the API.
Events
| Event | When |
|---|---|
license.created |
a key is issued (API or dashboard) |
license.activated |
a machine binds a fingerprint to the key |
license.deactivated |
a machine frees its seat |
license.reset |
all seats of a license were freed at once |
license.revoked |
the license is revoked (e.g. refund) |
license.expired |
the license's expiry passes (emitted once) |
webhook.test |
"send test event" button in the dashboard |
license.expired is only emitted when something actually checks the license
after expiry (activation or validation) — there is no background cron.
Delivery
Every event is POSTed to your URL as JSON — up to 4 attempts (the initial
try, then retries 1s / 5s / 15s later). Each attempt is logged in the
dashboard (status, attempts, last error). Two extra headers ride along:
X-MinMax-Event— the event name (handy for routing without parsing).X-MinMax-Signature—HMAC-SHA256(raw body, your webhook secret)hex. Verify it before trusting the payload.
Example payload:
{
"id": "625c16ceabe94c5ca230fa360bd21bdb",
"event": "license.activated",
"timestamp": "2026-08-18 10:41:52+00:00",
"data": {
"product": "pixelforge-pro",
"license": {"id": 1, "key": "9TMC-...", "status": "active", "...": "..."},
"device": {"fingerprint": "9f86d081...", "platform": "windows", "...": "..."}
}
}
Verifying signatures
import hashlib, hmac, json
from flask import Flask, request # or your favorite framework
app = Flask(__name__)
SECRET = "your webhook secret"
@app.post("/hook")
def hook():
body = request.get_data()
got = request.headers.get("X-MinMax-Signature", "")
expect = hmac.new(SECRET.encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(got, expect):
return "bad signature", 400
event = json.loads(body)
print(event["event"], event["data"]["license"]["key"])
return "ok" # anything < 400 triggers a retry otherwise
The money loop
The pattern this exists for:
- Customer buys on Lemon Squeezy / Gumroad / Stripe.
- Their webhook calls
POST /v1/products/{id}/licenseswith your admin token. - Your server receives
license.created, emails the key, etc. - Refund/chargeback →
POST /v1/licenses/{id}/revoke→ you receivelicense.revoked→ cut off any downstream access (cloud features, support, updates server).
Deliveries are retried, so a downed receiver doesn't lose events. There is no
replay protection yet — at solo-dev scale, dedupe by event id if you care.