Skip to content
License Server v1.34.0 · released 6 Nov 2026release notes →
Skip to content

Release Notes ​

What changed in each release of License Server, and what it means if you run it yourself. Every version listed here is downloadable from the public release mirror; the installation guide covers upgrading an existing deployment.

Hosted accounts are upgraded by Casazium and are always on the current version — there is nothing to do on your side, and nothing below applies to you unless it says so.

v1.34.0 — 6 November 2026 ​

The buyer portal shows how many machines a license is using. Hosted accounts already run 1.34.0 and need do nothing.

Added ​

  • The buyer portal shows how many machines a license is using out of its limit ("Machines: 18 of 25", or "(no limit)"). With seats that free themselves, machines that haven't checked in recently still count; the page says how many there are, and that when the license is full a new machine takes one of their seats.

Changed ​

  • Buyer portal: a machine that hasn't checked in is marked inactive only on an active license with a machine limit, the only case where a new machine can take its seat, and the label now says so.

If you run it yourself (MULTI_TENANT off): the portal change reaches your buyers if you've turned the portal on (PORTAL_ENABLED). There is nothing to migrate and no new setting to make. The self-hosted bundle download now pins License Server 1.34.0 with console 1.20.1.

Console 1.20.1 ​

Released on 6 November 2026: clearer wording for machine counts. No License Server change is needed. There are no setting changes.

  • Issue license: Max activations now reads "How many machines can activate this license". It used to say "at the same time", which suggested concurrent use; a license limits activated machines, not running ones. Edit terms says the same, and the Limits box says its limits aren't the number of machines.
  • Storefront mappings: Activations per unit says it also follows a Lemon Squeezy or Paddle order's quantity, as on a one-time purchase: see Per unit on one-time purchases.

v1.33.1 — 6 November 2026 ​

A fix to how the server is built. Hosted accounts already run 1.33.1 and need do nothing.

Fixed ​

  • The published server no longer differs from its source in a few edge cases. One obfuscation setting used to build the Docker image and the self-hosted binary could run part of an expression that the source skips. It is now off. The shipped-bundle test run found no affected route that a self-hosted server serves.

If you run it yourself (MULTI_TENANT off): upgrading is recommended, so you run the build that was tested. There is nothing to migrate and no new setting to make. The self-hosted bundle download now pins License Server 1.33.1 with console 1.20.0.

Console 1.20.0 ​

Released on 6 November 2026: Settings shows your organization name. No License Server change is needed. There are no setting changes.

  • Settings → Account: the section starts with your organization name, set when you signed up, and says how to change it: email support. It can't be changed in the console. Using the Console lists the Account section.

v1.33.0 — 6 November 2026 ​

Adds one database column, created at the first start. Self-hosted servers return the new field as null. Hosted accounts already run 1.33.0 and need do nothing.

Added ​

  • Hosted deployments: a license's admin record and the license list show when Casazium revoked it (revoked_by_operator_at; see the API reference), otherwise null. Reactivating the license clears it.

If you run it yourself (MULTI_TENANT off): the new column is added once at the first start, and the new field is always null. There is nothing to migrate and no new setting to make. The self-hosted bundle download now pins License Server 1.33.0 with console 1.19.0.

Console 1.19.0 ​

Released on 6 November 2026: a license Casazium revoked says so. It needs License Server 1.33.0; with an older server nothing changes. There are no setting changes.

  • Licenses: a license Casazium revoked, for example because its key may have leaked, says Revoked by Casazium on its page and in the licenses list. Contact support for the details. You can still unrevoke it: Unrevoke asks you to confirm first, and unrevoking clears the label. Using the Console describes it.

v1.32.0 — 5 November 2026 ​

A change for hosted deployments only. Hosted accounts already run 1.32.0 and need do nothing.

Added ​

  • Hosted deployments: operator support views.

If you run it yourself (MULTI_TENANT off): you gain nothing from this release beyond a few new database indexes, built once at the first start. There is nothing to migrate and no new setting to make. The self-hosted bundle download now pins License Server 1.32.0 with console 1.18.0.

v1.31.0 — 5 November 2026 ​

Shows failed storefront webhook signatures in the webhook listing. There are no setting changes. Hosted accounts already run 1.31.0 and need do nothing.

Added ​

  • Storefront webhooks: the webhook listing shows requests whose signature didn't verify: about how many in the last 24 hours, and when the latest arrived and why (signature_failures_24h, last_signature_failure_at, last_signature_failure_reason; see the API reference). A storefront using an old signing secret shows up here. Anyone who knows a webhook's URL can cause failures too, so if your purchases are arriving you can ignore them. Nothing from the failed requests is kept.
  • Hosted deployments: the server records, to the hour, when each license and each activation last called it (validating, verifying, checking for updates, reporting usage or activating), for support. Self-hosted servers record nothing new.

If you run it yourself (MULTI_TENANT off): upgrade to see the signature failures; nothing to migrate and no new setting to make. The self-hosted bundle download now pins License Server 1.31.0 with console 1.18.0. From Console 1.18.0 the console shows them (below); with Console 1.17.2, read them through the API.

Console 1.18.0 ​

Released on 5 November 2026: storefront webhooks receiving requests with a bad signature say so, with what to do. It needs License Server 1.31.0; with an older server nothing changes. There are no setting changes.

  • Settings → Storefront webhooks: a webhook receiving requests with a bad signature says so: about how many in the last 24 hours, when the last one was, why it failed in plain words, and what to do. For a signature that doesn't match, that's deleting an old endpoint still sending to it, or Save signing secret again, right there. The collapsed webhook row shows a Bad signatures badge while there are recent ones. Anyone who knows a webhook's URL can cause these, so if your purchases are arriving you can ignore them. Using the Console describes the panel.

v1.30.0 — 30 October 2026 ​

A change for hosted deployments only: self-hosted servers gain nothing from this release and can skip it. Hosted accounts already run 1.30.0 and need do nothing unless Casazium disables one of your webhooks or takes down one of your builds (see below).

Added ​

  • Hosted deployments: Casazium can take down a release and disable a storefront webhook in an incident. Registering a build Casazium took down is refused until support lifts it, and the webhook listing shows when Casazium disabled a webhook (disabled_by_operator_at). The API reference says what to do: registering a release and disabling a webhook. Console 1.17.0 shows such a webhook as "Disabled by Casazium".

If you run it yourself (MULTI_TENANT off): nothing changes, there is nothing to migrate and no new setting to make. The self-hosted bundle download now pins License Server 1.30.0 with console 1.17.2.

Console 1.17.2 ​

Released on 5 November 2026: clearer first-license screens and license page. There are no setting changes, and License Server needs no change.

  • Issue license: Product ID, Tier, Issued to and Max activations each have a short hint, and the date and time fields line up.
  • After your first license: the welcome line congratulates you instead of still asking you to issue one.
  • License issued: the key is labeled "License key" and shown in a copy field like the Product UUID, so the two are easier to tell apart.
  • License page: next to the product, a link to the Products page, where its display name is set. Seats: a full license is no longer shown in red. A full multi-seat license shows in yellow, and red now means more machines than seats, so new activations are refused. The activation example links to the SDK docs.

Console 1.17.1 ​

Released on 31 October 2026: a fix for hosted accounts. There are no setting changes, and License Server needs no change.

  • Products page: since Console 1.15.0 it showed "Something went wrong" for every hosted account. It loads again. Self-hosted consoles were not affected.

v1.29.0 — 29 October 2026 ​

Recommended for every server: the security fix below applies to self-hosted servers too. Hosted accounts already run 1.29.0 and need do nothing.

Added ​

  • Hosted deployments: audited operator actions for an account's email and settings, and for suspending or restoring an account; every administrative change made on the server itself is recorded too.

Security ​

  • A storefront purchase's license email is sent only to a single plain email address. A buyer email that is a list, carries a display name or a control character, or has no dot in its domain still gets its license issued, but no email is sent, and a resend says why.

If you run it yourself (MULTI_TENANT off): upgrade for the security fix. The operator actions and their audit are hosted-only, so nothing else changes, there is nothing to migrate and no new setting to make. The self-hosted bundle download now pins License Server 1.29.0, still with console 1.16.1.

v1.28.0 — 29 October 2026 ​

A change for hosted deployments. Hosted accounts already run 1.28.0 and need do nothing.

Changed ​

  • Hosted deployments: operator support views tell test purchases from live ones, and operator actions always record why they were taken.

If you run it yourself (MULTI_TENANT off): nothing changes. Your server never starts the operator listener, there is nothing to migrate and no new setting to make. The self-hosted bundle download now pins License Server 1.28.0, still with console 1.16.1.

v1.27.0 — 29 October 2026 ​

An addition for hosted deployments. Hosted accounts already run 1.27.0 and need do nothing.

Added ​

  • Hosted deployments: more read-only, masked and audited views on the operator listener for supporting an account, and a tamper-evident record of operator actions.

If you run it yourself (MULTI_TENANT off): nothing changes. Your server never starts the operator listener, there is nothing to migrate and no new setting to make. The self-hosted bundle download now pins License Server 1.27.0, still with console 1.16.1.

v1.26.0 — 29 October 2026 ​

An addition for hosted deployments. Hosted accounts already run 1.26.0 and need do nothing.

Added ​

  • Hosted deployments: the operator listener can list one account's licenses, masked and audited.

If you run it yourself (MULTI_TENANT off): nothing changes. Your server never starts the operator listener, there is nothing to migrate and no new setting to make. The self-hosted bundle download now pins License Server 1.26.0, still with console 1.16.1.

v1.25.0 — 28 October 2026 ​

An addition for hosted deployments. Hosted accounts already run 1.25.0 and need do nothing.

Added ​

  • Hosted deployments: operator support actions on the operator listener added in 1.24.0, each one audited. They are off unless the operator listener is set up.

Changed ​

  • Changing a license's terms and resending a storefront purchase's license email now share their checks with those support actions. Nothing changes in how either behaves.

If you run it yourself (MULTI_TENANT off): nothing changes. Your server never starts the operator listener, there is nothing to migrate and no new setting to make. The self-hosted bundle download now pins License Server 1.25.0, still with console 1.16.1.

v1.24.0 — 28 October 2026 ​

An addition for hosted deployments, released with console 1.16.1. Hosted accounts already run 1.24.0 and console 1.16.1, and need do nothing.

