Skip to main content

Webhooks

Licensr can POST an event to your server whenever something happens to one of your licenses. Use it to send license emails from your own domain, add buyers to your CRM, or keep your own records in sync.

You don't need webhooks to sell: by default Licensr emails the key and your product checks it with the License API. Add them when you want your own server in the loop.

Add an endpoint under Developers → API & Webhooks → Webhooks in the dashboard. Only your organization's admins can add or change endpoints.

Events​

EventWhen
license.issuedA purchase completed or an admin issued a license. Includes the license key.
license.key_regeneratedThe buyer or an admin reset the key. Includes the new key; the old one stopped working.
license.revokedAn active license was turned off: a refund, a chargeback, an unpaid subscription, or an admin.
license.expiredAn active license ran out: it passed its end date, or a canceled subscription's last period ended.
subscription.renewedA subscription paid for its next period.
subscription.canceledA subscription was canceled.
subscription.past_dueA subscription payment failed. The license keeps working until grace_until.
pingSent when you add an endpoint, change its URL, rotate its secret, or click Test. Answer with any 2xx.

Every request has the same envelope:

{
"id": "evt_3f2c...",
"type": "license.issued",
"livemode": true,
"created_at": "2026-10-21T14:03:11.402+00:00",
"data": {
"license": {
"id": "5b1e...",
"key": "lic_...",
"status": "active",
"plugin_slug": "resizer-pro",
"plan_id": "1c4f...",
"customer_email": "buyer@example.com",
"expires_at": null,
"max_seats": 3,
"max_domains": null
}
}
}

Subscription events carry data.subscription with id, status, plan_id, customer_email, current_period_end, canceled_at and grace_until.

livemode is true for a real buyer and false for the test pool. An event goes only to endpoints of the same pool. When you add an endpoint, choose Live (the default, and what an endpoint you already had stays) or Test. Test keys, test licenses, and test endpoints are for your dev or staging app. A purchase is live unless it is a Stripe test purchase (see Testing payments); the hosted buy page always sells live.

Testing payments​

Run a Stripe purchase end to end with no real money:

  1. In Stripe's test mode, create a price for each plan. Link it under Developers → API & Webhooks → API keys → Stripe test prices.
  2. Start the purchase:
    • POST /v1/billing/checkout with a test key (pk_test_…) sells the plan's test price. Add "test_clock": true to put the buyer on a new Stripe test clock. Advance the returned test_clock_id in Stripe to run renewals and failed payments.
    • Or sell the test price from a Stripe pricing table or payment link.
  3. Pay with a Stripe test card. license.issued reaches your test endpoints with livemode: false.

A test purchase emails no one, creates no customer account, and stays out of revenue. Test checkout works with Stripe only: other providers return 409 test_checkout_unsupported, and a plan with no test price returns 409 test_price_missing.

When a renewal fails, subscription.past_due carries grace_until, and the license keeps working until then. If no payment succeeds by that date, the license is turned off and you get license.revoked. If Stripe cancels the subscription first, you get subscription.canceled, then license.expired when the paid period ends.

The license key is only in license.issued and license.key_regenerated. Licensr stores keys hashed, so it keeps the plaintext (encrypted) only until the event is delivered, or for at most 7 days if delivery keeps failing. After that the delivery can't be replayed; reset the license key to send a new license.key_regenerated.

Send license emails from your own domain​

By default Licensr emails the buyer their key. To send it yourself, from your own address and with your own open and click tracking:

  1. Add an endpoint and make sure its test ping succeeds.
  2. On Developers → API & Webhooks → Webhooks, turn on My server sends license emails. Licensr refuses unless an enabled endpoint that is not flagged as failing answered a test ping or took a delivery in the last 7 days.
  3. Email the key from license.issued (and the new one from license.key_regenerated).

From then on Licensr sends no license or key-reset emails. Customer accounts are a separate switch, Create a customer account with each purchase, on the Storefront page. There is no fallback: if your endpoint fails, Licensr retries, flags the endpoint and emails you, but never emails the buyer instead. Replay the failed deliveries once it is fixed. While this is on, Licensr also refuses to delete, disable or change the URL of the last working endpoint.

Verify the signature​

Every request carries:

  • Licensr-Signature: t=<unix seconds>,v1=<hex>: an HMAC-SHA256 of <t>.<raw request body>, keyed with your endpoint's signing secret (whsec_..., shown once when you add the endpoint or rotate it).
  • Licensr-Event-Id and Licensr-Event-Type.

Check the signature against the raw body before parsing it, and reject requests whose timestamp is more than a few minutes old.

import crypto from 'node:crypto';

export function verifyLicensrSignature(rawBody, header, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(',').map((part) => part.split('=')));
const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) <= toleranceSeconds;
return (
fresh &&
expected.length === parts.v1?.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
);
}
import hashlib, hmac, time

def verify_licensr_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
parts = dict(part.split("=", 1) for part in header.split(","))
expected = hmac.new(secret.encode(), f"{parts['t']}.".encode() + raw_body, hashlib.sha256).hexdigest()
fresh = abs(time.time() - int(parts["t"])) <= tolerance
return fresh and hmac.compare_digest(expected, parts.get("v1", ""))

Respond and retry​

  • Answer with any 2xx within 10 seconds. Licensr only looks at the status code and never reads the response body. Do slow work (sending an email, calling your CRM) after responding.
  • Anything else, a timeout, or a redirect counts as a failure. Licensr retries after about 5 minutes, 30 minutes, 2 hours, 6 hours and 12 hours, then marks the delivery failed. Retries run on a schedule, so they can be a little later than that.
  • The same event can arrive more than once. Use Licensr-Event-Id (the envelope id) to ignore duplicates.
  • After 5 failures in a row the endpoint is flagged in the dashboard and your organization's contact email is told. The next successful delivery or test ping clears the flag.
  • Replay a failed delivery from the dashboard once your endpoint is fixed.

Endpoint rules​

The URL must use https and resolve to a public internet address; Licensr does not follow redirects. To test against your own machine, expose it with a tunnel such as ngrok or cloudflared.