minmaxkey

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-SignatureHMAC-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:

  1. Customer buys on Lemon Squeezy / Gumroad / Stripe.
  2. Their webhook calls POST /v1/products/{id}/licenses with your admin token.
  3. Your server receives license.created, emails the key, etc.
  4. Refund/chargeback → POST /v1/licenses/{id}/revoke → you receive license.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.