License Activation API¶
API endpoints for license activation, used by SentriKat instances to validate and activate licenses.
These routes are on the license server, not on your own installation:
Errors from these routes carry a JSON body of the form {"detail": "..."}.
Online Activation¶
Exchange an activation code for a signed license key bound to your Installation ID.
Request Body¶
| Field | Type | Required | Description |
|---|---|---|---|
activation_code | string | Yes | Activation code (format: SK-XXXX-XXXX-XXXX-XXXX, 8-128 characters) |
installation_id | string | Yes | Installation ID from SentriKat (format: SK-INST- followed by 32 uppercase hexadecimal characters, 40 in all; the license server accepts 10 to 128 characters) |
app_version | string | No | SentriKat application version (e.g. 1.3.0, max 32 characters) |
Example Request¶
curl -X POST https://api.sentrikat.com/api/v1/license/activate \
-H "Content-Type: application/json" \
-d '{
"activation_code": "SK-XXXX-XXXX-XXXX-XXXX",
"installation_id": "SK-INST-<32 uppercase hex characters>",
"app_version": "1.3.0"
}'
Response (200 OK)¶
The license_key is a signed license string in the format base64url(JSON_payload).base64url(RSA_signature). The SentriKat instance verifies this signature using its embedded RSA public key.
Error Responses¶
| Status | Description |
|---|---|
400 | Invalid activation code format, invalid installation ID format, or the license is not active or has expired |
404 | Activation code not found, or its license no longer exists |
409 | Activation code has already been used, or the license has reached its maximum number of activations (the code is not consumed in that case) |
410 | Activation code has expired, or a newer code replaced it |
422 | A field is missing or outside its length limits |
429 | Rate limit exceeded |
500 | License signing unavailable |
Rate Limiting¶
Two limits apply to this endpoint:
- 5 requests per minute per IP address
- 10 activation attempts per hour per IP address
Every attempt counts toward the hourly limit, successful or not. A request over either limit gets 429 Too Many Requests.
Security Notes¶
- Activation codes are validated against the pattern
^[A-Za-z0-9\-]+$(8-128 characters) - Installation IDs must start with
SK-INST- - All activation attempts are logged (code, installation_id, IP, success/failure)
- One activation code = one installation (single use)
- Codes expire 90 days after they are issued (default)
Heartbeat¶
Periodic check-in from licensed SentriKat installations. The installation sends it every 12 hours. A Community Edition installation does not send it.
The heartbeat carries counters, knowledge-base state, a data-quality index and the outcome of the last offline bundle import. It never carries product lists, hostnames or CVEs. Every field is listed, with an example payload, at sentrikat.com/telemetry.
Request Body¶
| Field | Type | Required | Description |
|---|---|---|---|
installation_id | string | Yes | Installation ID |
license_key | string | No | The license key. Either license_key or license_id identifies the license |
license_id | string | No | The license UUID, accepted instead of license_key |
edition | string | No | Edition reported by the installation |
app_version | string | No | SentriKat application version |
usage | object | No | Counters: agents, products |
kb | object | No | Knowledge-base state: counters, versions and timestamps only. Unknown fields are rejected |
quality | object | No | Data-quality index. Parts the server cannot read are dropped, never rejected |
offline_bundle | object | No | Outcome of the last offline bundle import: sequence, outcome, generated_at, imported_at |
Response (200 OK)¶
{
"status": "active",
"message": "License is active",
"telemetry_stored": true,
"quality_stored": null,
"updated_limits": {
"max_users": null,
"max_organizations": null,
"max_products": null,
"max_agents": 10,
"max_storage_gb": null,
"features": ["ldap", "sso", "email_alerts", "..."]
},
"signed_license": "<base64url payload>.<base64url signature>",
"edition": "pro",
"timestamp": "2026-10-07T12:00:00"
}
| Field | Description |
|---|---|
status | active, suspended, revoked or expired. Every one of them is HTTP 200 |
reason | null when active. Otherwise the reason: license_revoked, license_missing, activation_missing, license_suspended or license_expired |
grace_until | Only with suspended: when the suspension becomes a revocation (UTC) |
message | Human-readable text |
updated_limits | Current limits and feature names. Present when the license is active |
signed_license | A re-signed license token, present only for the installation the license is bound to, so an edition change applies at the next heartbeat |
edition | Current edition |
telemetry_stored | true when the kb block was stored, false when it was discarded (no activation for this installation), null when no kb was sent |
kb_datasets_ignored | Dataset names in kb the server does not know. They were dropped and the rest was kept |
quality_stored | Same meaning as telemetry_stored, for the quality block |
quality_metrics_ignored | Number of metrics in quality that could not be read |
timestamp | Server time (UTC) |
What is stored¶
Per activation: last seen time, application version, the agents and products counters, the latest kb block (replaced whole, not merged), the latest offline bundle outcome, and one data-quality report for each new measurement (kept 180 days, and not part of the read-back below). Each heartbeat is also written to the activation log with the source IP address and user agent.
To see exactly what is held about one installation, call the read-back endpoint with the license key and 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"}'
It is limited to 30 requests per hour.
Update Check¶
Check for available SentriKat updates. Called periodically by SentriKat installations.
No authentication required, just the installation ID header.
Request Headers¶
| Header | Required | Description |
|---|---|---|
X-Installation-ID | Yes | Installation ID (e.g. SK-INST-<32 uppercase hex characters>). Without it the response is the full release detail shown in the customer portal, not the update check below |
X-App-Version | No | Current SentriKat version (e.g. 1.2.0-beta) |
Example Request¶
curl https://api.sentrikat.com/api/v1/releases/latest \
-H "X-Installation-ID: SK-INST-<32 uppercase hex characters>" \
-H "X-App-Version: 1.2.0-beta"
Response (200 OK)¶
{
"version": "1.3.0-beta",
"download_url": "https://api.sentrikat.com/api/v1/releases/1.3.0-beta/download",
"sha256": "<sha256 of the package>",
"image": "ghcr.io/<org>/sentrikat:1.3.0-beta",
"released_at": "2026-09-30T12:00:00+00:00",
"release_notes": "Bug fixes and performance improvements",
"update_available": true
}
Response (204 No Content)¶
Returned when no release has been published yet.
Notes¶
update_availableistruewhen the latest version is newer thanX-App-Version- If
X-App-Versionis not sent,update_availabledefaults tofalse - The portal logs each update check for analytics (installation, version, IP)
imageis the Docker image URL. Use this for Docker-based deploymentsdownload_urlis the tar.gz package on the portal, for offline/air-gapped deployments. It isnullwhen no package is available yetsha256is the checksum of that package: compare it before loading the image- Up to 30 checks per minute from one address; beyond that the answer is
429
Verify Signed License¶
Verify a signed license string from the portal (used by SentriKat instances).
Request Body¶
| Field | Type | Required | Description |
|---|---|---|---|
signed_license | string | Yes | The base64url(payload).base64url(signature) string |
installation_id | string | Yes | The requesting installation's ID |
Response (200 OK)¶
The answer is always HTTP 200. Read valid: a rejection is valid: false with a message, not an error status.
{
"valid": true,
"message": "License is valid",
"edition": "pro",
"customer": "Acme Corp",
"limits": {
"users": null,
"organizations": null,
"products": null,
"agents": 10
},
"features": ["ldap", "sso", "email_alerts", "..."],
"expires_at": "2027-02-08T00:00:00",
"issued_at": "2026-02-08T00:00:00"
}
features is a list of machine names, the same list the signed license carries (see License Payload Format).
Validation Rules¶
- The string must have the form
payload.signatureand the signature must be valid RSA-4096 (PKCS1v15 + SHA256) - The installation ID in the payload must be present and match the requesting installation
- The license must exist in the database (revoked licenses are rejected)
- The license must be ACTIVE status
- The license must not be expired
- The maximum activation limit is enforced. When the installation has no activation yet and the license is under its limit, this call records one
Limited to 10 requests per minute per IP address.
License Payload Format¶
The signed license payload contains:
{
"license_id": "<uuid>",
"license_key": "SK-XXXX-XXXX-XXXX-XXXX",
"customer": "Company Name",
"email": "[email protected]",
"edition": "pro",
"installation_id": "SK-INST-<32 uppercase hex characters>",
"issued_at": "2026-02-08T12:00:00",
"expires_at": "2027-02-08T12:00:00",
"limits": {
"max_users": null,
"max_organizations": null,
"max_products": null,
"max_agents": 10,
"max_agent_api_keys": 10
},
"features": ["ldap", "sso", "email_alerts", "webhooks", "white_label", "api_access", "audit_export", "multi_org", "jira_integration", "siem_integration", "sbom_export", "compliance_reports", "air_gap", "scheduled_backups"]
}
features is a list of machine names. Professional carries the names above; Community carries none (its quotas are limits, not features). Paid add-ons such as compliance_pack appear only on a license that includes them.
The payload is JSON-encoded, base64url-encoded, then RSA-signed. The full license string is: