How a self-hosted Versus install proves its entitlement with a signed, offline-verifiable license token — activated online with a code, offline by exchanging a fingerprint, or fully air-gapped with nothing ever phoning home.
A self-hosted Versus install proves what it's entitled to run with a license token — a small, signed file that lists the customer, the license class, the modules you've bought, any numeric limits, and an expiry. This page explains what that token is, the three ways to activate one (online, offline, and air-gapped), and how the platform behaves as a license moves through its lifecycle.
The design goal throughout is simple: the signed file is always authoritative, and nothing is ever forced to phone home. Online activation is a convenience layer on top of a primitive that verifies entirely offline.
A Versus license is a compact, JWS-style token — three base64url segments, header.payload.signature — signed with Ed25519 (EdDSA). The header carries typ: VESLIC, alg: EdDSA, and a key id (kid); the payload is the entitlement:
{
"license_id": "6f3c…",
"issued_at": "2026-08-19T00:00:00Z",
"not_before": "2026-08-19T00:00:00Z",
"expires_at": "2027-08-19T00:00:00Z",
"customer": { "org_name": "Acme Corp", "org_code": "acme", "sub": "" },
"class": "subscription",
"edition": "enterprise",
"features": ["tls_clm", "adc", "ipam_ddi"],
"limits": { "max_devices": 500 },
"binding": { "install_fingerprint": "…" },
"grace_days": 14
}
The private signing key never ships with the product — it lives only in the issuer (the hosted Licensing Service and the ops tooling). The product carries an embedded public keyring keyed by kid, so every install can verify a token's signature with no network access at all. Because each token names the kid it was signed under, old licenses keep verifying across key rotations; additional trusted keys can be supplied at runtime through the LICENSE_TRUSTED_KEYS environment variable without a code change.
Verification checks the signature and format only. Policy — expiry, not_before, grace, and fingerprint binding — is applied separately when the token is installed, so a syntactically valid but expired or mis-bound license is still rejected.
License enforcement is off unless the deployment is explicitly on-premise. An install enforces licensing only when VERSUS_DEPLOYMENT_MODE=onprem (or LICENSING_ENFORCED=true) is set. On the managed cloud service, on developer machines, and on existing installs, the licensing subsystem is inert: every feature is available and no token is required. Multi-tenant SaaS entitlements are handled by a separate mechanism.
Everything that follows describes a self-hosted, enforcing install.
On first access, each install generates a stable fingerprint — a random per-install identifier that is created once and never changes for the life of the deployment. Licenses can be node-locked by binding them to a fingerprint: a bound token installs only on the install whose fingerprint matches, and is rejected everywhere else.
Read your fingerprint from the API or the System → Licensing page:
GET /api/licensing/fingerprint
Authorization: Bearer …
200 OK
{ "fingerprint": "a1b2c3…" }
You need this value for offline activation (below), where it's the input the Licensing Service binds the token to.
The fastest path for a connected install. When a license is issued, the Licensing Service can mint a one-time, human-friendly activation code in the form VS-XXXX-XXXX-XXXX (the alphabet excludes ambiguous characters like 0/O and 1/I). You paste that code into System → Licensing or post it to the API, and the install does the rest:
POST /api/licensing/activate
Authorization: Bearer … # Network-Admin
Content-Type: application/json
{ "activation_code": "VS-7K4M-92QF-JD3P" }
Under the hood the install sends the code together with its own fingerprint and product version to the Licensing Service's public endpoint (POST /api/activation/v1/activate). The service validates the code — it must exist, be unconsumed, and be unexpired — re-signs the license bound to your fingerprint, marks the code consumed, and returns the token. The install then verifies and stores it exactly as if you'd uploaded a file. The response is your refreshed licensing status.
Activation is a no-op — and returns a clear error — on any install that isn't enforcing licensing or has no license server configured (i.e. an air-gapped install), so you can't accidentally trigger a network call where one isn't wanted.
If the install can't reach the Licensing Service but you can, activate out-of-band:
GET /api/licensing/fingerprint)..veslic token node-locked to that fingerprint.POST /api/licensing/apply
Authorization: Bearer … # Network-Admin
Content-Type: application/json
{ "token": "eyJhbGc….eyJ….sig" }
Applying a token verifies its signature against the embedded keyring, checks not_before and expiry (with grace), and confirms the fingerprint binding matches this install. A newly-applied token replaces any previous one and clears any prior revocation — a freshly issued license always supersedes what came before.
An air-gapped install is simply an enforcing install with no license server configured. It never contacts anything: you activate it exactly like the offline flow above — fingerprint out, signed .veslic in — and the token's offline signature verification is the whole trust story. No heartbeat runs, no usage is reported, and a network failure can never lock you out because there is no network call to fail. This is the intended posture for classified or isolated environments.
Every read of the current license computes a state:
| State | Meaning |
|---|---|
not_enforced | Licensing is off (cloud, dev, or non-onprem install). |
unlicensed | Enforcing, but no valid token is applied. |
valid | A verified token, within its term. |
grace | Past expires_at but still inside grace_days. |
expired | Past the grace period. |
revoked | Revoked by the issuer (delivered via heartbeat). |
invalid | A token that fails signature verification. |
Enforcement is deliberately gentle and never hard-bricks an install. Within grace_days (default 14) after expiry, everything keeps working and the UI warns you. Once an enforcing install passes grace — or is revoked — it goes read-only: reads succeed, but writes are refused with an HTTP 402. Even then, login, license management (/api/licensing), and auth are always exempt, so an admin can sign in and apply a renewal to recover.
Beyond term and grace, a license also gates modules. A request into a module the license doesn't include is refused with 402 regardless of state; the always-included Platform Core is never gated.
Licenses select from a set of à-la-carte functional modules:
| Module key | Label |
|---|---|
tls_clm | TLS / CLM |
firewall | Firewall |
secure_vault | Secure Vault |
adc | ADC |
ipam_ddi | IPAM / DDI |
data_warehouse | Data Warehouse (Snowflake) |
Curated bundles — Keystone, Perimeter, Fabric, and Continuum — are presets that select a set of these modules. Platform Core (Versus Desk, Workflow, AI Ops, Vulnerability Management, Observe, ITSM, GitOps/CI-CD, Cloud, Compliance) ships with every license and is never gated.
Licenses come in three classes: trial (45-day term by default), subscription (1/2/3-year terms), and nfr (not-for-resale, for internal and SE demo use).
A connected install — one that's enforcing and has a license server configured — periodically checks in with the Licensing Service to pick up renewals and revocations. This is the only ongoing network activity licensing performs, and it's optional: air-gapped installs never run it.
The heartbeat is driven by a management command on a timer (a systemd timer or Kubernetes CronJob), at the server-advised cadence (default 24 hours):
python manage.py validate_license
python manage.py validate_license --dry-run # report only; contact nothing, write nothing
Each cycle posts the license_id, fingerprint, a fresh random nonce, and best-effort usage counts (managed device and collector counts — counts only, no PII) to POST /api/activation/v1/validate. The service answers with the license's current state, a revoked flag, and — on a renewal or upgrade — a superseding token the install verifies and swaps in automatically.
Two invariants make this safe:
You can trigger a check on demand from System → Licensing ("Check now") or the API:
POST /api/licensing/validate
Authorization: Bearer … # Network-Admin
→ returns the refreshed licensing status
Because a revocation is delivered through the heartbeat, revoking a license only takes effect on connected installs — an air-gapped install is governed entirely by the term and fingerprint baked into its signed file.
Any authenticated user can read the current status; it's what the System → Licensing page renders:
GET /api/licensing/status
Authorization: Bearer …
200 OK
{
"enforced": true,
"state": "valid",
"class": "subscription",
"edition": "enterprise",
"features": ["tls_clm", "adc", "ipam_ddi"],
"limits": { "max_devices": 500 },
"expires_at": "2027-08-19T00:00:00Z",
"days_remaining": 364,
"fingerprint": "a1b2c3…",
"activation_source": "online",
"connectivity": { "status": "ok", "last_verified_at": "…", "next_check_estimate": "…" }
}
The page surfaces the class chip, current state, days remaining, activation source (online vs. offline), and — for connected installs — a connectivity indicator (ok / stale / offline / never) with the last successful check-in. The connectivity hint is display-only; it never gates anything, so a flaky link shows a warning icon but changes nothing about what you can do.
| Endpoint | Method | Access | Purpose |
|---|---|---|---|
/api/licensing/status | GET | Authenticated | Current license state, entitlement, connectivity. |
/api/licensing/fingerprint | GET | Authenticated | This install's fingerprint (for offline activation). |
/api/licensing/apply | POST | Network-Admin | Apply a .veslic token (offline / air-gapped). |
/api/licensing/activate | POST | Network-Admin | Online activation with an activation code. |
/api/licensing/validate | POST | Network-Admin | Run one heartbeat now ("Check now"). |
license.<host>These are called by connected installs — the activation code or signed nonce is the bearer, so no user auth is required — and are rate-limited.
| Endpoint | Method | Purpose |
|---|---|---|
/api/activation/v1/activate | POST | Exchange an activation code for a node-bound token. |
/api/activation/v1/validate | POST | Heartbeat: state, revocation, superseding token (signed envelope). |