What SentriKat sends home
A licensed SentriKat installation checks in with our license server every 12 hours. A Community Edition installation does not send it. Knowledge-base synchronization, described below, is a separate channel and applies to every edition that is not air-gapped. The license service answers on three names, license.sentrikat.com, api.sentrikat.com and portal.sentrikat.com: allow all three in your firewall. This page lists every field that check-in can carry, what each one is for, and how to read back exactly what we hold about your installation.
Last updated: 2026-10-09
The short version
From the license check-in: counters, versions and timestamps. From knowledge-base synchronization, unless you switch it off: the vendor and product names of software your installation could not identify, and the mappings you have confirmed. Never hosts, users, addresses or scan results. On-premises, scan results never leave your network. In the Cloud they are stored on our servers in the EU.
Every field, listed
This table is the contract, not a summary of it. What the server does with a field that is not on it depends on where the field is.
-
In the
kbblock, an unlisted field makes the server refuse the whole heartbeat with an error (HTTP 422). Inside a quality-signal entry, an unlisted key is discarded and logged instead. -
At the top level of the heartbeat, and among the
usagecounters, an unlisted key is ignored: it is not read, it is not stored, and the heartbeat is still accepted. -
In
offline_bundle, an unlisted key is discarded and logged. -
In
quality, a block in any other schema is discarded, a metric the server cannot read is dropped and counted, and the metrics it can read are stored as they arrived. - Dataset names follow a separate rule, below.
| Field | Type | What it is for | Example |
|---|---|---|---|
| installation_id | string | Identifies which of your installations is checking in. Generated by your instance on first start. | "SK-INST-7F3A9C21B4E05D8A6C13F27B90D4E5A8" |
| license_key | string | Authenticates the check-in. Your license, not your data. | "SK-XXXX-XXXX-XXXX-XXXX" |
| license_id | string (a UUID) | Identifies the license in place of license_key. A check-in carries one or the other. Your license, not your data. | "3f2b8c1e-5a7d-4c0e-9b1a-6d2f4e8a7c10" |
| edition | string | Which edition your instance believes it is running, so an upgrade applies without a re-activation. | "professional" |
| app_version | string | Which version you are on. Support's first question, and how we know a fix has reached you. | "1.4.2" |
| usage.agents | integer | How many agents are enrolled, checked against your seat count. A number, never a list. | 42 |
| usage.products | integer | How many products are tracked, checked against your plan. A number, never a list. | 137 |
| usage.organizations | integer | How many organizations the installation holds, checked against your plan. A number, never a list. The license service stores the agent and product counters. | 3 |
| usage.api_keys | integer | How many API keys exist, as a number, never the keys. The license service stores the agent and product counters. | 5 |
| kb.row_counts | map of dataset to integer | How many rows of each dataset your knowledge base holds. Lets us see that your instance has 40,000 vulnerabilities where it should have 300,000, without you running queries for us. Keys come from the fixed list below. | { "vulnerabilities": 312904, "osv_advisories": 296118 } |
| kb.cursors | map of dataset to timestamp | How far each incremental sync has reached. A cursor that has not moved in a week is the single most common cause of a stale knowledge base, and it is invisible from our side without this. | { "vulnerabilities": "2026-08-22T04:15:00Z" } |
| kb.last_sync_at | timestamp | When your instance last completed a sync. | "2026-08-22T04:15:00Z" |
| kb.last_sync_status | one of: ok, error, partial, never | How that sync ended. A fixed set of words, never a message: an error message is free text, and free text is where a URL with a token in it ends up. | "ok" |
| kb.last_sync_failed_dataset | dataset name | Which dataset failed, when one did. A name from the fixed list below, so support knows where to look. | "osv_advisories" |
| kb.bundle_version | string | For air-gapped instances that load offline bundles instead of syncing. Asking them when they last synced is the wrong question; this is the right one. | "2026-08-22" |
| kb.bundle_loaded_at | timestamp | When that bundle was loaded. | "2026-08-22T06:02:00Z" |
| kb.oracle_signals | one entry per internal quality check: {oracle_id, status, transition, at} | Whether our own internal quality checks pass on your data. status is one of ok/warn/fail/stale/unknown; transition is one of none/became_bad/recovered, computed by your installation, never re-derived here; at is when the check measured, not when this heartbeat was sent, and null means it has never run. Never a check result's content, only its status. An entry can also carry age_days, next_due and overdue, declared by your installation: how old the last measurement is, when the next one is due, and whether it is late. | { "oracle_id": "recall", "status": "ok", "transition": "none", "at": "2026-09-05T08:00:00Z" } |
| kb.oracle_signals_version | integer | Which version of the quality-signal format the entries above are in, so the shape can change later without either side guessing which one it is looking at. | 1 |
| offline_bundle.sequence | integer | The sequence number of the last offline bundle your installation imported. Only an installation that imports bundles reports it. | 12 |
| offline_bundle.outcome | string | How that import ended, as one of four words chosen by your installation: applicato (the bundle's sequence number was read and checked, so an older bundle would have been refused), assente (a bundle with no sequence number, normally an older one, so that check could not be made), illeggibile (the sequence number or the expiry date in the bundle could not be read: a defect on SentriKat's side, not in your installation, and the check was not made), inatteso (a recent bundle that should carry a sequence number and does not: a defect on SentriKat's side, and the check was not made). Stored as sent, cut at 64 characters. The last import only, replaced each time, never a history. An import that is refused is not reported here. | "applicato" |
| offline_bundle.generated_at | timestamp | When that bundle was generated, as written in the bundle. Stored as sent, cut at 64 characters. | "2026-10-07T06:00:00Z" |
| offline_bundle.imported_at | timestamp | When your installation imported it. Stored as sent, cut at 64 characters. | "2026-10-08T09:12:00Z" |
| quality.schema | string | Which version of the data-quality format the block is in. The only value read is sentrikat.quality-index.v1: a block with any other value is discarded and nothing from it is stored. | "sentrikat.quality-index.v1" |
| quality.misurato_a | timestamp | When the data quality was measured, not when this heartbeat was sent. | "2026-09-23T08:45:00Z" |
| quality.metriche | list of metrics | One entry per measured dimension of the quality of your knowledge base, made of the keys listed next. Counts and names only, nothing about your machines or your software. An entry the server cannot read is dropped and counted; an entry it can read is stored as it arrived. | [ { "nome": "copertura_identita", ... } ] |
| quality.metriche[].nome | string | The name of the dimension that was measured. | "copertura_identita" |
| quality.metriche[].num | number | The numerator of the measurement. | 680 |
| quality.metriche[].den | number | The denominator. The ratio is computed on our side and a finished score is never sent. | 1627 |
| quality.metriche[].metodo | one of: misurato, proxy, non_misurabile | How the value was obtained: measured, estimated through a proxy, or not measurable. | "misurato" |
| quality.metriche[].coperto_num | number | The numerator of the coverage: how much of the data the measurement could see. | 1627 |
| quality.metriche[].coperto_den | number | The denominator of the coverage: how much there was to see. | 1627 |
| quality.metriche[].critico | boolean | True when the sender marks this dimension as critical. | false |
| quality.metriche[].ferme | list of names | The names of what has stopped or is late within this dimension, when anything has. | [] |
| quality.coda | object of two integers | The size of the review queue: righe is how many rows are in it, etichette_umane is how many of them a person has already decided. | { "etichette_umane": 0, "righe": 732 } |
The fixed list of datasets
row_counts, cursors and
last_sync_failed_dataset accept these names and no
others. Without a closed list, "a map of dataset names to
timestamps" would be a map of anything to anything, and the
sentence above it would mean nothing.
A name outside this list is discarded. It is never stored, and it does not cost the rest of the heartbeat: the fields around it are kept. Your instance is told in the response which names were discarded, so a partly accepted report cannot be mistaken for a complete one, and we record alongside your state how many were discarded, as a number and never as the names themselves.
| Name | What it holds |
|---|---|
| vulnerabilities | CVE records |
| cpe_dictionary | Software identities used to match your inventory |
| exploit_intel | Known-exploited flags |
| epss | Exploitation probability scores |
| distro_advisories | Linux distribution security advisories |
| osv_advisories | Open-source package advisories |
| curated_mappings | Reviewed software-identity mappings |
| cna_ranges | Vendor-declared affected version ranges |
Knowledge-base synchronization
Every 12 hours, an installation of any edition that is not air-gapped synchronizes its knowledge base with ours. Each request carries the installation ID and the edition and, with a license, the first 64 characters of the license key. Our license service stores the mappings (first row) only from Professional licenses and refuses them from other editions; it stores the software identities (second row) from any valid license.
| What | Contents | Default | Switch off |
|---|---|---|---|
| Mappings you have learned | Up to 500 per run: vendor_pattern, product_pattern, cpe_vendor, cpe_product, confidence, source, usage_count. | On | SENTRIKAT_KB_SHARE_MAPPINGS=false |
| Software your installation could not identify | Up to 200 per run, only if the name has a CVE history: vendor, name, installs (a count) and cve_history (yes or no). No version, host or user. | On | The switch in the installation's interface, or SENTRIKAT_MAPPING_CONTRIBUTIONS=false |
| Mappings from the community and the curated ones | Downloaded to your installation. Nothing about you is in the download. | On | SENTRIKAT_KB_SYNC_ENABLED=false (this also turns off the first row; it does not turn off the second) |
What is never sent
Listed explicitly, because "we only collect what we need" is a sentence every vendor writes and none of them define.
- Any CVE found on one of your machines
- Any hostname, IP, or identifier of a machine you scanned
- Any file path, configuration, or SBOM
- Any user of your instance: no names, no email addresses, no credentials
- Any log line, stack trace, or free-text error message
- Any scan result, finding, ticket, or report
An example heartbeat
Nothing in this example is redacted: the values are made up and the field names are the real ones. It does not show these optional fields: license_id, usage.organizations, usage.api_keys, kb.last_sync_failed_dataset, kb.bundle_loaded_at, kb.oracle_signals, kb.oracle_signals_version, offline_bundle and quality. The table above lists every field.
POST https://api.sentrikat.com/api/v1/license/heartbeat
{
"installation_id": "SK-INST-7F3A9C21B4E05D8A6C13F27B90D4E5A8",
"license_key": "SK-XXXX-XXXX-XXXX-XXXX",
"edition": "professional",
"app_version": "1.4.2",
"usage": {
"agents": 42,
"products": 137
},
"kb": {
"row_counts": {
"vulnerabilities": 312904,
"cpe_dictionary": 1402883,
"osv_advisories": 296118,
"distro_advisories": 1469177
},
"cursors": {
"vulnerabilities": "2026-08-22T04:15:00Z",
"osv_advisories": "2026-08-22T03:25:00Z"
},
"last_sync_at": "2026-08-22T04:15:00Z",
"last_sync_status": "ok",
"bundle_version": null
}
}
Read back what we hold
The table above says what we collect. This command says what we actually have, for your installation, right now. Run it whenever you like; it needs no new credential beyond your license key and your installation ID.
curl -sS https://api.sentrikat.com/api/v1/license/telemetry \
-H 'Content-Type: application/json' \
-d '{"license_key":"YOUR-LICENSE-KEY","installation_id":"YOUR-INSTALLATION-ID"}'
Besides what the payload carried, the response shows what the license service records about the installation itself: its edition and version, when it was activated and last seen, the address its requests came from, and a hostname and operating system description only if a client has provided them (the current SentriKat release provides neither), the report of the last offline bundle, and the times at which the knowledge-base state and that report were received. The data-quality reports are not in this response: they are kept apart, under the installation ID alone. They are deleted 180 days after we receive them, and when the account is deleted (unless the same installation ID also appears on another customer's account); freeing a seat does not delete them. To see them, write to [email protected].
If the response contains something this page does not list, this page is what is wrong. Tell us at [email protected] and we will correct it.
Turning it off
The license check-in itself cannot be disabled: it is how a license stays valid, and it is processing necessary to perform our contract with you (Art. 6(1)(b) GDPR). Knowledge-base synchronization can be switched off, with the settings in the table above. Air-gapped instances send nothing at all, by construction, and are licensed offline.
Retention, your rights, and who processes what are covered in the Privacy Policy and the DPA.