Added ​

  • Hosted deployments: an optional, read-only operator listener for a separate operator admin app. It is off unless OPERATOR_API_KEY_SHA256 is set (the server stores only the key's hash); it listens on loopback only (OPERATOR_LISTEN, default 127.0.0.1:3101), masks license keys and buyer emails, and records searches and email reveals in a new audit table. Self-hosted servers (MULTI_TENANT off) never start it.

If you run it yourself (MULTI_TENANT off): nothing changes. There is nothing to migrate and no new setting to make; OPERATOR_API_KEY_SHA256 is ignored, though a value you do set is still checked at startup. The self-hosted bundle download now pins License Server 1.24.0, with console 1.16.1, so a bundle install gets the console's security update below.

Console 1.16.1 ​

Released on 28 October 2026: a security update for every console, hosted and self-hosted. There are no setting changes, and License Server needs no change.

  • Next.js 16.4.3 (from 16.2.12). It fixes the Next.js security advisories published since 16.2.12. Of those, the Server Actions denial of service (GHSA-p3m5-59mp-jv5r) applies to the console: crafted requests could make it unresponsive. The others need features the console doesn't use, or don't apply to a production Linux server. The image library sharp moves to 0.35.5 or later.
  • Hosted consoles: the startup warning now says to clear LICENSE_ADMIN_API_KEY, rather than delete it: an empty value counts as unset.

v1.23.0 — 28 October 2026 ​

A security change for servers run with MULTI_TENANT=true. Hosted accounts already run 1.23.0 and need do nothing.

Security ​

  • With MULTI_TENANT=true, ADMIN_API_KEY manages tenants only through a Unix socket inside the server's container. 1.22.0 accepted it from a loopback connection inside the container, which a network sidecar sharing the container's network could imitate (no hosted server ran one). Now no connection over the network qualifies, not even from 127.0.0.1. The socket is /tmp/casazium-license-admin/admin.sock, by default in a directory only the server's user can open; the startup log names it, and the optional ADMIN_SOCKET_PATH moves it. Nothing else changes: PROVISIONING_API_KEY and the routes that still take ADMIN_API_KEY from outside work as in 1.22.0.

If you run it yourself (MULTI_TENANT off): nothing changes, and there is no new setting to make. The self-hosted bundle download now pins License Server 1.23.0, with console 1.15.0.

v1.22.0 — 28 October 2026 ​

A security change for servers run with MULTI_TENANT=true, released with console 1.16.0. Hosted accounts already run 1.22.0 and console 1.16.0, and need do nothing.

Security ​

  • A hosted console no longer holds the server's admin key. The new optional PROVISIONING_API_KEY covers the only two things a hosted console asks of the server outside a tenant's own key: creating a tenant at signup, and recording a login email the console has verified (which also needs that tenant's own key). It opens nothing else.
  • With MULTI_TENANT=true, ADMIN_API_KEY manages tenants only from inside the server's own container. From anywhere else, those requests get a 403. ADMIN_API_KEY still works from outside on GET /admin/build-info, POST /admin/crash-test, GET /admin/tier-a-status and GET /self-license/status. The server refuses to start if PROVISIONING_API_KEY is weak or equals ADMIN_API_KEY.

If you run it yourself (MULTI_TENANT off): there is nothing to migrate and no new setting; PROVISIONING_API_KEY is ignored, though a value you do set is still checked at startup, as above. One small change: a tenant-management request without a valid ADMIN_API_KEY is now refused with a 403 before its body is checked, where a malformed body used to get a 400 first. The self-hosted bundle download now pins License Server 1.22.0, with console 1.15.0.

Console 1.16.0 ​

Released on 28 October 2026, for hosted consoles only: a self-hosted console is unchanged, which is why the bundle keeps console 1.15.0.

  • LICENSE_PROVISIONING_API_KEY (or LICENSE_PROVISIONING_API_KEY_FILE) holds the server's PROVISIONING_API_KEY. With MULTI_TENANT=true it is the only key the console sends outside a tenant's own, and it replaces LICENSE_ADMIN_API_KEY, which a hosted console should no longer hold: the console warns at startup while it is set.
  • The two keys follow the server. Before License Server 1.22.0, a hosted console keeps working with only LICENSE_ADMIN_API_KEY, and can't sign anyone up with the provisioning key. From 1.22.0 the server refuses the admin key from outside, so set the provisioning key when you upgrade the server, deploy the server and then the console, and then clear LICENSE_ADMIN_API_KEY.

v1.21.2 — 27 October 2026 ​

Two changes for console 1.15.0, released the same day. Hosted accounts already run 1.21.2 and console 1.15.0.

Added ​

  • subscription_refund.first_invoice on the deliveries list and the one-purchase lookup: true when the refund was of the subscription's first payment. On Lemon Squeezy and Paddle the purchase's own refund fields describe the same refund; on Stripe they stay empty, so this is the only record of it. The console uses it to show a first-payment refund once, and to tell it from a renewal refund.

Fixed ​

  • No false needs_event after moving a Stripe endpoint. Replacing an endpoint that couldn't match refunds with one at 2025-05-28.basil or later cleared needs_api_upgrade, but the last subscription payment recorded under the old version has no refund link, so refund_matching then read needs_event until the next payment. Moving up to 2025-05-28.basil or later now forgets that payment.

If you run it yourself: there is nothing to migrate, and no new settings. The self-hosted bundle download now pins License Server 1.21.2 and console 1.15.0.

Console 1.15.0 ​

Released on 27 October 2026. Its new screens show what License Server 1.19.0 and 1.20.0 added. Each needs the server version given, and works best with 1.21.2.

  • Licenses from sandbox purchases. A license from your storefront's test mode shows a sandbox badge in Licenses, and its page says Purchase: sandbox (test purchase) or live. Licenses has a Mode filter: Live, Sandbox or Not recorded. A sandbox license whose product refuses them shows refused (sandbox) instead of active, as your app is told. License Server 1.20.0 or later. See Finding a customer or license.
  • Accept licenses from sandbox purchases, a switch in each product's Edit, turns the sandbox switch off and on. Before you save it off, it says about how many of that product's licenses it affects. A product that refuses them shows sandbox refused. License Server 1.20.0 or later. See Products.
  • Paid through on a license's page, when the server records it (License Server 1.20.0 or later). Read-only.
  • Stripe refund matching (hosted accounts: the self-hosted console has no Settings page). A Stripe webhook whose refunds of subscription payments can't be matched shows Refunds not matched, with the steps to fix it. For an endpoint on an API version that can't link refunds, Replace signing secret saves the new endpoint's secret: see Moving a Stripe endpoint to a newer API version. License Server 1.19.0 or later; 1.19.x to 1.21.1 can show a warning that clears on its own.
  • Refunds of subscription payments on purchases (hosted accounts). A purchase shows the latest refund on its subscription (License Server 1.19.0 or later). With 1.21.2 it says whether it was the first payment or a renewal (with its invoice), and a first-payment refund already shown on the purchase isn't repeated. A refund that shortened the license reads Refunded · shortened, and one that found no license Refunded · no license; both read plain Refunded before.
  • Products says where prices come from. On a hosted account, the page says prices and descriptions live in your storefront and links to Settings → Storefront webhooks.
  • A warning when Stripe sales would issue no license (hosted accounts). A Stripe webhook whose mappings are all prices or products, with no Payment Link or casazium_ref mapping, says its sales issue no license: a Stripe checkout carries no prices.

v1.21.1 — 27 October 2026 ​

Two small fixes to 1.21.0. Hosted accounts already run 1.21.1. Console stays 1.14.2.

Fixed ​

  • Browser scripts can read the .lic file's name. The Content-Disposition header (license-<product_id>.lic) from POST /v1/license-export/file is now readable by a page on another origin, like the Deprecation and Link headers. A page that fetches the file can save it under that name instead of building the name itself.
  • More mistyped URLs are kept out of the server's logs. A license key or portal token in a URL that uses %2F, ;, %3B, \ or %5C where a / belongs, as in /v1/license-export;<key> or /v1%2Fexport-license/<key>, isn't served, but the server logged the request with the key in it. It is now [redacted], like the URLs covered since 1.19.2 and 1.21.0. Your proxy's access log still records whatever URL a client sends: send the key in the body only.

If you run it yourself: there is nothing to migrate, and no new settings. The self-hosted bundle download now pins License Server 1.21.1, with console 1.14.2 as before.

SDK 0.5.1 ​

Published to npm the same day, with no code changes: comments, CLI help and the README no longer call a license key "not a secret". SDK 0.5.0, also published that day, is described under v1.21.0.

v1.21.0 — 27 October 2026 ​

License keys can leave the export URLs. A key in a URL is recorded by every proxy, load balancer or CDN that logs requests, and by browser history. The three export routes now take it in the request body. The old forms keep working; the only change your code may notice is the .lic file name (below). The old forms also send deprecation headers. Hosted accounts already run 1.21.0. Console stays 1.14.2.

What to do:

  • If you call the export routes directly (curl, your own code), send the key in the body now.
  • If you use the SDK, SDK 0.4.x and earlier call the old forms and keep working until 2.0.0. After that only exportOfflineLicense() keeps working; exportLicense() stops. SDK 0.5.0, published the same day, uses the new forms and needs License Server 1.21.0 or later. If you run your own server, upgrade it first, then change the dependency to ^0.5.0.
  • If a script looks for the downloaded file by name, it is now license-<product_id>.lic.

Added ​

  • POST /v1/license-export, POST /v1/license-export/offline and POST /v1/license-export/file, each taking {"key": "..."}. They answer exactly as GET /v1/export-license/:key, /:key/offline and /:key/file do: the same body, status, error messages and refusals, and one shared rate limit. Browsers can call them: cross-origin requests are allowed, and the browser caches the preflight check for an hour. See Export a license: key in the body.

Deprecated ​

  • GET /v1/export-license/:key, /:key/file and /:key/offline. They still work, and every answer carries Deprecation: @1793059200 (RFC 9745; the date they were deprecated, not a removal date) and a Link header with rel="deprecation" pointing to the API reference. Both headers are readable by browser scripts. They'll be removed in 2.0.0, except GET /v1/export-license/:key/offline, which stays longer for apps already installed.

Changed ​

  • A downloaded .lic file is named after the product,license-<product_id>.lic, no longer after the license key, from the export route and the buyer portal alike. A script that matched the old name needs the new one.
  • A key in the URL by mistake: if a client puts the key in the path of the new routes (/v1/license-export/<key>, which answers 404), the server keeps it out of its own logs, as it has the old routes' keys since 1.19.2. Your proxy's access log still records it. Send the key in the body only.

The console (1.14.2) still uses the GET forms, and is unaffected.

If you run it yourself: there is nothing to migrate, and no new settings. Your reverse proxy's access log still records the key for apps on the GET forms, including copies of your app already installed: keep that log short-lived, or turn it off. The self-hosted bundle download now pins License Server 1.21.0, with console 1.14.2 as before.

v1.20.0 — 26 October 2026 ​

Signed dates in the offline license file, and sandbox licenses you can turn off once you go live. Nothing to migrate: every change adds a field, and no existing error message changes. It applies to hosted accounts too. Console stays 1.14.2.

If you verify the offline license file without our SDK, read "Verify the license object exactly as received" below first: a verifier that rebuilds the object fails on the new files.

Added ​

  • The offline license file signs more.GET /v1/export-license/:key/offline now signs issued_at, exported_at (the server's clock when it signed the file) and, when known, livemode and paid_through, all after limits. Your app can trust them with no server call: for example, count offline time from the signed exported_at rather than a time your app saved, or reject livemode: false. Files from older servers verify as before, and older SDKs verify the new files. See What the file signs.
  • Verify the license object exactly as received. Fields may be added after limits, so verify JSON.stringify of the parsed object, in the order received. Don't rebuild it from the fields you know, sort its keys, or round-trip it through a typed struct. Treat every field after limits as optional.
  • livemode on licenses: true for a license from a live storefront purchase, false for a sandbox (test-mode) one, null when unknown (issued by hand or through the API, or an older license whose purchase records are gone). In verify-license, activate-license, GET /v1/admin/license/:key and GET /v1/list-licenses, which gains a livemode filter (true, false or unknown).
  • Refuse sandbox licenses once you go live:PATCH /v1/admin/products/:id with "test_licenses": "refuse". That product's sandbox licenses then get the answer a revoked license gets, wherever a buyer uses one (for example License is not active from verify-license, with "reason": "test_license" where the answer has room for it). Licenses with no recorded mode are never affected, and "allow" restores them. Sandbox licenses keep working after you go live unless you do this. Offline files exported earlier keep verifying offline; your app can reject livemode: false in the file itself. Most licenses issued before 1.20.0 have no mode, so the switch doesn't reach test purchases made before 1.20.0 reached your server or account: revoke those by hand. ?livemode=unknown lists them, along with licenses issued by hand. See Sandbox licenses after you go live.
  • paid_through on licenses: the end of the latest period paid for with money. For a storefront subscription, its latest paid invoice that isn't fully refunded (a trial's or a 100% coupon's $0 invoice earns none); for a paid one-time purchase with a term, its expiry. A refund takes it back, whatever the refund policy. You can set it with POST /v1/admin/update-license-terms. In verify-license, the admin license routes and the offline file. Existing licenses don't have it yet: a subscription's license gets it at its next payment, and an existing one-time license only if you set it.

The console (1.14.2) doesn't show livemode, paid_through or the sandbox switch yet: use the API.

If you run it yourself: there is nothing to migrate, and no new settings. The self-hosted bundle download now pins License Server 1.20.0, with console 1.14.2 as before.

v1.19.2 — 26 October 2026 ​

License keys are kept out of the server's logs. Console stays 1.14.2.

Fixed ​

  • License keys are no longer written to the server's logs. The request log recorded the full URL, so routes with a key in the path (GET /v1/export-license/:key and its /file and /offline forms, /v1/list-activations/:key and /v1/admin/license/:key) logged the key, and GET /v1/list-licenses logged its key and issued_to searches. Those are now [redacted]. Log lines that named a license (seat reclaims, deactivations, token reissues, terms updates, rate limits) now carry keyRef, the first 12 hex characters of the key's SHA-256, instead of the key. Client IP addresses are still logged, as before.

If you run it yourself: there is nothing to migrate, and no new settings. To find a license in the logs, compute its keyRef, for example printf %s "$KEY" | shasum -a 256 | cut -c1-12 (or sha256sum).

v1.19.1 — 25 October 2026 ​

Fixes found testing 1.19.0's refunds of subscription payments. Console stays 1.14.2.

Fixed ​

  • refund_matching shows needs_event within minutes, and only for its own webhook. In GET /v1/admin/storefront-webhooks, a Stripe webhook now reads needs_event once its latest paid subscription invoice (charged automatically, more than $0; a free trial, a 100% coupon, or an invoice Stripe emails to the customer to pay instead doesn't count) is 10 minutes old and that payment's invoice_payment.paid hasn't arrived. 1.19.0 waited a day, and looked at every Stripe webhook you'd connected, test and live, so a test-mode subscription could flag a live webhook.

    After the upgrade, every webhook shows ok until its next paid subscription invoice. If you saw needs_event under 1.19.0, or sell subscriptions, check invoice_payment.paid is on the storefront endpoint now.

    To fix needs_event, add invoice_payment.paid to the storefront endpoint in Stripe, the one whose URL contains /v1/webhooks/storefront/. Then resend that payment's invoice_payment.paid with the Stripe CLI, stripe events resend <evt_id> --webhook-endpoint=<we_id> (add --live for a live-mode payment), so a recent refund that was waiting applies. If the refund was made longer ago, it's no longer waiting: change that license by hand. Where to find the IDs is in Refunds of subscription payments.

  • Self-hosted with BILLING_PROVIDER=stripe: the Stripe billing webhook now acknowledges checkouts that aren't plan checkouts, such as a storefront Payment Link sold through the same Stripe account. It used to answer 400, so Stripe retried each one for days and could disable the endpoint.

The console (1.14.2) still doesn't show refund_matching or refunds of subscription payments: check them through the API.

If you run it yourself: there is nothing to migrate, and no new settings. The self-hosted bundle download now pins License Server 1.19.1, with console 1.14.2 as before.

v1.19.0 — 25 October 2026 ​

Refunds of subscription payments ​

This changes behavior. A refund of a subscription payment can now end or shorten a license that used to keep running, on Stripe, Lemon Squeezy and Paddle. It applies to hosted accounts too. To keep subscription refunds manual, set the webhook to record refunds only: in the console, When a purchase is fully refunded → Keep it (record only), or PATCH /v1/admin/storefront-webhooks/:id with { "refund_policy": "record" }.

Under the default, revoke, a full refund takes back what its payment paid for:

  • The subscription's only paid payment that hasn't been refunded, usually the first: the license is revoked. A free trial's or a 100% coupon's $0 invoice doesn't count as paid.
  • A renewal, the latest payment, with paid ones before it that haven't been refunded: the license rolls back to the end of what is still paid for, with no grace. If that date has passed, the license is expired, not revoked, and a later renewal extends it again. A license you set to never expire by hand isn't changed.
  • An older payment, while a later payment hasn't been refunded: recorded only. On Lemon Squeezy and Paddle, that now includes the first order after renewals, which used to revoke the license outright.

Before 1.19.0, Stripe matched no refund of a subscription payment, and no storefront acted on a refund of a renewal.

  • Subscriptions that started before 1.19.0 are never revoked by a refund, not even a refund of their first order, which 1.18.1 revoked on Lemon Squeezy and Paddle. Revoke those by hand if you need to. A renewal refund rolls them back, never below the paid time they had before you upgraded to 1.19.0.
  • Partial refunds are still recorded only, but several partial refunds of one payment that add up to it count as a full refund.
  • A refund that arrives before its payment's event waits for it for a limited time, and is applied if that event arrives.
  • A refunded upgrade rolls the expiry back, but keeps the plan and seats until the subscription changes plan.
  • Offline license files exported before a refund stay valid to the expiry they were signed with. Have your app fetch a fresh file when an online check says the license has expired.

See Refunds of subscription payments for every rule.

New events to subscribe

  • Stripe: on an endpoint at API version 2025-05-28.basil or later, add invoice_payment.paid, and keep charge.refunded. An endpoint from 2025-03-31.basil to before 2025-05-28.basil can't match refunds of subscription payments, and Stripe can't change an endpoint's API version: replace it with a new endpoint, as in Moving a Stripe endpoint to a newer API version. An endpoint older than 2025-03-31.basil needs no new event.
  • Lemon Squeezy: add subscription_payment_refunded.
  • Paddle: nothing to add. The adjustment events you already have carry these refunds.

API changes ​

  • GET /v1/admin/storefront-webhooks includes api_version and refund_matching for a Stripe webhook (null for the others). refund_matching says whether refunds of subscription payments can be matched: ok (no problem found), needs_api_upgrade (replace the endpoint), needs_event (add invoice_payment.paid), or null until an event arrives. In 1.19.0, a missing invoice_payment.paid shows only a day after the first subscription invoice; from 1.19.1 it shows within minutes (see v1.19.1).
  • Each deliveries row, and the one-purchase lookup, includes subscription_refund: the latest refund on any of the subscription's payments, or null.
  • refund_action can be rolled_back.

The console (1.14.2) doesn't show refunds of subscription payments yet. On Stripe it shows none of them, even one that revoked the license; on Lemon Squeezy and Paddle it shows a refund of the first order only. Check subscription_refund through the API.

Fixed ​

  • Single-executable (SEA) builds: a Paddle subscription purchase now issues its license. Since 1.11.0 it failed with an error. The Docker image and hosted accounts weren't affected.

If you run it yourself: there is nothing to migrate, and no new settings; the new tables and columns are added automatically at startup. Subscribe the new events above. The self-hosted bundle download now pins License Server 1.19.0, with console 1.14.2 as before.

v1.18.1 — 22 October 2026 ​

Changing plans on a hosted account ​

Changing plans no longer starts a second subscription. On a paid plan, picking another plan, or switching between monthly and annual billing, opened a new checkout, so the account was billed for both subscriptions. It now opens Stripe's billing portal on your existing subscription, which shows the prorated price and changes that subscription when you confirm. Your first subscription, from Free, still goes through checkout. If you were billed for two subscriptions, contact us.

  • GET /v1/billing/status includes interval, your paid plan's billing period: monthly, annual or null.
  • POST /v1/billing/checkout returns a billing portal URL for any change to an existing paid subscription, as it already did for plan: "free".

Console 1.14.1 ​

  • With a paid plan, Billing says what each choice does. Other plans read Switch and Free reads Downgrade, and the page says the change is made on your current subscription. A past-due subscription, or one already set to cancel, reads Manage. See Managing your subscription.
  • Monthly and annual are separate choices. Billing starts on your own billing period, and your plan at the other period reads Switch to annual or Switch to monthly.
  • The "Subscription canceled" notice names your plan. It said "Pro" to Business accounts too.

If you run it yourself: there is nothing to do for 1.18.1. Billing applies to hosted accounts only; the one database change, a new column, is added automatically at startup. The self-hosted bundle download now pins License Server 1.18.1 and console 1.14.2; update the console for the perpetual-license checkbox below.

Console 1.14.2 ​

Released on 23 October 2026, with no License Server change.

  • Issue a perpetual license. Issue license, and Issue your first license on a new account, offer Perpetual (never expires), as Edit terms does. Ticked, it hides the expiry date, time and time zone, and the license is issued with no expiry. Unticked, the expiry still defaults to a year from today. See Issuing a license.

SDK 0.4.2 ​

Published to npm on 22 October 2026, with no code changes: the npm page links to these docs and to the contact page.

v1.18.0 — 21 October 2026 ​

Groundwork for an upcoming feature, switched off. There is no change in behavior: licenses, license emails and the buyer portal work exactly as in 1.17.0.

API changes ​

  • GET / includes features,{ "vendor_voice": false } by default, so License Console 1.14.0 shows only what the server supports.
  • Product responses include buyer_instructions, which is null for now (GET /v1/admin/products and the other products routes). A PATCH /v1/admin/products/:id with text in it is refused with 400.

Console 1.14.0 ​

  • The Products page's warning for a product whose ID buyers can't be shown now says what buyers actually see: "Buyers see no product name ('Your license', 'the app') - set a display name". It said "your purchase".
  • Find a purchase names each storefront's ID in its placeholder and help: Lemon Squeezy and Paddle IDs need their full prefix (ls_order_…, paddle_txn_…), as Deliveries lists them.
  • Hosted accounts: the console reports your verified login email to License Server when you verify it or confirm a new one. A failure never fails the verification.

If you run it yourself: there is nothing to migrate. 1.18.0 has two new settings, VENDOR_VOICE_ENABLED and EMAIL_VIA_NAME; leave both unset. The self-hosted bundle download now pins License Server 1.18.0 and console 1.14.0.

v1.17.0 — 21 October 2026 ​

Your buyers now see your product's own name in their license email and license portal. You can turn off the portal's license file for an app that only takes a key, and you can have Casazium issue a storefront's licenses without emailing them, so you send each key yourself.

Your product's name in the email and the portal ​

Console 1.13.0 has a new Products page, on hosted accounts and in the self-hosted bundle. It lists each product with its display name, its product ID, and whether the portal offers the license file. A product appears there once a license or release names it; Add product sets one up before its first sale. Edit sets the Display name (up to 80 characters), with a preview of the email subject.

  • The display name is what buyers see: the license email's subject (Your Acme Pro license) and text, and the license portal's title and heading. Left empty, the product ID is used instead.
  • It has to pass a few rules, because it appears in an email sent from Casazium's domain: letters, numbers, spaces and . , & ' - ( ) + : / ! # ® ™; no mixed alphabets or look-alike characters; no invisible characters; and no web or email address. A dotted word ending in a real top-level domain counts as a web address, so acme.shop and Foo.app are refused while Node.js passes. If your product's real name ends in a real domain, ask support to approve it. The console shows the reason when a name is refused.
  • Through the API, it's PATCH /v1/admin/products/:id.

Check your product IDs

A product ID is now shown to buyers only if it passes the same rules. One that doesn't is left out: the email's subject reads just "Your license", and the portal's title "Your license". That catches product IDs such as com.acme.pro (.pro is a real top-level domain), and any with _ or @ in them. The Products page marks each such product ("Buyers see 'your purchase' - set a display name"; that's the console's wording, and the email reads "Your license" and "the app"). Give it a display name to show buyers your product's name again.

The portal no longer says "Casazium License Portal". Its title is "your product license" (or "Your license"), and its heading is the product's name. The Product row shows the same name, or is left out. On a hosted account, the page ends with "License portal provided by Casazium."

Turn off the license file ​

Each product on the Products page has a switch, Offer the license file in the buyer portal. Turn it off if your app only takes the license key: the portal then hides its Download license file button, and its download link answers "This product doesn't use a license file. Enter your license key in the app instead." Your own exports through the API (/v1/export-license/:key/file and /offline) work as before. Through the API, it's offline_file on PATCH /v1/admin/products/:id.

Deliver the key yourself ​

If your storefront or your own app already emails your customers, you can have Casazium issue the licenses and leave sending them to you. On a storefront webhook in Settings → Storefront webhooks, Sending the buyer their license now offers I deliver it myself besides Casazium emails the buyer, which stays the default.

  • Licenses are still issued on every purchase, as before; the buyer just isn't emailed. Paddle purchases still fetch the buyer's email from Paddle, so you have it.
  • Choosing it asks first, and says how many of the purchases loaded under Deliveries are still waiting to be emailed. Those are left to you too: switching moves every purchase of that webhook not yet emailed (still processing, awaiting payment, or issued but not sent), except a fully refunded one. Switching back saves at once and moves nothing back.
  • In Deliveries, such a purchase is badged Issued · you deliver and never counts as needing attention. The webhook's collapsed row shows You deliver.
  • To find a key, use Find a purchase by its checkout or order ID above Deliveries. It shows the purchase's outcome, its license key (with Copy) and the buyer's email. Enter the ID as Deliveries shows it: the Stripe Checkout Session ID, ls_order_ followed by the Lemon Squeezy order's identifier, or paddle_txn_ followed by the Paddle transaction ID.
  • Resend email still works on any of these purchases, for a buyer who says your own email never arrived: Casazium emails that buyer anyway, and the purchase becomes Issued & sent.
  • Send pending deliveries isn't offered on a Paddle webhook set this way, since nothing waits to be emailed; use Resend email for one buyer.

Through the API, set "buyer_delivery": "vendor" with PATCH /v1/admin/storefront-webhooks/:id, and look a purchase up with GET /v1/admin/storefront-webhooks/:id/deliveries/:checkout_session_id. Such purchases have the new delivery_status vendor. The storefront page walks through it, including how to give the buyer a portal link.

Held license emails ​

The daily limit of 200 license emails per account now counts every license email sent: first sends, Send pending deliveries and resends alike. The first start of 1.17.0 counts the emails already sent that day, so the day's count carries over.

When a license email is held back, Deliveries now says why, under the purchase's badges, and the purchase counts in Needs attention:

  • Held: daily limit - resend tomorrow: the account had sent 200 license emails in the last 24 hours.
  • Held: buyer emails paused: Casazium has paused this account's buyer emails. The licenses are still issued. Contact support.

Stripe and Lemon Squeezy don't redeliver a purchase once License Server has answered it, so a held purchase there needs Resend email once the limit allows. On Paddle, Send pending deliveries sends held purchases too. While buyer emails are paused, a resend is refused ("Buyer emails are paused for this account - contact support"). Through the API, a deliveries row's new delivery_held field is daily_cap or suspended, and is cleared once the email goes out.

Hosted accounts: a new free account whose license emails draw spam complaints or bounce too often has its buyer emails paused automatically; for other accounts, Casazium looks into it first.

Console 1.13.0 ​

  • Products, the license file switch, I deliver it myself, Find a purchase and the held-email notes, as above. They need License Server 1.17.0; on an older server, the Products page, the setting and the lookup don't appear.
  • Send pending deliveries that stops because the account's buyer emails are paused says so: "Buyer emails are paused for your account
    • contact support."

If you run it yourself:

  • Check EMAIL_FROM before upgrading. It must be a bare address (licenses@example.com) or a name and an address (Example Licenses <licenses@example.com>, the name optionally in double quotes). Anything else, such as a list of addresses or a stray character, now stops the server from starting, with a message naming EMAIL_FROM. Before 1.17.0 it went to the email service unchecked.
  • Upgrade the console with the server. Don't set a webhook to deliver it yourself with Console 1.12 or older: it shows every such purchase as needing attention. The self-hosted bundle download now pins License Server 1.17.0 and console 1.13.0, which go together.
  • Two new settings, both optional on a single-tenant server.RESEND_WEBHOOK_SECRET records Resend's bounces and complaints, and logs an alert when they pass a limit, and RECIPIENT_HASH_KEY keys the hashes of buyer addresses that the server keeps for counting bounces and complaints; without it, the server uses a random key held in memory, so a restart only resets those counts. With MULTI_TENANT=true, RECIPIENT_HASH_KEY is required, and the server won't start without it. Either way, a RESEND_WEBHOOK_SECRET that isn't a whsec_ secret, or a RECIPIENT_HASH_KEY under 32 characters, stops the server from starting. See Self-hosted: the email needs configuring.
  • There is nothing to migrate: the new columns and tables are added when the server starts. Existing products keep offering the license file, and existing webhooks keep emailing buyers.

v1.16.0 — 21 October 2026 ​

API change: times are now ISO 8601 ​

If your code reads these times itself, check it

This applies to hosted accounts as well as servers you run yourself. Code that converts the old format by hand, such as value.replace(' ', 'T') + 'Z', turns the new format into an invalid date.

API responses now give the times the server records itself as ISO 8601 in UTC, ending in Z. They used to be SQLite's YYYY-MM-DD HH:MM:SS: UTC, but with no zone, so JavaScript's new Date() read them as local time.

Before 1.16.0From 1.16.0
"2026-10-20 16:51:02""2026-10-20T16:51:02.000Z"

The fields that change:

  • issued_at, in list-licenses, admin/license/:key and verify-license;
  • revoked_at, in list-licenses and admin/license/:key;
  • activated_at, in list-activations, recent-activations, admin/stats, and the activations that list-licenses can include;
  • a storefront webhook's created_at (when connected and in the list) and last_event_at, a mapping's created_at, and a delivery's processed_at and refunded_at;
  • tenant_created_at, last_license_issued_at and last_activation_at in admin/report-extract.

A time that isn't set is still null. Times that were already ISO 8601 (expires_at, a release's times, an activation's lease_expires_at and last_seen_at, a subscription's dates) are unchanged, and so is signed content: license files, offline exports and release manifests are verified byte for byte, and were already ISO 8601.

  • To read both formats, use the parseTime recipe in the API reference's Times paragraph. It adds the Z only when a value has no zone, so the same code works against a server either side of 1.16.0.
  • The License Console reads both (since Console 1.6.1), so nothing changes there.

This format change shipped in a minor release, although How versions work promises that a breaking API change comes with a major version. It should have been a major version. Future breaking API changes will come with a major version, as promised.

Resend a license email ​

For a buyer who lost their license email or never got it: in the console's Deliveries, each issued purchase that wasn't fully refunded has a menu (⋯) with Resend email…. It asks first ("They'll get a new link to the license portal; the old link stops working."), then says who it was sent to. It works for Stripe, Lemon Squeezy and Paddle, whether the email was sent, is stuck or was only logged (with one Paddle exception, below). Through the API, it's POST /v1/admin/storefront-webhooks/:id/deliveries/:checkout_session_id/resend.

  • It goes to the buyer's email from the purchase, the license's issued_to, and can't be sent anywhere else.
  • It carries a new portal link. The link in the buyer's earlier email stops working once the new one has been sent.
  • Limits: 3 resends per purchase a day, at least 10 minutes apart, and each one sent counts toward the account's daily limit of 200 license emails. A resend that fails counts toward nothing.
  • Refused for a fully refunded purchase, a revoked or deleted license, and on a server without an email provider.
  • Paddle: a purchase whose buyer email hasn't been fetched from Paddle yet can't be resent; use Send pending deliveries, which fetches it. Once it, or Paddle's redelivery, has fetched the email, the license's issued_to holds it in place of the storefront: placeholder (never replacing a value you set), so the purchase can be resent later. A Paddle purchase whose buyer email couldn't be fetched when it was issued, and was then sent by Send pending deliveries or Paddle's redelivery before 1.16.0, still has the placeholder and can't be resent: send that buyer their key yourself.
  • A replaced webhook's purchases can be resent from its own Deliveries.
  • Redelivering from the storefront still works as before, and never issues a second license.

Every license email, a resend or not, now replaces the buyer's portal link only once it has been sent, so a failed send never breaks a link the buyer already has. (A send that timed out or lost its connection still replaces it, since that email may have arrived.)

Hosted accounts: purchases made before about 17:40 UTC on 20 October 2026 weren't emailed (v1.15.1) and are recorded as sent, so a redelivery from the storefront won't send them. Resend email will, except for a Paddle purchase whose buyer email couldn't be fetched when it was issued and was sent later, before 1.16.0: send that buyer their key yourself.

A Stripe Payment Link mapping now has to be the link's plink_ ID. Its https://buy.stripe.com/... address was accepted before, but a checkout carries the ID, so such a mapping never matched and every purchase through the link was recorded unmapped. Adding one is now refused (400) with where to find the ID: in the Stripe Dashboard, open the Payment Link and copy the part after /payment-links/ in the address bar (Finding a Stripe Payment Link's ID).

Existing mappings are left as they are. If a Payment Link mapping's reference doesn't start with plink_, delete it and add it again with the ID. Purchases already recorded unmapped aren't issued afterwards; issue those licenses yourself.

Console 1.12.0 ​

  • Resend email, as above. The Needs attention and Buyer not emailed notes in Deliveries point at it. It needs License Server 1.16.0; on an older server the menu doesn't appear and the notes read as before.
  • The Payment Link ID field is checked before saving a mapping, with a hint under it saying where the ID is. A buy.stripe.com address is refused there with any License Server version.

If you run it yourself: there is nothing to migrate; the table that records resends is created when the server starts. Check any code or script of your own that reads the times above. Resend email needs an email provider (EMAIL_PROVIDER=resend, see Self-hosted: the email needs configuring). The self-hosted bundle download now pins License Server 1.16.0 and console 1.12.0.

v1.15.2 — 20 October 2026 ​

A license email that's easier to act on ​

The email a storefront buyer gets with their key:

  • The key copies cleanly. It sits alone on its own line, in a monospace font. Some mail apps showed it wrapped in backticks, which buyers copied along with the key, so activation failed.
  • The portal link works in every mail app. Nothing follows it on its line; a full stop after it was pulled into the link by many mail apps.
  • It says what to do with the key: "Enter this key in your product when it asks for a license key." The portal link is introduced as "View and manage your license here:".
  • It has a plain-text part, for mail apps that don't show HTML.
  • "Questions about your purchase? Reply to this email." appears only on a single-tenant server with EMAIL_REPLY_TO set, so a reply reaches you. It never appears with MULTI_TENANT=true.

The license portal shows No limits instead of None for a license without limits, and a line under Download license file says the file is only needed if the app activates offline.

Console 1.11.1 ​

  • An API request sent to the console's address says where to send it. A request under /v1/ was redirected to the sign-in page. It now gets a JSON 404 that names the License Server API's public address, such as "Send API requests to https://license-cloud-api.casazium.com/v1/verify-license".
  • The Stripe webhook setup steps follow Stripe's current order: choose the events, then the endpoint URL. The events are the same.
  • The first-license form at sign-up offers Free inactive machines, as Issue license does.

If you run it yourself: there is nothing to migrate. To show the reply line, set EMAIL_REPLY_TO (Self-hosted: the email needs configuring). The self-hosted bundle download now pins License Server 1.15.2 and console 1.11.1.

v1.15.1 — 20 October 2026 ​

A license email that wasn't sent no longer shows as sent ​

Without an email provider (EMAIL_PROVIDER unset or stub), License Server issues a storefront purchase's license but only writes the buyer's email to its log. It recorded that purchase as sent, so the console said Issued & sent while the buyer never got their key.

  • It's now recorded as logged, which console 1.11.0 shows as Issued · email not sent, with a Buyer not emailed alert that says what to do. Through the API, delivery_status gains the value logged.
  • Once a provider is set, a redelivery emails the buyer the same key, and never issues a second license: resend the event from Stripe's dashboard (within about 30 days), resend the webhook from Lemon Squeezy within 7 days of the order, or click Send pending deliveries on a Paddle webhook. Past that, send the buyer their key yourself.
  • Logged purchases are kept until they're sent (unless fully refunded), so they can still be sent later.
  • Purchases from before 1.15.1 stay recorded as sent. To find any whose email was only logged, search the server's log for Storefront license email (EMAIL_PROVIDER=stub, not actually sent).
  • Send pending deliveries refuses (409) without a provider, instead of logging every email again.
  • Every start logs a warning while buyer emails are off: Buyer license emails are off (EMAIL_PROVIDER=stub).

Hosted accounts: purchases until 20 October weren't emailed ​

Casazium's hosted service ran without an email provider until about 17:40 UTC on 20 October 2026. A storefront purchase through it issued its license, but the buyer wasn't emailed, and Deliveries showed Issued & sent. Buyer emails have been on since then. If a buyer bought through your storefront webhook before about 17:40 UTC on 20 October, they didn't receive their key: send it to them yourself. The key is in the purchase's row in Deliveries; to find the buyer, look up the row's Checkout session in your storefront's dashboard: a Stripe Checkout Session ID as shown; for Lemon Squeezy, drop the ls_order_ prefix to get the order's identifier (not its order number); for Paddle, drop paddle_txn_ to get the transaction ID. The Processed time narrows it down too. A redelivery from the storefront won't send it, because those purchases are still recorded as sent.

The hosted service can no longer start without a choice of email provider: in multi-tenant mode (MULTI_TENANT=true), License Server refuses to start unless EMAIL_PROVIDER is set (resend, or stub on purpose). A single-tenant server (MULTI_TENANT unset or false) is unaffected; leaving EMAIL_PROVIDER unset still means the stub.

Console 1.11.0 ​

  • A new license no longer expires the day it's issued. The issue form, and the first-license form at sign-up, set the expiry to today at 23:59, so a license issued with the default expiry stopped working that night. It now defaults to a year from today. If you issued a license without changing its expiry, it has expired, or expires tonight: give it a new expiry with Edit terms. An offline license file downloaded before then keeps the old expiry, so download it again afterwards.
  • Edit terms offered the current time when you unticked Perpetual, so saving expired the license at once. It now offers a year from today.
  • Issued · email not sent and the Buyer not emailed alert, as above. They need License Server 1.15.1; an older server still reports such purchases as sent.

If you run it yourself: there is nothing to migrate. If you run MULTI_TENANT=true, set EMAIL_PROVIDER (resend, or stub on purpose) before upgrading, or the server won't start. To check whether your server emails buyers, look at its start-up log: if it says Buyer license emails are off, it doesn't. Set EMAIL_PROVIDER=resend with RESEND_API_KEY and EMAIL_FROM (Self-hosted: the email needs configuring), then redeliver each purchase marked Issued · email not sent, as above. For purchases from before 1.15.1, the log line above finds them; send those buyers their keys yourself. The self-hosted bundle download now pins License Server 1.15.1 and console 1.11.0.

v1.15.0 — 19 October 2026 ​

See a crash report arrive ​

If you've turned on crash reporting, you can now check it works without waiting for a real error. POST /v1/admin/crash-test, sent with your ADMIN_API_KEY, throws a deliberate error: it answers 500, changes nothing, and your error service receives a report with a stack trace.

  • Admin key only. It needs ADMIN_API_KEY; a tenant API key is refused (403).
  • Repeat tests group into one issue, so you can send it as often as you like and resolve that one issue afterwards.
  • The stack is obfuscated. In every build Casazium distributes, each frame from the server's own code is on line 1 of one obfuscated file: index.js:1:<column> in the Docker image, the bundled file's name in a SEA binary. The report shows that reporting works end to end, not a readable file and line.

Other changes ​

  • The license portal shows its dates in UTC. On a server whose timezone isn't UTC, it read a license's issue date as local time, so the date could be a day off: on US Eastern time, a license issued after 20:00 UTC (19:00 in winter) showed the next day. Each machine's activation time now reads like 2026-10-19 23:45 UTC. The Docker images run in UTC and were unaffected unless you set TZ.
  • Crash reports keep up to 50 stack frames instead of 10, which the web framework's own frames could fill before reaching the server's code, and their file paths are relative to the server's working directory (in the Docker image, index.js and node_modules/fastify/…). Self-hosted: only if you've turned crash reporting on.

If you run it yourself: there is nothing to migrate. The self-hosted bundle download now pins License Server 1.15.0 and console 1.10.0 (no new console release).

v1.14.0 — 19 October 2026 ​

Seats that free themselves ​

A license can now take back seats held by machines that are no longer used: a reinstalled laptop, a replaced or broken computer. Give the license a lease (Free inactive machines in console 1.10.0: after 30 or 90 days, or 1 to 365 days of your choice), and a machine that hasn't checked in for that long gives up its seat when another machine needs one. Set it when you issue a license, on an existing license with POST /admin/update-license-terms, or on a storefront mapping (lease_seconds). See Seats that free themselves.

  • No app update needed. A machine checks in with calls apps already make: validate-license (the SDK's validate()), track-usage, or activating again.
  • Turn leases on only if your app checks in, with validate() or track-usage. An app that only verifies the key or a license file never checks in, so its seats would be given away while it keeps running.
  • Nothing is taken early. A machine past its lease keeps working until a new activation needs its seat. Lapsed machines still count toward the seats, so a lease never lets a license go over them.
  • Existing licenses: turning it on starts a lease for the machines already activated, so seats held by long-gone machines come back after one lease.
  • Off by default, in the API, on mappings and in the console.
  • Activations report lease_expires_at, last_seen_at and lapsed. The console's machine list and the customer's license portal show when each machine was last seen and mark inactive ones.

This isn't floating licensing: a lease is at least a day, so it frees seats from machines that are gone, not from apps that are closed.

Other changes ​

  • deactivate-license has its own per-key rate limit (20 requests per 15 minutes), separate from activate-license's, so an app retrying activation can't use up the requests a machine needs to give its seat back.
  • The single-executable (SEA) builds could fail storefront purchases and refunds with a 500. Fixed; the Docker image was unaffected.
  • Crash reporting holds much less memory when a burst of long error messages arrives (self-hosted: only if you've turned crash reporting on).
  • Release builds are reproducible, and the published Docker image runs exactly the application bundle that CI tested.
  • Console 1.10.0: Edit terms starts from the license's current terms each time it opens. It used to reopen with the terms it first showed, so a second edit could put back an old expiry, seat count or limit.

If you run it yourself: the new database columns are added when the server starts; nothing else is required. Use console 1.10.0 for the new setting; it shows it only on License Server 1.14.0 or later. The self-hosted bundle download now pins License Server 1.14.0 and console 1.10.0.

v1.13.0 — 18 October 2026 ​

Subscriptions follow plan and seat changes ​

A subscription's license now follows the plan the customer is on. When they upgrade, downgrade or switch to yearly, the license takes the new plan's mapping — tier, limits and number of machines — on the same key. For seat-based pricing, a mapping can give activations per unit: the license allows that many machines per seat bought and follows the seat count as it changes. The expiry still moves only when a payment arrives. See Plan changes and seats.

  • Stripe mappings take a plan's price or product. A checkout still issues from its Payment Link or casazium_ref mapping, so map both the Payment Link and each price.
  • Fewer seats than machines deactivates nothing: new activations are refused until some are removed.
  • A mapping added or changed later doesn't rewrite licenses already issued. The new apply endpoint (and Apply in console 1.9.0) previews and then gives a mapping's terms to the subscriptions already on its plan.
  • Paddle: a purchase with several recurring items is issued from the first one a mapping matches, not just the first recurring item.
  • Deliveries show each subscription's plan, seat count and whether a mapping matches it, and the license's tier and machines in use.

Checked end to end before release in each storefront's test mode: an upgrade, a seat change and Apply on each storefront (on Stripe, the upgrade and seat change through its customer portal), a Stripe downgrade scheduled for the end of the period, and a Paddle subscription with two recurring items.

Existing subscribers keep their terms until you act: map every plan, let each subscription report its plan (its next subscription event, which records it without changing the license), then Apply the mappings. See Existing subscribers.

Stripe: switching to yearly extends the license ​

Under Stripe's flexible billing (the default for new subscriptions), a switch from monthly to yearly keeps the billing date and invoices the rest of the year as prorations, which License Server ignored: the license ran out at the end of the old month even though the year was paid. License Server now extends the license when such an invoice is paid for time beyond what was already paid, including on subscriptions run on a Stripe test clock. In Stripe's customer portal, invoice prorations immediately (always_invoice) so an upgrade is charged when it's made. See Plan changes and seats.

Other fixes ​

  • When a Stripe payment arrives before the subscription event it depends on, License Server answers 503 so Stripe sends it again. Crash reporting no longer files these deliberate retries as server errors (self-hosted: only if you've turned crash reporting on).

If you run it yourself: the new database columns are added when the server starts; nothing else is required. Use console 1.9.0 for the new screens. The self-hosted bundle download now pins License Server 1.13.0 and console 1.9.0.

v1.12.2 — 17 October 2026 ​

Paddle refunds made from the dashboard ​

A full refund made from Paddle's dashboard didn't revoke the license. The dashboard refunds line items, and Paddle labels a refund of every item "partial", even when it returns everything the buyer paid. License Server trusted that label, so it recorded a partial refund and kept the license, whatever the webhook's refund policy said. It now records what each Paddle purchase was charged and treats a refund that reaches it as full. Found by testing a refund in Paddle's sandbox end to end; hosted accounts already have the fix.

If you run it yourself: nothing is required — no database or setting changes. A purchase recorded before 1.12.2 has no charged amount stored, so a dashboard refund of one is still judged by Paddle's label: if you sell through Paddle, check refunds of older purchases and revoke by hand. The self-hosted bundle download now pins License Server 1.12.2, with console 1.8.0 as before.

v1.12.1 — 16 October 2026 ​

Usage reports for licenses with feature flags ​

POST /v1/usage-report answered 500 for a license whose limits include features. features is a list of feature names, not a count, so the report now leaves it out and lists the countable limits as before. That's also what the SDK's getUsageReport() calls. Hosted accounts already have the fix.

If you run it yourself: nothing is required — no database or setting changes, and console 1.8.0 works with it as with 1.12.0. The self-hosted bundle download now pins License Server 1.12.1, with console 1.8.0 as before.

v1.12.0 — 16 October 2026 ​

Data export for larger accounts ​

Settings → Download my data in the console made one License Server request per license to fetch its activations, and every one counted against the admin rate limit of 300 requests per 15 minutes. An account with more than about 300 licenses couldn't export at all. Hosted accounts can now export any number up to the 25,000-license ceiling.

  • GET /list-licenses?include_activations=true returns each license's activations inline, the same entries as GET /list-activations/:key, so a page of up to 1,000 licenses brings its activations in one request. It's off by default; see the API reference.
  • License Console 1.8.0 uses it for the export: even 25,000 licenses take 26 requests. Against an older server the console falls back to one request per license, as before. Console 1.8.0 also reports errors more cheaply during an error storm (hosted only).
  • GET /list-activations/:key lists activations made in the same second newest first, like the new option. Their order was unspecified.

If you run it yourself: nothing is required — no database or setting changes. Console 1.8.0 works with older servers, and older consoles work with 1.12.0. The self-hosted bundle download now pins License Server 1.12.0 and console 1.8.0.

v1.11.2 — 16 October 2026 ​

SDK 0.4.0 ​

Published to npm alongside this release (npm install @casazium/license-sdk@0.4.0).

  • validate(key, instanceId) checks that a machine is still activated. verifyKey() checks only the license, so after a machine's seat was freed elsewhere (from the license portal, or by another machine) it kept passing. A launch check should call both, as the desktop app guide now does.
  • HTTP errors carry err.status, so an app can tell a refused key (a 4xx) from a server it can't reach or use (a 5xx, or a network error, which has no status) without parsing the message. Messages are unchanged.
  • listLicenses({ page }) pages again. Every page returned the first; it now asks the server for the right one.

Server ​

  • One unused database index is dropped at startup. Nothing read it, so this only saves a write on each storefront purchase. Plus internal cleanups from the storefront review, with no change in behavior.
  • Crash reporting accepts one more environment name, used by Casazium's own demo server. Self-hosted crash reporting is unaffected.

If you run it yourself: nothing is required — the index is the only database change, and any console that works with 1.11.1 works with 1.11.2. The self-hosted bundle download now pins License Server 1.11.2, with console 1.7.0 as before.

v1.11.1 — 15 October 2026 ​

Storefront fixes ​

Four fixes from a review of the storefront code (Stripe, Lemon Squeezy and Paddle), each with a test that failed before it. Hosted accounts already have them.

  • Saving a Paddle API key while the webhook changes. The key is checked with Paddle before it's saved, which can take a couple of seconds. Disconnecting the webhook in that moment brought it back as active, with the erased key restored, and connecting a replacement was refused. Saving the signing secret in that moment left a fully set-up webhook stuck at "Needs setup". Both now come out right.
  • A storefront retry no longer emails a revoked key. If a purchase's first email failed and you then revoked or deleted its license, the storefront's next retry sent the buyer the key anyway. It now doesn't, and tells the storefront to stop retrying.
  • A resent license email names the right product. It used the product's current mapping, so after you deleted or changed a mapping the subject read "Your license" or named another product. It now names the product the license was issued for.
  • Send pending deliveries stops if the webhook changes mid-run. Disconnecting the webhook or saving a new API key during a run let it carry on with the old key. It now stops, with stopped: "webhook_changed".

If you run it yourself: nothing is required — no database or setting changes, and any console that works with 1.11.0 works with 1.11.1. The self-hosted bundle download now pins License Server 1.11.1, with console 1.7.0 as before.

v1.11.0 — 15 October 2026 ​

Paddle storefront fulfillment ​

Paddle Billing joins Stripe and Lemon Squeezy. Connect it once, and a completed purchase issues a license and emails the buyer their key; a subscription keeps its license's expiry in step with what's been paid; and an approved refund or chargeback revokes it. No code on your side. Paddle Billing has no license keys of its own, and Paddle's docs leave fulfillment to you, so this replaces code you'd otherwise write and host.

  • Paddle's events don't carry the buyer's email, so a Paddle webhook also takes an API key with only the Customers: read permission, which License Server uses to look each buyer up. It's checked with Paddle before it's saved, stored encrypted and never shown again. The webhook goes live once it has both the signing secret and the key.
  • Sandbox or live is chosen when you connect, and can't be changed: Paddle's events don't say which they came from. Test in sandbox, then connect a live webhook.
  • Map a price, or a whole product (covering its monthly and yearly prices). A casazium_ref in custom data counts only on transactions you create through Paddle's API, since Paddle.js lets a buyer set custom data in their browser.
  • Send pending deliveries. If the key stops working (revoked, expired, or from the wrong environment), purchases still issue their licenses, and the webhook is flagged. Save a new key, then Send pending deliveries emails the licenses that waited. Paddle doesn't redeliver an event it was answered, so this is the way to send them.

See the Paddle guide for the setup, and Storefront auto-fulfillment for every rule.

Crash reporting, optional ​

Set CRASH_REPORT_DSN and CRASH_REPORT_ENVIRONMENT and License Server reports 5xx replies, crashes and background-job errors to an error service that speaks Sentry's protocol. Messages are masked before they leave the process; bodies, headers, URLs, query strings, tenant IDs and IP addresses are never sent. It is off unless you set it, has no built-in destination, and a self-hosted install never reports to Casazium. A malformed setting never stops the server: it starts with reporting off and logs why. See Crash reporting.

Smaller changes, every storefront ​

  • Disabling a webhook erases its signing secret (and a Paddle API key).
  • The daily email cap counts emails when they're sent, so a late send of an old purchase counts too.
  • Saving a signing secret returns the webhook's status. A Paddle webhook stays pending until its API key is saved too.

If you run it yourself:

  • Nothing is required. New columns are added at startup, crash reporting stays off, and Stripe and Lemon Squeezy work as before.
  • If you run your own Compose file, set stop_grace_period: 20s on the License Server service, as the shipped files now do. On shutdown the server finishes sending a license email, which can take the email service's 10-second timeout, and Docker's default grace is 10 seconds.
  • The self-hosted bundle download now pins License Server 1.11.0 and console 1.7.0. Console 1.7.0 adds Paddle to the hosted console's Storefront webhooks screen, keeps the open webhook across a page refresh, and gives Deliveries a Refresh button. The console in the bundle doesn't include that screen, so self-hosted, set up Paddle through the API.

v1.10.1 — 14 October 2026 ​

Lemon Squeezy: only the variant decides what a purchase issues ​

A security fix, and nothing else: 1.10.0 plus this change.

Since 1.7.0, a Lemon Squeezy mapping could match a casazium_ref value in the order's checkout custom data, and it took priority over the variant. A buyer can add custom data to any Lemon Squeezy checkout link, so they could buy your cheapest variant and name the casazium_ref of your dearest mapping, getting its tier, limits and duration.

  • Lemon Squeezy purchases now match by variant only. Checkout custom data is never used, not even for a variant you haven't mapped.
  • New casazium_ref ("ref_kind": "metadata") mappings on a Lemon Squeezy webhook are refused with a 400 that says why. Existing ones stay listed, so you can remove them, but never match.
  • An order that matches no mapping now logs the variant ID it tried.
  • Stripe is unaffected. Its casazium_ref comes from your Payment Link's metadata, which buyers can't change.

If you map Lemon Squeezy purchases by casazium_ref: add a variant mapping for every variant you sell — before upgrading if you run it yourself, and now if you're hosted. Once the fix is running, an order that matches only a casazium_ref mapping is recorded as unmapped and issues nothing, and resending the webhook won't retry it: issue that license by hand. Then remove the old casazium_ref mappings.

If you run it yourself: the self-hosted bundle download now pins License Server 1.10.1 and console 1.6.4. Console 1.6.4 stops offering casazium_ref mappings for Lemon Squeezy and marks any you already have as "Not safe".

v1.10.0 — 13 October 2026 ​

Offline license checks, tightened ​

From a review of the test suite, with each fix confirmed by running it.

  • Offline licenses are tagged as licenses. Software-update manifests (check-update) are signed with the same key as offline licenses, and nothing told them apart: a manifest — which any license holder can fetch — passed as a license with no key and no expiry. The offline export now carries "type": "license".
  • The JavaScript SDK's verifySignedFile() checks expiry. It checked only the signature and product, and the docs never said to check expires_at, so an expired offline license kept working. From SDK 0.3.0 it checks all four: signature, type, product_uuid and expiry. It returns false, rather than throwing, on a malformed file.
  • A date JavaScript can't parse is refused as expires_at — a leap second (…T23:59:60Z) slipped through and never expired. A license stored with one is repaired at startup. expires_at is now stored in one form (2030-06-01T10:00:00.000Z) whatever form it was sent in.
  • /verify-license-file refuses a file whose terms were cut — shortened, downgraded, or for another product — with "License terms have changed - export a new license file". A renewal that extends a license doesn't trigger it.

Other fixes ​

  • A replacement storefront webhook can map what the old one did. Before, the disabled webhook's mappings blocked the same Payment Link or variant with 409, and purchases through the new webhook issued nothing.
  • Lemon Squeezy subscriptions billed daily could extend a day early, and extend on a failed renewal. Monthly and yearly plans weren't affected.
  • Deleting a hosted account cancels its subscription, as the console said it would.
  • The portal's license file includes product_uuid; /verify-license-file-base64 returns the license's limits; track-usage refuses features (a list, not a count); activation checks a license's status as every other check does.

If you run it yourself:

  • The self-hosted bundle download now pins License Server 1.10.0 and console 1.6.3 (1.6.3, the same day: Delete account says when the subscription couldn't be canceled, so the account was kept).
  • Export offline license files again if your app uses SDK 0.3.0 or checks type: files exported before 1.10.0 have none, and are refused.
  • With real email on, set PUBLIC_BASE_URL (LICENSE_PUBLIC_URL in the self-hosted bundle): License Server now refuses to start without it, rather than emailing buyers a broken portal link.

v1.9.0 — 7 October 2026 ​

Storefront subscriptions, on Stripe and Lemon Squeezy ​

Sell a subscription through your storefront and its buyer keeps one license for as long as they pay for it. The key arrives with the first purchase; each renewal moves the same license's expiry forward, plus a grace period (7 days by default, 0–30, per webhook); and when the subscription ends, so does the license. Renewals send no email.

  • Only a payment extends a license — Stripe's invoice.paid, or Lemon Squeezy's payment events — never a subscription's status on its own, so a license is never extended for a period that wasn't paid for.
  • A failed payment leaves the license running to the end of what's paid, plus the grace, while the storefront retries. A cancellation keeps exactly what was paid. Trials and free pauses run to their end.
  • The deliveries list shows each purchase's subscription — its state and the date it's paid to or ends — and flags a subscription purchase with no payments after a day, which usually means the payment events aren't enabled.
  • Not yet handled: plan and seat changes, and refunds of renewal payments.

See Subscriptions for every rule.

If you already use storefront webhooks

  • Stripe: a Payment Link with a recurring price now issues a license (before 1.9.0 it was ignored). Enable invoice.paid and customer.subscription.created, .updated and .deleted on the endpoint, or those licenses expire after the grace period.
  • Lemon Squeezy: enable the subscription events together with subscription_payment_success and subscription_payment_recovered. Without the payment events a subscription never extends. Until a subscription's first payment arrives, its license keeps the mapping's terms, so a webhook with only order_created behaves as before. Licenses from subscriptions sold before 1.9.0 aren't linked to them and don't change.
  • A subscription's first payment sets its license's expiry, replacing one you changed by hand before it.

Smaller changes ​

  • Free one-time Stripe purchases now issue a license. A $0 price, or a 100% off coupon or promotion code, completes without a payment; these waited forever for a payment event that never comes. Stuck purchases from before the upgrade aren't retried — issue those licenses by hand.
  • Lemon Squeezy refunds (1.8.0) are verified against a live refund. To test one in test mode, send it through Lemon Squeezy's refund API (for example with curl): its dashboard refund fails in test mode.

No manual migration: the new table and columns are created at startup. If you run the self-hosted bundle: the bundle download now pins License Server 1.9.0 and console 1.6.1. The console in the bundle doesn't include the Storefront webhooks screen, so self-hosted, set the grace period through the API (PATCH /admin/storefront-webhooks/{id} with { "subscription_grace_days": 14 }).

v1.8.0 — 7 October 2026 ​

Refunds now revoke storefront licenses — by default ​

When a storefront purchase is fully refunded, License Server now revokes the license it issued: Stripe charge.refunded, Lemon Squeezy order_refunded. This is the default for every storefront webhook, including ones you connected before this release. To keep licenses after refunds, set the webhook to record only — in the console (1.5.0), When a purchase is fully refunded → Keep it (record only), or PATCH /admin/storefront-webhooks/{id} with { "refund_policy": "record" }. Revocation also needs the refund event enabled in your storefront's webhook settings.

  • Partial refunds are recorded, never revoked.
  • A full refund is final: a license you reinstate afterwards stays active through storefront retries and resends.
  • A purchase refunded before its license is issued never gets one, and a refunded purchase is never emailed its key.
  • Stripe purchases made before this release can't be matched to a refund — revoke those by hand.
  • A downloaded offline license file can't learn of a revocation and keeps verifying until it expires.
  • Lemon Squeezy refunds have since been verified against a live Lemon Squeezy refund (2026-10-07), as Stripe's were.

The deliveries list shows what each refund did. See Refunds.

Smaller changes ​

  • A disabled storefront webhook now refuses a new signing secret or refund setting with 409 ("connect a new one") instead of silently reviving, or failing with a 500 when a replacement exists.

No manual migration: the new database columns are added at startup. If you run the self-hosted bundle: the bundle download now pins License Server 1.8.0 and console 1.5.0. The console in the bundle doesn't include the Storefront webhooks screen, so self-hosted, set the refund policy through the API.

v1.7.0 — 6 October 2026 ​

Lemon Squeezy storefront fulfillment ​

Storefront auto-fulfillment now works with Lemon Squeezy as well as Stripe Payment Links. Connect it once, and every completed Lemon Squeezy order issues a license and emails the buyer their key, with no integration code on your side.

  • Connect with "provider": "lemonsqueezy", or Connect Lemon Squeezy in the console (console 1.4.0).
  • Lemon Squeezy has you choose the webhook's signing secret: use the same 16–40 character secret in Lemon Squeezy and in License Server. The console generates one for you.
  • Subscribe the webhook to order_created, and map each product by its variant ID, or by a casazium_ref value passed through checkout.
  • Lemon Squeezy's webhooks carry no timestamp, so License Server ignores an order more than 7 days old. That stops a captured request from issuing a second license later. After an outage, resend the webhook from Lemon Squeezy within 7 days of the order.

Stripe works exactly as before. A mapping's ref_kind is now checked against its webhook's provider: variant on a Stripe webhook, or payment_link on a Lemon Squeezy one, is a 400.

Smaller changes ​

  • A storefront webhook request whose body isn't valid JSON now counts against the same per-IP failure limit as a bad signature. Its 400 response reads { "error": "Invalid webhook payload" } (previously { "error": "Bad Request" }).

No database migration. If you run the self-hosted bundle: the bundle download now pins License Server 1.7.0 and console 1.4.0. The console that ships in the bundle doesn't include the Storefront webhooks screen, so self-hosted, you connect Lemon Squeezy through the API.

v1.6.2 — 5 October 2026 ​

Secrets can be read from files ​

Each of the four required secrets — ADMIN_API_KEY, ENCRYPTION_KEY, LICENSE_SIGNING_SECRET and LICENSE_RSA_PRIVATE_KEY — can now be read from a file: set ADMIN_API_KEY_FILE (and so on) to the file's path instead of the value itself. That's the convention Docker secrets and most orchestrators use, so you can keep secrets out of environment variables and compose files. A value set directly still wins, and if the file named is missing or empty the server refuses to start rather than generating a different secret.

If you run the self-hosted bundle: the bundle download now uses this to share its generated admin key between License Server and the console (console 1.3.6). Nothing changes for you unless you update to the new bundle files; your existing key keeps working either way.

v1.6.1 — 28 September 2026 ​

The 30-day evaluation now counts from your first boot ​

Evaluating without a license (CASAZIUM_UNLICENSED_EVAL=1) now gives every installation a full 30 days, starting the first time it boots in evaluation mode. Previously the 30 days ran from the date the image was built, so a copy downloaded late in a release's life got only a few days, and one downloaded more than 30 days after that release couldn't start in evaluation mode at all.

The start date is recorded in the installation's own database, so restarts, upgrades and new image pulls don't change it. It never starts later than the oldest license or activation already in the database. When the 30 days are up, the server refuses to start until you add a purchased license, and nothing in the database is lost. The expiry message now reads the evaluation allowance window for this installation has expired.

If you're evaluating on an earlier version: upgrading to 1.6.1 starts your 30 days from the oldest license you've issued, or from the first boot on 1.6.1 if you haven't issued any. Licensed installations and hosted accounts are unaffected.

v1.6.0 — 28 September 2026 ​

Hosted — Business, a volume tier between Pro and self-hosted ​

A new Business plan: up to 10,000 active licenses for $79/mo ($758/yr billed annually, ~20% off), for teams issuing at real volume rather than outgrowing Pro's 1,000-license ceiling. Priced well below comparable volume tiers from Keygen, Cryptlex and LicenseSpring, following a market analysis and pricing review. See Plans and quotas for the full table.

If you run your own multi-tenant deployment with Stripe billing enabled (BILLING_PROVIDER=stripe), it now needs two more environment variables, STRIPE_PRICE_BUSINESS_MONTHLY and STRIPE_PRICE_BUSINESS_ANNUAL, alongside the existing Pro price IDs — the server refuses to start if either is missing while Stripe billing is on.

Hosted — Free raised from 5 to 50 active licenses ​

The same pricing review that added Business found Free looked thin next to comparable free tiers elsewhere. No self-hosted impact — self-hosted has never had a Casazium-imposed license limit.

v1.5.4 — 25 September 2026 ​

@casazium/license-sdk 0.2.2 (patch). Eight methods — verifyKey(), trackUsage(), getUsageReport(), exportLicense(), exportOfflineLicense(), getPublicKey(), and the Node/CLI-only listLicenses()/revoke() threw a bare error naming only the method and HTTP status, discarding the response body's own error message. activate(), deactivate() and checkUpdate() already surfaced it; these eight now match. Previously, a trackUsage 403 could mean an inactive or expired license, an invalid activation token, a metric absent from that license's limits, or a metric over its limit — four unrelated causes collapsed into one indistinguishable message, forcing a manual admin-API lookup just to tell them apart.

v1.5.3 — 24 September 2026 ​

Fixed: the SEA binary silently ignored .env-file values for TIER_A_LICENSE_FILE, TIER_A_LICENSE, and CASAZIUM_UNLICENSED_EVAL. The Tier-A license check ran before the module that actually reads .env was ever imported, so a value set only in .env wasn't in the environment yet when the check happened — it always fell through to the 30-day evaluation window regardless of how correctly .env was written. Exporting the same variable directly in the shell worked fine, which is what made this easy to miss. Docker and npm run dev were never affected; this was specific to the SEA binary's own boot order. Fixed by loading .env before the license check runs, not after.

v1.5.2 — 24 September 2026 ​

Tier-A activation-license anchor key rotated. Self-hosted Tier-A builds published from this release onward verify activation licenses against a new anchor key. An activation license signed under the previous key no longer verifies against a build compiled from this release forward — no previously-issued license is known to depend on the old key. This affects only the internal verification mechanism, not the TIER_A_LICENSE_FILE/TIER_A_LICENSE configuration format, CASAZIUM_UNLICENSED_EVAL, or any other application behavior.

v1.5.1 — 24 September 2026 ​

Runtime bumped from Node 22 to Node 24, in both the Docker image and the SEA binaries. Not routine: Wolfi (the Docker base image's package repo) dropped the nodejs-22 package line entirely, breaking every build. Went forward to Node 24 rather than back to a Debian-based image, since Node 22 is no longer obtainable from Wolfi at any patch version. Also required bumping better-sqlite3 from 11.x to 13.x, which crashes under Node 24; 13.x doesn't, and ships prebuilt binaries for every platform, so the Dockerfile's forced-rebuild step for 11.x was removed along with it.

No application-level behavior changes here — this is a toolchain update. If you run the published Docker image or the SEA binary as released, there is nothing to do. If you build your own image from source, better-sqlite3 will now install its prebuilt binary instead of compiling one.

v1.5.0 — 24 September 2026 ​

Storefront purchase-history retention. Resolved purchase records (GET /admin/storefront-webhooks/{id}/deliveries) are now automatically pruned after STOREFRONT_PURCHASE_EVENTS_RETENTION_DAYS (default 400 days, minimum 35) once a purchase is fully resolved and, if a license was issued, actually delivered. Licenses themselves are never affected — only the storefront delivery-history view is trimmed. A purchase still awaiting payment confirmation, or one whose license was issued but never delivered, is never pruned regardless of age.

Litestream's compaction interval widened from 5 to 15 minutes (l0-retention-check-interval in the shipped litestream.yml), after a live Backblaze B2 Class C request-cap alert hit 100%. Affects any self-hosted deployment with Litestream replication enabled; off by default, so most deployments see no change.

Fixed: a missing or invalid required environment variable no longer dumps megabytes of obfuscated source before the real error message on the SEA binary. The required-variable check used to throw before the binary's own error handler existed to catch it, so Node's default unhandled-exception reporter printed a source "code frame" instead — on the obfuscated build, effectively an unreadable wall of text with the real message buried at the very end. It's now a clean, caught error.

Fixed: DB_FILE=:memory: now actually creates an in-memory SQLite database, instead of a real file literally named :memory: on disk. Development use only; incompatible with Litestream and the backup scripts, which both assume a real on-disk file.

v1.4.0 — 23 September 2026 ​

Sell from your own storefront ​

Connect a Stripe Payment Link and a completed purchase issues the license and emails the buyer their key, with no integration code on your side. Map a Payment Link once — to a product, tier, seat count and duration — and every sale through it fulfills itself.

Licenses are issued through the same path POST /issue-license uses, not a parallel implementation, and the confirmation email is tracked separately from issuance: a failed email is retried without the key ever being issued twice.

It is on every plan, self-hosted and hosted alike, with no additional gating. The only ceiling is the active-license quota you already have.

Storefront auto-fulfillment covers the setup and, more importantly, the decisions each mapping encodes — including a duration_days that defaults to perpetual if you leave it out.

Hosted — Pro now allows 1,000 active licenses ​

Raised from 100, at the same $39/mo, after a pricing review found the gap against comparable tiers was volume rather than price. Free is unchanged at 5. Self-hosted has no Casazium-imposed limit and is unaffected.

EMAIL_REPLY_TO ​

Self-hosted only: an optional reply-to address for the storefront confirmation email, alongside EMAIL_FROM. Omitted entirely rather than sent empty when unset.

On how this one was built ​

Storefront auto-fulfillment shipped after a five-round adversarial design review, an independent security audit, and a second logging audit. The audit found six real issues — a rate-limit design that could be turned against a tenant, a payment-mode gate gap, a revoked-tenant issuance gap, two log leaks of credentials or personal data, and an email-relay abuse vector — all fixed before this release.

None of them ever reached a tagged release, so they are described here rather than listed as fixes: no version of this feature has been publicly available in a broken state.

v1.3.1 — 21 September 2026 ​

A follow-up to v1.3.0, closing gaps that release left behind.

product_uuid now appears in the plain license exports.GET /export-license/:key and /export-license/:key/file both predate the product_uuid redesign and were missed by it — unlike the offline export, neither carried the field at all, in the signed JSON or the encrypted .lic file.

POST /verify-license-file and its base64 counterpart now accept and echo product_uuid. Found while fixing the above: since the field is now part of what /export-license/:key signs, a client resubmitting its own exported envelope would have had it silently stripped before verification — breaking the round trip for any license that had one.

@casazium/license-sdk 0.2.1 makes exportLicense() require expectedProductUuid and throw on a mismatch, matching verifyKey(), checkUpdate() and verifySignedFile(). That method had no product-identity check at all before, which is a defense-in-depth gap rather than only an inconsistency.

v1.3.0 — 21 September 2026 ​

A breaking release. Read this before upgrading, particularly the SDK note at the end.

product_id is no longer globally exclusive ​

Previously, the first tenant to issue a license or register a release under a given product_id owned that string across the whole platform, and everyone else was rejected. That is gone: product_id strings are now scoped per tenant, so two tenants — or, self-hosted, two products of your own — can use the same string without colliding.

The relationship key going forward is product_uuid: immutable, generated by the server, never settable from request input. Every license and release issued from this version onward has one, and verify-license, check-update and the offline exports return it.

Two consequences if you have been working around the old behavior:

  • POST /admin/release-product-id has been removed. It existed to free a claimed product_id for reuse. With no shared namespace left, there is nothing to free. Calls to it will fail.
  • The permanent product_id retirement behavior is gone too. Deleting a tenant now removes its products by a plain cascade. That behavior protected a scarce global resource that no longer exists.

product_uuid is now returned by the admin routes as well ​

Until this release only the public routes returned it, which made the SDK's new requirement awkward to satisfy — you could verify a product_uuid but had no supported way to look yours up. issue-license, register-release, list-licenses, list-releases, GET /admin/license/:key and GET /release/:id all return it now, read-only. The hosted console shows it on the license and release detail pages, and on both success screens, with a copy button.

The SDK requires it — update your app before upgrading ​

@casazium/license-sdk 0.2.0 makes expectedProductUuid a required argument to verifyKey(), checkUpdate() and verifySignedFile(). They throw if it is omitted, and fail if the server's response describes a different product than the one you expected. That is a breaking change to the call signatures: every app using the SDK needs to pass its own product_uuid before it will work against this version.

The point of the check is that a response proving a license is valid is not the same as proving your product's license is valid. The argument makes the difference impossible to skip.

v1.2.0 — 16 September 2026 ​

Two features, both off by default, and the first official SDK.

End-user license portal ​

A page your own customer can reach directly — no Casazium account, no password — to view their license, deactivate a machine they no longer use, and download their .lic file. It exists so that "I replaced my laptop and now it says I'm out of seats" stops being a support email you have to answer by hand.

Enable it with PORTAL_ENABLED. It requires no integration work: the portal is served by the same License Server instance you already run.

Continuous replication via Litestream ​

Stream every database change to your own S3-compatible storage within seconds of it happening, independent of Casazium's infrastructure and independent of your own backup schedule. If you self-host because you want the data under your control, this is the piece that makes "under your control" include the recovery story.

Off by default; DEPLOYMENT.md in the release archive covers the configuration.

Official JavaScript SDK ​

@casazium/license-sdk is now on npm, covering verification, activation, deactivation and update checks, with a machine-fingerprint helper. It is a convenience layer over the same HTTP API documented here — nothing it does is unavailable to a plain fetch call, and the API remains the contract.

v1.1.0 — 14 September 2026 ​

A licensing-terms change, not a code change, and the reason to read this one: the end-user license agreement gained a commitment for self-hosted licensees. If Casazium stops operating the service, you get at least 90 days' written notice, and before shutdown a final build with the activation check and all Master Server communication removed — yours to run indefinitely. A validation failure caused by Casazium ceasing to operate can never degrade your installation.

The full text is in the EULA, §8 and the §4 proviso.

v1.0.11 — 24 August 2026 ​

A release-pipeline fix with no user-facing change. If you are on v1.0.10 or v1.0.11, upgrade for the v1.1.0 and v1.2.0 content above rather than for anything in this one.


Before v1.0.11 ​

Releases before this page existed are not written up here. The release mirror lists every published version with its binaries; the notes on the older ones are thin, and rather than pad this page with reconstructions, we would rather say so.

How versions work ​

Ordinary semantic versioning: a patch release is a fix, a minor release adds something backward-compatible, and a major release would break something — there has not been one. The API is the contract, and any breaking change to it comes with a major version and its own migration note here.