On this page0
Unfamiliar word? See Glossary: words used in these docs. Looking for the tasks in order? Begin at Start Here.
Reference
License Administration
What an administrator can see and do about licensing on your instance: read the license status, ask for a check-in now, and reset. Activation and the alert thresholds are managed by Zonitix and are not set from the instance.
Where This Lives In The Product
In the app, open System → License & Entitlements. It shows the license status and plan, the features your plan includes, and a heartbeat health card with the number of missed check-ins, the grace time remaining and the two alert thresholds — those are read-only and labelled “Managed by Zonitix; shown here for reference”. There are two buttons:
- Force Sync Heartbeat — runs one check-in now. While the instance has not managed to register, the same button reads Retry registration.
- Emergency License Reset — clears the license state and switches off every module that depends on your plan. Only someone on the administrator role can complete it.
There is no control for entering a provisioning key and none for editing the thresholds.
When Someone Needs To Step In
Your instance is activated automatically: it registers with the Zonitix licensing server (the store) using the provisioning key we place on it when we build it, then checks in every five minutes, and the store answers with the features your plan includes. The two buttons exist for two moments: a manual re-check after a network outage, and clearing license state on a server that is being handed back. Both are written to the audit log.
A Missed Heartbeat Disables Nothing
The Key Is Never Logged
What Happens When A Subscription Lapses
Separately from missed check-ins, the instance pauses paid modules when the subscription itself has ended. This is the full rule; other pages link here. Each verified reply from the store carries a signed paid-through date. Paid modules are paused only when all of these hold:
- a paid-through date from a signature-verified store reply is on record;
- that date is more than 14 days in the past;
- the store has answered your instance since that date. An instance that simply cannot reach the store is never paused on this rule;
- the rule is in force on the instance.
If the date is missing or unreadable, or any of the conditions cannot be read, the instance treats the subscription as not lapsed.
- During the 14 days: nothing is paused.
- After them: requests to the modules that depend on your plan are refused with the message “Your subscription has lapsed. Premium features are paused until it is renewed — your data and core features remain available.”
- Never paused: the core modules that carry no license flag, your data, and the Report Engine. Links already sent to people outside your company keep working too: a document out for signature, a candidate’s portal and self-scheduling links, and the webhooks that complete them.
- Who is told: the app shows a banner to administrators and to anyone whose role lets them manage security settings. During the 14 days it names the date the subscription ended and the date the pause begins; once paused it says premium modules are paused. Closing it hides it for that browser session; it returns when the state changes.
- To restore: renew. The next verified heartbeat delivers the new signed date and the modules resume.
The status fields behind this rule and the refusal code are in the technical part below, under Subscription Lapse: Status Fields And Refusal.
If something here is not working as you expect, start with When something goes wrong.
The plain explanation is above. This part is for IT and developers: the exact settings, permission names, API addresses and data fields behind it. Closed: select the heading above to open it. While it is closed, your browser’s find-on-page (Ctrl+F or Cmd+F) does not search this part.
The screen is at /dashboard/license; the page and its menu entry need the manage_security permission. Force Sync Heartbeat is the same action as POST /license/sync and reads Retry registration while the status is PROVISIONING_FAILED; Emergency License Reset is the same action as POST /license/reset. The warning a run of failures produces is GRACE_EXPIRED, and heartbeatHealth.featuresDisabled in the status payload is always false. The fingerprint logged for the provisioning key is the first 8 characters of its SHA-256.
Two Sides Of One Heartbeat
The instance → store heartbeat (HMAC-signed, covered under License Verification) is what your instance sends. The endpoints here are what an admin can do on the instance itself: read the current state, kick the heartbeat manually, and reset. They never sign or verify anything — the signing happens inside the background service on every beat.
Mount
/license
Gate
manage_security
Beat Interval
5 minutes
The Endpoints
All five are mounted under /license in the user-management service and reached through the gateway at /api/v1/user-management/license. Every call needs an authenticated session. The read is granted to three permissions. Of the four writes, only sync is a working action for a manage_security holder: reset additionally requires the administrator role, and the other two are retired and always refuse.
| Method | Path | Purpose | Auth |
|---|---|---|---|
| GET | /api/v1/user-management/license | Current status, tier, entitlement flags, subscription state, seat/host figures, and the heartbeat strike/grace health. | manage_security | view_dashboard | view_users |
| POST | /api/v1/user-management/license/sync | Force one heartbeat now instead of waiting for the 5-minute beat; pull and write tier feature flags. | manage_security |
| POST | /api/v1/user-management/license/reset | Clear all license + provisioning state and set every store-mapped feature flag to off. | administrator role only |
| POST | /api/v1/user-management/license/provision | Retired. Always answers 410 — activation is automatic and store-managed. | manage_security |
| POST | /api/v1/user-management/license/heartbeat-policy | Disabled. Always answers 403 — the strike/grace thresholds are managed by Zonitix. | manage_security |
GET /license — Status
The one read. It takes no body and returns the instance’s licensing picture assembled from SecuritySetting rows: the handshake status (UNPROVISIONED → PROVISIONING_FAILED → PENDING_APPROVAL → ACTIVE), the resolved licenseType, the mapped features, host/seat figures from the last beat, the heartbeatHealth block, and the subscription block — the one that tells an admin a pause is coming before anything changes.
// Read current state. The session cookie is named __Host-token:
curl -s https://<your-instance>-api.zonitix.com/api/v1/user-management/license \
-b "__Host-token=<your-session-token>"
// Shape of the reply:
{
"status": "ACTIVE",
"licenseType": "Advanced",
"companyName": "Acme", "serviceId": "...", "hasProvisioningKey": true,
"features": { "reporting": true, "finance": true, "esignatures": true, "hris": true, "training": true, ... },
"lastHeartbeat": "2026-08-09T09:00:00.000Z",
"nextHeartbeat": "2026-08-09T09:05:00.000Z",
"heartbeatStatus": "SUCCESS",
"host": { "cpuCount": 4, "ramGb": 8 },
"seats": { "used": 37, "recommended": 96 },
"heartbeatHealth": {
"state": "OK", "consecutiveFailures": 0, "firstFailureAt": null,
"graceExpiresAt": null, "graceHoursRemaining": null,
"featuresDisabled": false,
"policy": { "warnStrikes": 3, "graceHours": 336 }
},
"subscription": {
"enforced": true, "state": "active", // active | grace | lapsed | unknown
"paidThroughAt": "2026-09-01T00:00:00.000Z",
"graceEndsAt": "2026-09-15T00:00:00.000Z",
"storeContactSinceExpiry": false, "premiumPaused": false
}
}
seats and host are informational: recommended is a sizing hint computed from the server’s CPU and memory, and nothing on the instance caps the number of users.
Invariant To Check
nextHeartbeat is derived as lastHeartbeat + 5 min, and heartbeatHealth.featuresDisabled is hard-coded to false — it describes the missed-heartbeat path only, which never disables anything. Whether paid modules are being withheld right now is a separate field: subscription.premiumPaused.
Calling These From A Script
The session cookie is __Host-token; there is no cookie called session. A POST that carries the cookie must also send an Origin header your instance trusts (its own web address), or the gateway answers 403 "CSRF check failed: missing or untrusted Origin." A request that authenticates with an Authorization header instead of the cookie is not subject to that check.
Activation Is Automatic — POST /license/provision Is Retired
There is no manual activation step. The provisioning key is injected into the instance as a secret when we build it; the background license service registers with it on its own and then polls until the store approves the instance. Watch GET /license: status reads PROVISIONING_FAILED while a key is present but registration has not yet succeeded, moves to PENDING_APPROVAL once the store has accepted the registration, and to ACTIVE only after the store approves the service and a later sync retrieves the permanent license key and secret. It does not go straight to ACTIVE.
The old endpoint for typing a key in by hand still exists so that the contract is explicit rather than silent, but it does nothing: every call answers 410 "Manual provisioning is disabled. This instance is activated automatically by the store." and writes a LICENSE_PROVISION_REJECTED audit line (warning) with the caller’s IP address. Whatever body you send is ignored.
POST /license/sync — Force A Heartbeat
Takes no body. Runs one heartbeat immediately rather than waiting up to five minutes for the next scheduled beat — the action to use after fixing outbound connectivity, or right after approval to pull the license down now. In the app this is the Force Sync Heartbeat button. The reply carries the store’s config object. Note that config is null when there is nothing to sync yet (no keys, or still waiting on approval), and the endpoint still returns 200.
// A cookie-authenticated POST needs a trusted Origin (see the note above):
curl -s -X POST https://<your-instance>-api.zonitix.com/api/v1/user-management/license/sync \
-b "__Host-token=<your-session-token>" \
-H "Origin: https://<your-instance>.zonitix.com"
// 200 on an active instance — config is the tier's feature set from the store:
{
"message": "Sync completed",
"config": { "tier": "Advanced", "reporting": "true", "finance": "true", ... }
}
// Audit line: LICENSE_SYNCED (info).
// Still 200 when nothing was synced (not activated yet, or the store did not answer or refused):
{ "message": "Sync did not complete", "config": null }
// Audit line: LICENSE_SYNC_NOT_COMPLETED (warning). Entitlements are left as they were.
What Sync Actually Writes
On a 200 heartbeat the instance first checks the store’s signature on the reply (see License Verification). A reply that fails that check is discarded and the instance keeps what it already holds. A reply that passes is persisted to SecuritySetting rows that the rest of the app reads:
- Feature flags. Every store config key is stored raw as
remote_<key>, and the mapped ones are also written to the local flag the feature routes actually enforce. The mapping is fixed — there is no dynamic key discovery:
// FEATURE_MAP — store key → local SecuritySetting flag (server-enforced):
security_lab → securityLabEnabled aws_mapping → awsMappingEnabled
compliance → complianceEnabled reporting → reportingEnabled
finance → financeEnabled
recruitment → recruitmentEnabled esignatures → esignaturesEnabled
alignsync → alignSyncEnabled hris → hrisEnabled
training → trainingEnabled
// Deliberately NOT mapped — will never be flipped by a heartbeat:
kill_switch (reserved — stored, never read), document_intelligence, analytics, mfa
training used to ride on the HR records flag. Until the store sends a training value of its own, GET /license reports features.training from hris.
- Tier + host figures.
license_typeis set fromlicenseTypeorconfig.tier; host CPU/RAM, recommended seats, and seats-used are saved for the status read. - Paid-through date. The subscription date inside a signed reply is saved; it is the only input to the lapse rule described below. An unsigned reply never writes it.
- Strike clock reset. A success clears the failure count, the first-failure marker, and the grace state back to
OK— one good beat is a full reset. - Agent trust bundle. If the store returned
latestTrustBundle, it is stored inSecuritySetting.agent_trust_bundle(see below). - Advertised agent releases. macOS/Linux/Windows releases are persisted only after their vendor signature verifies against the pinned key — the store is treated as a catalog, not a trust anchor. A bad or missing signature leaves the last-known-good untouched.
- Container reconcile request. A marker file is dropped for the host updater agent when an entitlement changed, and once on the first successful heartbeat, so that a new instance’s running services are trimmed to its plan. The agent starts or stops the matching containers with no redeploy.
Access Is Correct Before The Container Move
The container-sync marker carries no command, no service name, no path — only the fact that entitlements moved; the agent reads the entitlements itself and acts on a hardcoded command. Writing that marker is best-effort: if the updater agent is not installed the write fails, is logged loudly, and the heartbeat still succeeds. Entitlements are already applied the moment the flags were saved; only the running container set lags, and the next deploy reconciles it.
Strike / Grace Thresholds — Read-Only
Two thresholds decide how a run of missed heartbeats escalates. They are managed by Zonitix and cannot be changed from the instance; the License & Entitlements page shows them for reference, and GET /license returns them as heartbeatHealth.policy.
warnStrikes— consecutive failed beats before the connection is reported degraded. Default 3.graceHours— hours since the first failure before the grace window is reported expired. Default 336 (14 days).
POST /license/heartbeat-policy is intentionally disabled. Every call, whatever the body, answers 403 "Heartbeat policy is managed by Zonitix and cannot be changed from this instance." and writes a LICENSE_HEARTBEAT_POLICY_BLOCKED audit line (warning). Nothing is stored.
Worked Example: Strike → Warn → Degrade
Beats run every 5 minutes. Take the defaults, warnStrikes: 3, graceHours: 336, and suppose the store becomes unreachable at 09:00:
// Each failure increments a CONSECUTIVE strike count. State machine:
09:00 fail #1 count=1 state=OK (below warnStrikes)
09:05 fail #2 count=2 state=OK
09:10 fail #3 count=3 state=WARNING → escalation email to manage_security
... (keeps failing for ~14 days)
09:00 +336h grace expired → state=GRACE_EXPIRED → louder email
// At EVERY step: entitlements unchanged, featuresDisabled=false, nothing off.
09:00 next day ONE success → count=0, state=OK, grace clock cleared.
So warnStrikes sets how fast a blip becomes a WARNING (3 strikes ≈ 15 minutes at the 5-minute beat), and graceHours sets how long the instance may run unverified before the state becomes the loud GRACE_EXPIRED. Strikes must be consecutive — a single successful beat resets everything. The bias is deliberately toward the customer: the overwhelmingly likely cause of a failing heartbeat is our side (store down, DNS, blocked egress), so the strike policy only ever raises alarms, it never enforces.
Degraded ≠ Disabled
WARNING and GRACE_EXPIRED are notification states only. The strike logic changes no feature flag — the escalation email itself says “nothing has been disabled”. The instance keeps running on its last known-good entitlements; there is no code path where missing beats turn a feature off. A lapsed subscription is a different rule, described in the plain part above under What Happens When A Subscription Lapses; its status fields are next.
Subscription Lapse: Status Fields And Refusal
The rule itself is in the plain part above, under What Happens When A Subscription Lapses. These are the identifiers behind it.
- What counts as an answer from the store: an HTTP reply from the store counts, except the gateway-type statuses 502, 503, 504 and 520–530, which are treated as coming from the network in front of the store; a timeout does not count either.
- Whether the rule is in force: the status field
subscription.enforcedreports it. - During the 14 days:
subscription.statereadsgraceandsubscription.graceEndsAtholds the date the pause would begin. - After them:
subscription.premiumPausedistrue, and requests to plan-gated modules answer 402"Subscription lapsed". - Who sees the banner: administrators and anyone whose role holds
manage_security.
POST /license/reset — Clear State
Administrator role only. Holding manage_security is not enough: any other caller is refused with a 403, and the handler’s own second check answers 403 "Only an administrator can reset the licence." with a LICENSE_RESET_DENIED audit line (warning). In the app this is the Emergency License Reset button.
Takes no body. Wipes the license key, license secret, service id, provisioning key, auto-provision flag, last-heartbeat markers, and license type; clears the strike/grace clock (an unprovisioned instance is not “degraded”); and sets every store-mapped feature flag to an explicit false, so all license-gated features go dark. The flags are set to off rather than deleted because the feature routes treat a missing flag as allowed. It returns 200 "License reset successfully" and writes a LICENSE_RESET audit line (warning). The next verified heartbeat after the instance is activated again rewrites every flag with its real entitlements.
// As the administrator. A cookie-authenticated POST needs a trusted Origin:
curl -s -X POST https://<your-instance>-api.zonitix.com/api/v1/user-management/license/reset \
-b "__Host-token=<your-session-token>" \
-H "Origin: https://<your-instance>.zonitix.com"
// 200:
{ "message": "License reset successfully" }
Thresholds Survive A Reset
Reset clears license state but leaves the warnStrikes / graceHours thresholds alone — those are policy, not state. Unless Zonitix has set other values for your instance, they are the defaults, 3 and 336.
The Same Beat Carries Agent-Key Revocation
Licensing is not the only thing riding the store heartbeat. When the store answers a beat it may include latestTrustBundle — the vendor trust bundle that carries agent release-key revocation. Sync persists it verbatim into SecuritySetting.agent_trust_bundle, and device-service serves it to your enrolled agents on their own check-in. This is the path by which a compromised or retired agent signing key is revoked fleet-wide.
Fail-Open, Never Blocks Licensing
Agents Re-Verify, The Store Is Not Trusted
There Is No Remote Feature Kill-Switch
To be exact about scope: the trust bundle revokes agent signing keys, not your licensed features. The store’s kill_switch / remote-suspend flag is deliberately excluded from the feature map and is never heartbeat-enforced — a remote suspend that could brick a paying customer on a store glitch is more dangerous than the feature it guards. What does exist is the subscription-lapse pause above, which acts only on a signed paid-through date that is more than 14 days past.