DOCS LLMs

Offline Licensing

Offline Licensing

Generate cryptographically signed tokens for license validation without internet access.

POST /api/v1/products/{slug}/licenses/{key}/offline_token

Use Cases

Air-gapped environments — Secure facilities, government systems, industrial equipment where network access is restricted or prohibited.

Network resilience — Allow your app to work during internet outages with a configurable grace period (e.g., valid for 7 days offline).

Reduced latency — Skip network round-trips by validating locally. Useful for apps that check licenses frequently.

Embedded systems — IoT devices, kiosks, or appliances that may not have reliable connectivity.

How It Works

1. Generate token (online)     2. Verify locally (offline)
┌─────────────┐                ┌─────────────────────────┐
│ Your App    │                │ Your App                │
│ or Server   │                │                         │
└──────┬──────┘                │  ┌─────────────────┐    │
       │                       │  │ Verify Ed25519  │    │
       │ POST /offline_token   │  │ signature with  │    │
       ▼                       │  │ embedded key    │    │
┌─────────────┐                │  └────────┬────────┘    │
│ LicenseSeat │───────────────▶│           │             │
│ API         │  Signed token  │  Check exp, entitlements│
└─────────────┘                └─────────────────────────┘

Request

Parameter Required Description
device_id Conditional Required for hardware_locked licenses. Binds token to specific device.
ttl_days No Token lifetime in days (default: 30, max: 90)
curl -X POST https://licenseseat.com/api/v1/products/my-app/licenses/LICENSE-KEY/offline_token \
  -H "Authorization: Bearer pk_live_xxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"device_id": "device-uuid", "ttl_days": 30}'

Response

{
  "object": "offline_token",
  "token": {
    "schema_version": 1,
    "license_key": "LICENSE-KEY",
    "product_slug": "my-app",
    "plan_key": "pro",
    "mode": "hardware_locked",
    "seat_limit": 3,
    "device_id": "device-uuid",
    "iat": 1737504000,
    "exp": 1740096000,
    "nbf": 1737504000,
    "license_expires_at": null,
    "kid": "org-xxx-offline-v1",
    "entitlements": [
      { "key": "pro", "expires_at": null }
    ],
    "metadata": {}
  },
  "signature": {
    "algorithm": "Ed25519",
    "key_id": "org-xxx-offline-v1",
    "value": "base64url_signature..."
  },
  "canonical": "{\"device_id\":\"device-uuid\",...}"
}

Token Fields

Field Description
iat Issued at (Unix timestamp)
exp Token expiration (Unix timestamp) — when offline validation stops working
nbf Not before (Unix timestamp)
license_expires_at License expiration (Unix timestamp, null = perpetual)
kid Key ID for fetching the public verification key
entitlements Snapshot of entitlements at generation time

Verification

SDKs handle this automatically. The JavaScript, Swift, C#, and C++ SDKs all include built-in offline verification with clock tamper detection.

For direct API integration, verify manually:

  1. Get public key from /signing_keys/{kid} (or embed at build time)
  2. Verify Ed25519 signature over the canonical string
  3. Check now > nbf and now < exp
  4. Check device_id matches (for hardware_locked)
  5. Check entitlements as needed

Python Example

from nacl.signing import VerifyKey
import time

PUBLIC_KEYS = {
    'org-xxx-offline-v1': b'...'  # Embed at build time
}

def verify_offline_token(token_data):
    sig = token_data['signature']
    canonical = token_data['canonical']

    # Verify signature
    key = VerifyKey(PUBLIC_KEYS[sig['key_id']])
    key.verify(canonical.encode(), base64url_decode(sig['value']))

    # Check timing
    now = int(time.time())
    token = token_data['token']

    if now < token['nbf']:
        raise ValueError('Token not yet valid')
    if now > token['exp']:
        raise ValueError('Token expired')

    return token

Clock Tamper Detection

Users can bypass expiration by rolling back their system clock. Mitigate this:

LAST_SEEN_KEY = 'last_seen_ts'

def check_clock_tamper(storage):
    now = int(time.time())
    last_seen = storage.get(LAST_SEEN_KEY, 0)

    # Clock went backwards by more than 5 minutes
    if now + 300 < last_seen:
        return True  # Tamper detected

    storage[LAST_SEEN_KEY] = now
    return False

SDKs implement this automatically.

Error Codes

Code HTTP Meaning
license_not_found 404 License doesn't exist
expired 422 License has expired
revoked 422 License has been revoked
signing_not_configured 400 Offline licensing not enabled

Best Practices

  1. Embed the public key at build time — don't fetch at runtime for true offline support
  2. Refresh tokens when online — capture entitlement changes, extend validity
  3. Use canonical directly — don't reconstruct JSON, use the provided string
  4. Set appropriate TTL — balance security (shorter) vs. offline duration (longer)

See Also

  • Signing Keys — Fetch public verification keys
  • SDKs — Built-in offline support in all SDKs