minmaxkey

Webhooks

Webhooks

Webhooks are server-to-server — MinMaxKey notifying your backend when a license changes state (issued, activated, revoked, renewed), so you can react without polling. They are not how your customer's app checks a license; that's the SDK. One endpoint URL per product, configured in the dashboard 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)
license.renewed a subscription renewal pushed the expiry forward
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:

  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.

A ready-made receiver that turns Stripe's webhooks into issued/revoked keys, with a local test walkthrough: Sell with Stripe.