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:
- Get public key from
/signing_keys/{kid}(or embed at build time) - Verify Ed25519 signature over the
canonicalstring - Check
now > nbfandnow < exp - Check
device_idmatches (for hardware_locked) - 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
- Embed the public key at build time — don't fetch at runtime for true offline support
- Refresh tokens when online — capture entitlement changes, extend validity
- Use
canonicaldirectly — don't reconstruct JSON, use the provided string - 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