Skip to content

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:

https://api.sentrikat.com/api/v1

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.

POST /api/v1/license/activate
Content-Type: application/json

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)

{
  "license_key": "<base64url payload>.<base64url signature>"
}

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.

POST /api/v1/license/heartbeat
Content-Type: application/json

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.

GET /api/v1/releases/latest

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_available is true when the latest version is newer than X-App-Version
  • If X-App-Version is not sent, update_available defaults to false
  • The portal logs each update check for analytics (installation, version, IP)
  • image is the Docker image URL. Use this for Docker-based deployments
  • download_url is the tar.gz package on the portal, for offline/air-gapped deployments. It is null when no package is available yet
  • sha256 is 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).

POST /api/v1/verify-signed
Content-Type: application/json

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

  1. The string must have the form payload.signature and the signature must be valid RSA-4096 (PKCS1v15 + SHA256)
  2. The installation ID in the payload must be present and match the requesting installation
  3. The license must exist in the database (revoked licenses are rejected)
  4. The license must be ACTIVE status
  5. The license must not be expired
  6. 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:

base64url(json_payload).base64url(rsa_signature)