Architecture & OperationsHeartbeat (License Check-In)
On this page0

Unfamiliar word? See Glossary: words used in these docs. Looking for the tasks in order? Begin at Start Here.

Reference

Heartbeat (License Check-In)

The heartbeat is the check-in your instance sends to the Zonitix licensing server (the store) every five minutes. The answer tells your instance which features your plan includes.

When Your Instance Cannot Check In

The check-in runs automatically in the background. If your instance cannot reach the store, nothing is switched off: the list of features from the last successful check-in is already saved in your instance’s own database and stays in force. The failed attempt is recorded, and the next successful check-in refreshes the list. A network interruption on its own never removes a feature. Features change when the store returns a new, verified answer. Separately, paid modules pause once a subscription has been lapsed for more than 14 days and the store has answered your instance since; the exact rule is under What Happens When A Subscription Lapses, further down this page.

License Check Interval

License check-ins are scheduled once every 5 minutes (separate from directory sync, which runs on the schedule you configure). This is the same beat the Deployment Health tab uses to show your instance as online. Each beat names your instance (license key, server name, your customer id and company name, the software version) and carries counts: how many users you have and, where you run our device agents, how many devices are on each agent version. It also carries a short report on which release the server runs and how its last update went, and one public key that lets the server fetch releases. It holds no per-user or per-device records and no employee or business data. Every field is listed under Technical detail below.

Failure Visibility

Every failed check-in writes an entry to your audit log with the underlying error, and the time and result of the last check-in are stored on the instance, so administrators can see exactly when contact was lost.

What Happens When A Subscription Lapses

Every verified reply carries a signed paid-through date. Paid modules pause only once that date is more than 14 days past and the store has answered your instance since that date; your data and the core features stay available throughout, and an instance that cannot reach the store is never paused by this rule. The full rule, what stays available, who is warned and how it is restored are on License Administration.

When A Module Leaves Your Plan

A plan change arrives as a verified reply that switches a module’s flag off. From then on the module’s pages and API refuse the request, and its scheduled background work stops running as well: scheduled report runs, compliance evidence collection, trend snapshots and digests, AlignSync automatic reconciliation and SaaS discovery, Security Lab scans and the vulnerability feed, cloud-mapping syncs, finance budget alerts and digests, and new recruitment interview reminders. A job is skipped only when its flag is explicitly off; a flag that has never synced or cannot be read leaves the job running.

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.

Live Heartbeat Protocol

// 1. Generate a payload with a FRESH timestamp (unix seconds)

const payload = { "timestamp": 1781756000, "licenseKey": "MS-...", ... }

// 2. Sign the ordered fields with SHA256-HMAC using your License Secret

// (all 10 fields are signed; a 7-field signature is refused — see License Verification)

const signature = crypto.createHmac('sha256', secret).update(JSON.stringify(dataToSign)).digest('hex');

// 3. Send it with a fresh random downlinkNonce (outside the signature)

// 4. The store's reply is signed too. The instance verifies it, then applies the features

Response: { "licenseType": "Advanced", "config": { "aws_mapping": true, ... },

"configSignature": "...", "configSignatureV2": "...",

"downlink": { "v": 2, "issuedAt": 1781756001, "seq": ..., "nonce": "<echoed>" } }

Everything One Beat Carries

This is the whole request, field by field. The first table is the signed part: ten values, signed together in this order (metrics is one value with up to four parts). The request also has one header, X-MS-Signature, which holds the signature.

FieldWhat it holds
serviceIdThe id the store gave this instance when it was provisioned.
licenseKeyYour license key. It selects the license whose secret the signature is checked with.
hostnameThe host name the licensing program sees on your server.
ipAddressAlways the fixed text 127.0.0.1. The instance does not put its real address in the payload.
customerIdYour customer id, set when the server was provisioned.
companyNameYour company name, set when the server was provisioned.
timestampThe send time, in whole seconds (Unix time).
appVersionThe version of the Zonitix software the instance runs.
userCountOne number: how many rows the user table holds, whatever their status. No names or addresses.
metrics.fleetOnly where you run our device agents. For each platform and each agent version: how many enrolled devices are on it (total) and how many of those checked in recently (healthy; within the last 45 minutes unless the server is configured otherwise). Counts only; no device names, serial numbers or owners. Null when the count could not be read.
metrics.updateSent only when the instance has an update report. A short report: the release name it runs (in the form v1.2.3), the channel (stable or main), two true/false flags copied from the server’s update settings, and the result of the last update. That result is empty, or four values: one word from a fixed list (such as applied, up-to-date, rolled-back, failed or timed-out), the release names after and before it, and the time.
metrics.subscriptionLapseSent only after your subscription has ended (in the 14 days after the paid-through date, or later). A short report: whether the instance is in grace or past it, the paid-through date, the date the grace period ends, two true/false values (whether pausing is switched on, and whether premium features are paused now) and two lists of plan feature names: those paused now and those that will pause when grace ends. A paid-up instance does not send it.
metrics.deployKeySent only when your server has published one. The public half of an SSH key pair made on your server, in the exact form ssh-ed25519 followed by 68 characters; anything else is not sent. The private half stays on the server host and is not given to any container.

Four more values travel in the same request outside the signature:

