The CLV1 protocol is now publicRead the spec
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:

  1. 7 days by default.
  2. On a workspace with offline licenses, the plan's offlineTtlSeconds sets 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, sending offlineTtlSeconds with 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

FieldMeaning
appApp id (app_…)
licLicense id (lic_…)
devSHA-256 (hex) of the device public key: the token only works on that machine
lexpLicense expiry (Unix seconds), null for lifetime
iatIssue time
expToken expiry: iat plus the offline window, never later than lexp
nbfSince CLV1.1: not valid before this time (equal to iat)
kidSince CLV1.1: id of the signing key, the first 16 hex characters of SHA-256 of the app public key
wmSince 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:

  1. Persist a stable device key (deviceKey), for example in the OS keychain or DPAPI. A new key on every run means dev never matches and every saved token is refused.
  2. Save offlineToken after every sign-in and every heartbeat, when it is not null.
  3. 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.
  • app names another app, or dev names another device key.
  • exp or lexp has passed.
  • iat or nbf is more than 60 seconds in the future, or the clock is more than 60 seconds behind a server time already seen.
  • maxOfflineSec is 0, or the token is older than maxOfflineSec seconds.

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 own exp.
  • 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 dev does 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 lexp set to null, so exp is simply iat plus 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.

How can we help?

Frequently asked

What is ClavisAUH?

A licensing and authentication service for desktop software: license keys, device binding, end-user sign-in and signed responses. All run from one dashboard

Why can't a cracker fake your server?

The SDK only trusts answers signed with your app's private key. That key never leaves our servers, so a fake server can't sign and gets rejected

What ends up in my binary?

Your app id and public key. Nothing secret, because each device makes its own key pair on first run

Does it work offline?

Yes. Every plan gets a signed offline token for 7 days, and your app checks it locally. Premium lets you choose how long. A revoked license can keep working offline for that time, unless Online-Only Mode is on

Which languages are supported?

JavaScript and TypeScript today. C++, C# and Python are next, on the same open protocol

Is Basic really free?

Yes. No time limit, no card. Move to Premium when you need more

Get in touch