Keyless
/{tenantId}/{productId}/keyless Anonymous state beacon for devices running without an active license — trial, free tier, or expired. Uses the common headers & path parameters.
Request body
Section titled “Request body”| Field | Type | Required | Description |
|---|---|---|---|
instance_id | string | yes | A stable anonymous device id (≤128 chars) |
state | string | no | "trial", "free_tier", or "expired" (default "free_tier") |
app_version / sdk_version / platform / sdk | string | no | Telemetry — recorded per device and shown on the dashboard’s Free-tier devices table. platform is the operating system (macos, windows, linux, ios, …), or the runtime (web, workers) where no OS applies; sdk is which SDK is reporting (swift, rust, js, csharp, cpp). sdk_version/platform/sdk are sent automatically by the SDKs; app_version is included only if you set it (e.g. the Rust SDK’s .app_version(…)). |
curl -X POST https://api.keylight.dev/acme/notes/keyless \ -H "Content-Type: application/json" \ -H "X-Keylight-SDK-Key: $KEYLIGHT_SDK_KEY" \ -d '{ "instance_id": "device-abc123", "state": "trial" }'await kl.reportKeylessState("trial");Response (200)
Section titled “Response (200)”{ "received": true, "free_tier_enabled": true, "trial_duration_days": 14, "kid": "k1", "issued_at": 1788671279, "expires_at": 1788757679, "signature": "base64…", "trial_started_at": 1788671279, "trial_already_used": false}The beacon is how an unlicensed device learns the settings your dashboard owns — it is the keyless counterpart to the settings that ride validate for licensed ones.
| Field | Type | Description |
|---|---|---|
received | boolean | Always true on success. |
free_tier_enabled | boolean | Always present. |
trial_duration_days | number | Absent when the tenant has never configured one — keep your own default. 0 means trials are off. Absent and 0 are different values. |
kid / issued_at / expires_at / signature | The signature over the two settings above. Present whenever trial_duration_days is. Byte-identical to the GET /config envelope — verify it the same way. It covers the settings only; the per-device trial_started_at and trial_already_used are outside it. | |
trial_started_at | number | Unix seconds. When this device first started a trial for this product, server-side. Never moves, so a device that wiped its local store gets its original date back rather than today’s. |
trial_already_used | boolean | true only when this device spent a trial that has since run out. |
Treat the trial fields as optional
Section titled “Treat the trial fields as optional”trial_started_at and trial_already_used are both absent unless the server has a live ledger entry for this device and this beacon was the one that refreshed it. They are absent when you send no machine_hash, when the device has never trialled, when its entry has aged out of the 18-month window, and on beacons the server answers without a database read — the write is gated so cadence can rise without cost.
So absence means no opinion, not false. Leave your cached values alone when they do not arrive; do not read a missing trial_already_used as permission, or a missing trial_started_at as “starts now”.
The ledger grants far more than it denies
Section titled “The ledger grants far more than it denies”trial_already_used is deliberately hard to make true. A device inside its first trial, one with no machine_hash, one whose entry expired, and — importantly — any device behind a machine_hash shared by an implausible number of installs all answer false. That last case is cloned VM images and imaged laptop fleets, which share a machine id: an unusual number of installs raises an operator alert and grants, never locks out.
A wrongly granted second trial costs one trial’s usage. A wrongly denied one costs a customer, and denials land on legitimate users in bulk. The asymmetry is the design.
Errors
Section titled “Errors”Same as Free tier (400, 401, 404, 429).