Software Distribution
Gate your own software's downloads and update checks behind a license key, and give end-user apps a signed way to check "is there a newer version." This works the same way whether you're running License Server self-hosted or on the Hosted (SaaS) Tier — the endpoints themselves don't care which. See the Software Distribution API reference for the full endpoint list and request/response shapes, and worked examples for both sides: publishing a release (Registering a Release) and consuming one (Auto-Update Checking).
What License Server does and doesn't do here
License Server never stores or proxies your software's actual bytes — only release metadata and a signed pointer. artifact_url must be a URL your own end users can already reach (your own CDN, object storage, or GitHub Releases). Registering a release doesn't verify the URL resolves or that checksum matches the real file — that's on you to get right at publish time; the signature only proves what you told License Server at registration hasn't been tampered with since.
Registering a release
In the console
On a hosted account, or in the self-hosted bundle's console, go to Releases and choose Register release. Fill in:
- Product ID: the same Product ID your licenses use;
- Version, such as
2.3.0; - Channel,
stableunless you change it, and Platform, such asdarwin-arm64; - Artifact URL: where you already host the file;
- Checksum, such as
sha256:...; - Release notes, optional.
Choose Register release. The "Release registered" page shows the Product UUID, with a copy button: pass it as expectedProductUuid to the SDK's checkUpdate(). The release's own page, opened from the Releases list, shows it again with the signature.
To take a release out of update checks, choose Unpublish on its row in the list or on its page, and confirm. Using the Console covers the list's filters.
Through the API
POST /v1/register-release with your admin API key does the same, for a build pipeline: see Registering a Release for a worked example, and the Software Distribution API reference for every field.
How update checks work
Register a release under your own product_id (channel/platform-scoped — e.g. stable/darwin-arm64; see Product IDs are per-tenant, not global — two different tenants may share the identical product_id string, each scoped to their own data), then have your app call POST /check-update with one of your customers' license keys to find out whether it's behind and get a signed manifest pointing at the newer version. "Latest" is the highest semver version among your published releases, not registration order — publishing a hotfix to an older line after a newer one is already out never shadows the genuinely newer release. Unpublishing a bad release stops it appearing in live check-update results immediately - the row itself is kept, not deleted. There's no dedicated "republish" call, but getting the version back is straightforward: register it again with the exact same product ID, version, channel, and platform - the uniqueness check that normally rejects a duplicate registration only applies to published releases, so re-registering an unpublished one always succeeds.
channel and platform are matched exactly and case-sensitively at check-update time - "Stable" and "stable" are different channels. A mismatch fails closed (no update offered), not an error, so a typo'd value here is silent - double-check these match what you registered if updates never seem to arrive for a customer.
The releases check-update can return are scoped to the calling license's own product_uuid (its real, immutable product identity, resolved server-side from the license itself) - product_id in the request is optional, checked only as a defense-in-depth convenience. If you're on @casazium/license-sdk v0.2.0+, checkUpdate() requires an expectedProductUuid argument and throws if the response describes a different product than expected - pass your own product's product_uuid (get it once from your first issue-license/register-release call and hard-code it) rather than skipping this check.
Unpublishing doesn't revoke an already-cached manifest
check-update's signature proves a manifest wasn't tampered with — it doesn't prove the manifest is still current. A response your app cached earlier still verifies successfully even after you unpublish that release. If your app ever caches a check-update response instead of calling it fresh, check the response's issued_at field yourself and reject anything older than you're willing to trust — this is your responsibility as the integrator, not something License Server can enforce on your behalf.
The 500-release limit
Each product_uuid / channel / platform combination holds up to 500 published releases. Registering the 501st into the same combination returns:
403 {"error": "Release limit reached (500 published releases) for this
product_id/channel/platform - unpublish old releases before registering
new ones"}The error text still says product_id, but the bucket is keyed on product_uuid as of v1.3.0. That distinction only became meaningful in that release: product_id strings are no longer globally exclusive, so two tenants using the same string get separate buckets of 500 rather than sharing one.
The scoping is what usually matters in practice: the cap is per combination, not per product. A product shipping stable and beta across three platforms has six independent buckets of 500, and a release in one never counts against another.
Only published releases count. Unpublishing frees a slot immediately, so hitting the cap is recoverable without losing history — the unpublished row is kept, not deleted, and can be re-registered later exactly as described above. Unpublishing your oldest releases is the intended remedy, not a workaround.
The limit is an anti-abuse ceiling, not a plan feature. It is identical self-hosted and hosted, doesn't vary by tier, and isn't configurable — an unbounded bucket makes check-update (a public, IP-rate-limited route) expensive to serve, which is the behavior the cap exists to prevent. At a realistic release cadence you should never reach it; if you are approaching 500 in one bucket, that is usually a sign that automated builds are being registered as releases.
Self-hosted: your signing key must persist
Update manifests are signed with the same RSA private key used for offline license verification (LICENSE_RSA_PRIVATE_KEY — see Generating Secure Keys), and verified by end-user apps against the matching public key served from GET /v1/public-key. If that key is ever rotated or regenerated — including via AUTO_GENERATE_SECRETS=true on a fresh deployment that didn't carry the old value forward — every manifest your app already shipped stops verifying for clients still holding the old public key. Back up LICENSE_RSA_PRIVATE_KEY (or your .secrets.json, if using auto-generation) the same way you'd back up any other production secret, and carry it forward on every redeploy.
On the Hosted (SaaS) Tier
check-update keeps working after a billing lapse
POST /register-release checks your subscription standing and refuses new releases once payment fails, the same as POST /issue-license. But POST /check-update doesn't - your existing end users keep getting served their already-registered releases and signed manifests regardless of your account's billing or revocation status, the same as every other public license-validation route. This matches this API's existing posture everywhere else (no public route consults account status), it's just worth knowing explicitly for this one, since it means "publishing" and "distributing what's already published" are not gated the same way.