Browse the library
Documentation libraryLicensing
Offline licenses
Signed offline tokens keep your software running on a machine that loses the network, for 7 days by default or a window you choose on Premium, while still refusing to work on another machine or to be stretched by turning the clock back.
7 min read
What an offline token is
Every successful sign-in and every heartbeat carries an offlineToken field, on every plan. It is a compact string, clv1.<payload>.<signature>: the payload is base64url-encoded JSON, and the signature is an Ed25519 signature over clv1.<payload>, made with your app's signing key. The SDK checks it entirely on the machine, with the public key embedded in your build, without calling the server.
An offline token is a temporary pass for when the network is gone, not a replacement for signing in. When the machine is online, your software signs in and heartbeats as usual, and each of those calls hands back a fresh token. Every plan has it, Basic included, in both protection modes. The Signed Offline License method adds the choice of how long: you set the length on the license plan, and that needs Premium.
When the server issues a token
The server fills offlineToken on every plan, Basic included. The field is null only while Online-Only Mode is on, which exists so a revocation always lands. This switch is in App security › Protection, tab Methods, in the License Enforcement group, and it only runs in Advanced Protection.
How long a token lasts:
- 7 days by default.
- On a workspace with offline licenses, the plan's
offlineTtlSecondssets the length instead. By default that is a Premium feature; check Account & organization › Plan & usage, where the Offline licenses row should read Included.
So Premium does not mean "offline at all", it means "you choose how long".
A new token is issued after every successful activate, trial, register, login and redeem, and after every heartbeat. The offline window therefore always counts from the last time the machine reached the server, not from the first activation.
Setting the offline window on a license plan
The offline window is the plan's offlineTtlSeconds field, in seconds, from 3600 (1 hour) to 7776000 (90 days); null keeps the 7 days. The plan form in Licensing & releases › Sign-in & license plans (the Add plan button, or click a plan to edit it) has no input for this field yet, so you set it through the dashboard API, signed in with the Admin or Owner role:
- New plan:
POST /v1/apps/<appId>/policies, sendingofflineTtlSecondswith the plan's other fields. - Existing plan:
PATCH /v1/apps/<appId>/policies/<policyId>with a body like this one, here 3 days:
{ "offlineTtlSeconds": 259200 }
Clicking Save plan in the form later keeps the value, because the form does not send that field. On a workspace without offline licenses, creating a plan with an offline window is refused with plan_limit, and a plan that already has a value gets 7-day tokens until the workspace has the feature.
What the token contains
| Field | Meaning |
|---|---|
app | App id (app_…) |
lic | License id (lic_…) |
dev | SHA-256 (hex) of the device public key: the token only works on that machine |
lexp | License expiry (Unix seconds), null for lifetime |
iat | Issue time |
exp | Token expiry: iat plus the offline window, never later than lexp |
nbf | Since CLV1.1: not valid before this time (equal to iat) |
kid | Since CLV1.1: id of the signing key, the first 16 hex characters of SHA-256 of the app public key |
wm | Since CLV1.1: buyer watermark, present only when Forensic Watermarking is on |
wm looks like <10 hex>.<6 hex>: the first half names the license, the second the machine, and it matches the grant.license.mark you get online. With Forensic Watermarking on, you can show the mark in an About box or a log header even offline. The plan's grace days (Grace after the end (days)) only apply online: an offline token never outlives the license.
Verifying with the SDK
verifyOffline(token?) returns the payload when the token is valid and null otherwise. Without an argument it uses the latest token the SDK holds in memory. To make offline use survive restarts:
- Persist a stable device key (
deviceKey), for example in the OS keychain or DPAPI. A new key on every run meansdevnever matches and every saved token is refused. - Save
offlineTokenafter every sign-in and every heartbeat, when it is notnull. - Fall back to
verifyOffline()only on network failures, never when the server answered with a refusal.
import { ClavisClient, ClavisError, hashComponent } from "@clavisauh/sdk";
const clavis = new ClavisClient({
baseUrl: "https://api.example.com",
appId: "app_…",
appPublicKey: "…",
device: { fingerprint: hashComponent(machineId) },
deviceKey: await loadDeviceKey(), // the same key on every run
security: { maxOfflineSec: 3 * 86400 }, // optional: never trust a token older than 3 days
});
async function unlock(licenseKey: string) {
try {
await clavis.init();
const grant = await clavis.activate(licenseKey);
if (grant.offlineToken) await saveToken(grant.offlineToken);
return { online: true, licenseId: grant.license.id };
} catch (err) {
// status 0 means no answer arrived: network_error or timeout
if (err instanceof ClavisError && err.status !== 0) {
// The server answered, or its answer failed verification: do not go offline
if (clavis.revoked) await deleteToken();
throw err;
}
// Network failure: use the saved token
const payload = clavis.verifyOffline(await loadToken());
if (!payload) throw new Error("Connect to the internet to verify your license");
return { online: false, licenseId: payload.lic, until: payload.exp };
}
}
setInterval(() => {
clavis
.heartbeat()
.then((hb) => (hb.offlineToken ? saveToken(hb.offlineToken) : undefined))
.catch(() => undefined); // offline: keep the previous token
}, 30 * 60_000);
loadDeviceKey, saveToken, loadToken and deleteToken are your own functions. verifyOffline() returns null when:
- There is no token, or the license was reported revoked earlier in this process.
- The signature does not come from a key in the build's
appPublicKey/appPublicKeys. appnames another app, ordevnames another device key.exporlexphas passed.iatornbfis more than 60 seconds in the future, or the clock is more than 60 seconds behind a server time already seen.maxOfflineSecis0, or the token is older thanmaxOfflineSecseconds.
Clock rewind protection
Pirates often turn the clock back to stretch a token. The SDK catches this in two ways. First, a token whose iat or nbf is more than 60 seconds in the future is refused: the machine claims a time before the token was even issued, so its clock was turned back. Second, within one process, the SDK remembers the latest server time from verified answers and refuses tokens once the clock runs more than 60 seconds behind it. The 60-second margin absorbs ordinary clock drift.
When you detect a rewind, you can report it once the machine is back online and signed in again. The report needs a valid session and raises an Offline clock rollback signal in App security › Protection; it only raises the risk score, never blocks anyone on its own, and at most 5 reports per session per hour are counted.
await clavis.request("/v1/client/report", { kind: "offline_rewind" });
The maxOfflineSec limit
maxOfflineSec is an extra cap on a token's age, counted from iat:
null: no extra cap, only the token's ownexp.0: never trust offline tokens.- A number of seconds N: refuse tokens issued more than N seconds ago.
It has two sources. Your build sets its own value with security.maxOfflineSec when it creates the ClavisClient. The server sends a hint in the signed init() answer (securityHints.maxOfflineSec): 0 when Online-Only Mode is on, null when it is off. The SDK keeps whichever is stricter, so no server, a fake one included, can loosen what your build sets. Read the value in force from clavis.security.maxOfflineSec. The server's hint only applies after init() succeeds in that process.
Revocation and offline tokens
An offline token carries everything it needs, so the server cannot call it back. When you revoke or suspend a license, all of its sessions end at once and the next heartbeat gets session_expired. When your software signs in again on that machine, the server answers license_revoked or license_suspended; the SDK sets clavis.revoked to true, drops its in-memory token, and verifyOffline() returns null for the rest of the process.
That flag lives in memory only, so delete your saved token when you see it (as the example does), or the old token will work again after a restart. A machine that never comes back online keeps running until exp: the offline window, 7 days by default, is the longest a revocation can take to reach a disconnected machine.
Key rotation
Tokens carry kid, and the SDK verifies with the matching key from appPublicKeys; older tokens without a kid (issued before CLV1.1) are tried against every trusted key. A build that only knows the new key refuses tokens signed by the old one, and the user has to go online once for a new token. Ship a build that carries both keys before you activate the new one.
Other things to know
- A token copied to another machine is refused, because
devdoes not match. - Reinstalling the OS and losing the device key makes the saved token useless; the machine must go online and sign in again.
- A lifetime license has
lexpset tonull, soexpis simplyiatplus the offline window. - Moving the clock forward only makes the token expire sooner, which helps nobody.
- The exact token format is in the protocol specification.