FieldWhat it holds
telemetry.seatsUsedOne number: active user accounts, not counting the Zonitix support account.
telemetry.hostThe host name again, the number of CPU cores and the memory in whole gigabytes.
telemetry.recommendedSeatsA number computed from the CPU and memory figures.
downlinkNonceA fresh random value. The store repeats it inside its signed reply, so a reply can only answer the request that asked for it.

Nothing else is in the request. It holds no user names, no email addresses, no device records, no documents and no audit-log entries.

What The Store Keeps

  • On your instance’s record, the latest value of: host name, the address field as sent, customer id, company name, app version, user count, the metrics.fleet counts, the metrics.update report and the metrics.subscriptionLapse report (cleared when a beat no longer carries one). Each beat overwrites the one before.
  • The metrics.subscriptionLapse report is also kept from a beat the store refuses because the subscription ended, so the Zonitix team can see which features your instance has paused.
  • One history row per beat with the time, the address field, the host name, the app version and the user count.
  • The telemetry group is not kept: the store’s request check drops any field it does not name, and it does not name that one.
  • Any other part of metrics is dropped before anything is written.

Verifying A Signature And The Settings Table

A failed heartbeat is written to the audit log as LICENSE_SYNC_FAILED. A five-minute timer runs the heartbeat and updates the instance’s local license settings (key/value rows in SecuritySetting). The signature is an HMAC-SHA256, hex-encoded, over the JSON of a ten-element array in a fixed order. It is not computed over the payload object, so a verifier must rebuild the same array:

// Verifying a heartbeat signature (Node.js):

const crypto = require('crypto');

function verifyHeartbeat(payload, signature, secret) {

// the ten signed fields, in this order; a missing value is signed as null

const dataToSign = [

payload.serviceId, payload.licenseKey, payload.hostname, payload.ipAddress,

payload.customerId, payload.companyName, payload.timestamp,

payload.appVersion ?? null, payload.userCount ?? null, payload.metrics ?? null,

].map((v) => (v === undefined ? null : v));

const expected = crypto.createHmac('sha256', secret)

.update(JSON.stringify(dataToSign)).digest('hex');

const a = Buffer.from(signature, 'utf8');

const b = Buffer.from(expected, 'utf8');

// length check first — timingSafeEqual THROWS on a size mismatch

return a.length === b.length && crypto.timingSafeEqual(a, b);

}


// SecuritySetting model (Sequelize, table: security_settings):

sequelize.define('SecuritySetting', {

key: { type: DataTypes.STRING, primaryKey: true },

value: { type: DataTypes.JSONB },

description: { type: DataTypes.TEXT, allowNull: true }

}, { tableName: 'security_settings', timestamps: true });

What Every Beat Must Satisfy

The store applies these checks in the order listed. Every failure answers JSON with a machine-readable code. Only the license row is a verdict about your license; every other 4xx is about the request or a pending approval, and 503 is a fault on our side. Signature construction itself is documented in full on the License Verification page.

RuleFailure
No more than 10 check-ins a minute from one IP. Applied before any other check, so the license was not evaluated: back off and retry.429 · RATE_LIMITED
timestamp (unix seconds) within 300s of store time. The license is not evaluated; the response carries serverTime so you can see the drift.403 · CLOCK_SKEW
X-MS-Signature present.401 · MISSING_SIGNATURE
A license with this key exists and the signature matches the 10-field payload. Seven-field signatures are refused. An unknown key gets the same answer as a bad signature, so the endpoint does not reveal which keys exist.403 · INVALID_SIGNATURE
License ACTIVE, account open, instance approved, subscription unexpired. Checked only after the signature verified, and before the approval and timestamp rows below. The refusal carries a revocation signed with your license secret, so it cannot be forged in transit or replayed. The instance does not act on the refusal itself: it keeps the entitlements it already holds until the subscription-lapse rule applies (What Happens When A Subscription Lapses, in the plain part above).403 · LICENSE_INACTIVE, ACCOUNT_CLOSED, SERVICE_INACTIVE, SUBSCRIPTION_EXPIRED
A re-provision of the instance is awaiting operator approval. Not a verdict about your license: no revocation, keep what you hold and retry.403 · SERVICE_PENDING
timestamp strictly greater than the last one accepted for this license.403 · STALE_TIMESTAMP
A fault on our side (database, configuration). Not a verdict about your license; retry on the normal schedule.503 · STORE_UNAVAILABLE

The monotonic-timestamp rule is a replay defence, and it has one consequence worth planning for: a retry must be re-signed with a new timestamp, never re-sent byte-for-byte. The baseline it compares against is set the moment your instance is provisioned, so the very first heartbeat after the handshake also has to be later than that — on a server with a correct clock this is automatic.

What A Removed Module Answers

When a module leaves your plan (described in the plain part above, under When A Module Leaves Your Plan), its pages and API refuse with a 403.

config.retrievalToken Is Not Returned

The heartbeat response hands back config wholesale, which meant the credential that exchanges for your plaintext license secret was being re-broadcast to anything that could read a response body — a proxy log, a TLS-terminating middlebox, a compromised instance — every five minutes. It is now stripped from the response. Any tooling that read the retrieval token out of a heartbeat reply will find it undefined and must stop doing so; the token belongs only to the provisioning handshake. Nothing else in config changed — feature flags and kill_switch are returned exactly as before.

Related reference