# LicenseSeat Documentation > Complete documentation for LicenseSeat - Easy, secure licensing server and license keys for your games, plugins, and apps ## Documentation - [LicenseSeat Documentation](/) - [Prerequisites](/getting-started-1-prerequisites/) - [Quickstart](/getting-started-2-quickstart/) - [Activate Device](/api-reference-activate/) - [Authentication](/api-reference-authentication/) - [Deactivate Device](/api-reference-deactivate/) - [Generate Download Token](/api-reference-download-token/) - [Error Codes](/api-reference-errors/) - [API Reference](/api-reference/) - [Get Latest Release](/api-reference-latest-release/) - [List Releases](/api-reference-list-releases/) - [Offline Licensing](/api-reference-offline-token/) - [Get Signing Key](/api-reference-signing-keys/) - [Validate License](/api-reference-validate-license/) - [C++ SDK](/sdks-cpp/) - [C# SDK](/sdks-csharp/) - [Interactive Demo](/sdks-demo/) - [SDKs](/sdks/) - [JavaScript SDK](/sdks-javascript/) - [Swift SDK](/sdks-swift/) --- ## Full Content ## LicenseSeat Documentation # LicenseSeat Documentation LicenseSeat is a licensing server for games, plugins, and apps. Issue license keys, validate them in your software, and manage activations—all without rolling your own infrastructure. ## What You'll Learn - **Getting Started**: Set up your first product and issue licenses in 15 minutes - **Official SDKs**: Drop-in libraries for Swift and JavaScript - **API Reference**: Complete documentation for all endpoints - **Integration Guides**: Code examples and best practices - **Offline Licensing**: Cryptographically signed offline validation ## Quick Links | Getting Started | SDKs | Reference | | -------------------------------------------------- | ----------------------------------- | ---------------------------------------------------- | | [[getting-started/1-prerequisites\|Prerequisites]] | [[sdks/swift\|Swift SDK]] | [[api-reference/authentication\|Authentication]] | | [[getting-started/2-quickstart\|Quickstart]] | [[sdks/javascript\|JavaScript SDK]] | [[api-reference/validate-license\|Validate License]] | ## Official SDKs We provide official SDKs for the most popular platforms: | Platform | Package | Install | | ------------------------- | ------------------- | -------------------------------------------------------------------- | | **Swift** | `licenseseat-swift` | `swift package add https://github.com/licenseseat/licenseseat-swift` | | **JavaScript/TypeScript** | `@licenseseat/js` | `npm install @licenseseat/js` | Both SDKs support: - License activation and deactivation - Online and offline validation (Ed25519 cryptographic signatures) - Automatic re-validation - Entitlement checking (`hasEntitlement()` for simple boolean checks) - Event-driven architecture - Singleton pattern for shared instances - Full TypeScript support (JS SDK includes `.d.ts` files) See the [[sdks/index\|SDK documentation]] for detailed integration guides. ## How LicenseSeat Works ``` 1. User purchases your app → You receive payment via Stripe/Gumroad/etc. 2. LicenseSeat webhook receives the event → Issues a license key automatically 3. User receives license key via email → Enters it in your app 4. Your app validates the key → LicenseSeat API confirms it's valid 5. App activates → User is happy, you're protected ``` ## Key Concepts | Concept | Description | | ---------------- | -------------------------------------------------------------- | | **Product** | Your software (e.g., "Hustl App", "PhotoEditor Pro") | | **License Plan** | A pricing tier with rules (seat limit, duration, entitlements) | | **License** | A unique key issued to a customer | | **Activation** | A device registered against a license | | **Entitlement** | A feature or capability granted by a license | ## API Base URL All API requests are made to: ``` https://licenseseat.com/api/v1 ``` All endpoints use product-scoped URLs (e.g., `/products/{slug}/licenses/{key}/validate`). --- ## Prerequisites # Prerequisites Before integrating LicenseSeat into your application, make sure you have: ## 1. A LicenseSeat Account Sign up at [licenseseat.com](https://licenseseat.com) and complete the onboarding process. You'll need: - An organization (created automatically on signup) - At least one **Product** configured in your dashboard - At least one **License Plan** for that product ## 2. Your API Key Find your publishable API key in the LicenseSeat dashboard under **Settings → API Keys**. Your key looks like `pk_live_xxxxxxxx` and is safe to embed in your apps — it can validate licenses and manage device activations, but can't perform privileged operations. > **Tip**: You have one publishable key per organization. It never changes and is always visible in your dashboard. ## 3. Understanding Your Integration Type LicenseSeat supports two integration patterns: ### Online Validation (Recommended) Your app calls the LicenseSeat API to validate licenses in real-time. **Best for:** - Desktop apps with internet access - Web applications - Mobile apps - Games with online features **Pros:** - Real-time license status - Instant revocation - No cryptographic complexity ### Offline Validation Your app validates a signed license file locally, without internet. **Best for:** - Air-gapped environments - Apps that must work offline - High-security deployments **Pros:** - Works without internet - Tamper-proof via Ed25519 signatures - No server dependency at runtime ## 4. Technical Requirements ### For Online Validation Your app needs to make HTTPS requests. Any language or framework works: - JavaScript/TypeScript (Node.js, browser with proxy) - Swift (iOS, macOS) - C++ (Windows, Linux, macOS) - C# (.NET, Unity) - Python - Ruby - Go - Rust ### For Offline Validation You'll need an Ed25519 signature verification library: | Language | Library | |----------|---------| | JavaScript | `@noble/ed25519` or `tweetnacl` | | Swift | `CryptoKit` (built-in) | | C++ | `libsodium` or `OpenSSL 1.1.1+` | | C# | `NSec` or `Chaos.NaCl` | | Python | `PyNaCl` or `cryptography` | | Rust | `ed25519-dalek` | | Go | `crypto/ed25519` (built-in) | ## Next Steps Ready to integrate? Continue to the [[getting-started/2-quickstart|Quickstart Guide]] to validate your first license in 15 minutes. --- ## Quickstart # Quickstart Get license validation working in under 5 minutes. ## Step 1: Add a License Input Your app needs a place for users to enter their license key: ``` ┌────────────────────────────────────────────┐ │ Enter your license key: │ │ ┌──────────────────────────────────────┐ │ │ │ XXXX-XXXX-XXXX-XXXX │ │ │ └──────────────────────────────────────┘ │ │ [Activate] │ └────────────────────────────────────────────┘ ``` ## Step 2: Connect to LicenseSeat When the user clicks **Activate**, validate the key using an SDK or the API directly. ### Use an SDK (Recommended) | Platform | Install | Docs | |----------|---------|------| | **JavaScript/TypeScript** | `npm install @licenseseat/js` | [[sdks/javascript]] | | **Swift** (macOS, iOS) | SPM: `licenseseat-swift` | [[sdks/swift]] | | **C#** (.NET, Unity, Godot) | `dotnet add package LicenseSeat` | [[sdks/csharp]] | | **C++** (Unreal, JUCE, native) | CMake FetchContent | [[sdks/cpp]] | ```javascript // JavaScript import LicenseSeat from '@licenseseat/js'; const sdk = new LicenseSeat({ apiKey: 'pk_live_xxxxxxxx', // Your publishable key productSlug: 'your-product' }); await sdk.activate('XXXX-XXXX-XXXX-XXXX'); if (sdk.hasEntitlement('pro')) { enableProFeatures(); } ``` ### Direct API (Python, Go, Rust, etc.) ```python # Python import requests response = requests.post( 'https://licenseseat.com/api/v1/products/my-app/licenses/XXXX-XXXX-XXXX-XXXX/validate', headers={'Authorization': 'Bearer pk_live_xxxxxxxx'}, # Publishable key json={'device_id': 'unique-device-id'} ) if response.json()['valid']: enable_pro_features() ``` ## Response ```json { "valid": true, "license": { "key": "XXXX-XXXX-XXXX-XXXX", "status": "active", "plan_key": "pro", "active_entitlements": [ { "key": "pro", "expires_at": null } ] } } ``` ## Next Steps - [[sdks/index|SDK Documentation]] — Full guides for each platform - [[api-reference/index|API Reference]] — All endpoints - [[api-reference/offline-token|Offline Licensing]] — Air-gapped validation --- ## Activate Device # Activate Device Activates a device for a license, consuming one seat. ``` POST /api/v1/products/{slug}/licenses/{key}/activate ``` > [Try it in the Interactive API Docs →](/api/) ## When to Use - User first launches app on a new device - User enters a license key for the first time - After validating license, before enabling features ## Path Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `slug` | Yes | Product slug (e.g., `my-app`) | | `key` | Yes | License key to activate | ## Request Body | Parameter | Required | Description | |-----------|----------|-------------| | `device_id` | Yes | Unique identifier for this device | | `device_name` | No | Human-readable device name | | `metadata` | No | Custom data (hostname, OS, etc.) | ## Example Request ```bash curl -X POST https://licenseseat.com/api/v1/products/my-app/licenses/XXXX-XXXX-XXXX-XXXX/activate \ -H "Authorization: Bearer pk_live_xxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "device_id": "057b55c8-1234-5678-abcd-123456789abc", "device_name": "John'\''s MacBook Pro", "metadata": {"os": "macOS 14.0", "app_version": "1.2.0"} }' ``` ## Response ```json { "object": "activation", "id": 123, "device_id": "057b55c8-1234-5678-abcd-123456789abc", "device_name": "John's MacBook Pro", "activated_at": "2024-01-15T10:30:00Z", "ip_address": "192.168.1.1", "metadata": { "os": "macOS 14.0", "app_version": "1.2.0" }, "license": { "object": "license", "key": "XXXX-XXXX-XXXX-XXXX", "status": "active", "active_seats": 1, "seat_limit": 3 } } ``` ## Response Fields | Field | Description | |-------|-------------| | `id` | Unique activation ID | | `device_id` | The device ID provided | | `device_name` | Human-readable device name | | `activated_at` | Timestamp of activation | | `license` | Nested license summary with updated seat count | **Already activated?** Returns existing activation (200 OK, not error). ## Error Responses ```json { "error": { "code": "seat_limit_exceeded", "message": "All seats are in use. Deactivate another device first.", "details": { "seat_limit": 3, "active_seats": 3 } } } ``` ## Error Codes | Code | HTTP | Meaning | |------|------|---------| | `license_not_found` | 404 | License key doesn't exist | | `expired` | 422 | License has expired | | `revoked` | 422 | License has been revoked | | `seat_limit_exceeded` | 422 | All seats are in use | ## Device ID Best Practices The `device_id` should be: - **Unique** — Different for each device - **Stable** — Doesn't change across restarts or OS updates - **Private** — Not personally identifiable ### Platform-Specific Approaches | Platform | Recommended Source | |----------|-------------------| | **macOS** | `IOPlatformUUID` via IOKit | | **Windows** | `Win32_ComputerSystemProduct.UUID` via WMI | | **Linux** | `/etc/machine-id` | | **iOS/Android** | Vendor identifier or `identifierForVendor` | | **Cross-platform** | SHA-256 hash of combined hardware identifiers | ## Typical Flow ``` 1. User enters license key 2. Validate license (optional but recommended) 3. Generate device identifier 4. Call activate endpoint 5. Store activation locally (for offline grace periods) 6. Enable app features ``` ## See Also - [[api-reference/deactivate|Deactivate Device]] — Release a seat - [[api-reference/validate-license|Validate License]] — Check before activating - [Full API Schema →](/api/) — Request/response details, try it live --- ## Authentication # Authentication All LicenseSeat API requests require authentication via your publishable API key. > [Try it in the Interactive API Docs →](/api/) ## Your Publishable Key Find your API key in the LicenseSeat dashboard under **Settings → API Keys**. Your publishable key (`pk_live_...`) is designed for client-side use: - **Safe to embed** in desktop apps, mobile apps, and distributed code - **Permanent** — one key per organization, never changes, can't be revoked - **Always visible** in your dashboard (copy it anytime) ```swift let client = LicenseSeat(apiKey: "pk_live_xxxxxxxx") ``` ## Usage Include your API key in the `Authorization` header: ``` Authorization: Bearer pk_live_xxxxxxxxxxxxxxxxxxxxxxxx ``` Or via curl: ```bash curl -X POST https://licenseseat.com/api/v1/products/my-app/licenses/XXXX-XXXX/validate \ -H "Authorization: Bearer pk_live_xxxxxxxx" \ -H "Content-Type: application/json" \ -d '{"device_id": "device-unique-id"}' ``` ## What You Can Do With your publishable key, you can: - Validate licenses - Activate and deactivate devices - Fetch releases and download tokens - Generate offline license tokens These are all the operations your app needs to verify and manage licenses. > **Coming soon**: Secret keys (`sk_live_...`) for server-side operations like creating licenses, revoking licenses, and other privileged actions via API. ## Error Responses | Error | Code | Fix | |-------|------|-----| | Missing API key | `authentication_required` | Add `Authorization` header | | Invalid API key | `invalid_api_key` | Check key in dashboard | See [[api-reference/errors|Error Codes]] for complete reference. --- ## Deactivate Device # Deactivate Device Deactivates a device, releasing its seat for use elsewhere. ``` POST /api/v1/products/{slug}/licenses/{key}/deactivate ``` > [Try it in the Interactive API Docs →](/api/) ## When to Use - User wants to transfer license to another device - App uninstall (best effort) - "Deactivate this device" settings option - Before hardware changes that alter device ID ## Path Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `slug` | Yes | Product slug (e.g., `my-app`) | | `key` | Yes | License key | ## Request Body | Parameter | Required | Description | |-----------|----------|-------------| | `device_id` | Yes | The device to deactivate | ## Example Request ```bash curl -X POST https://licenseseat.com/api/v1/products/my-app/licenses/XXXX-XXXX-XXXX-XXXX/deactivate \ -H "Authorization: Bearer pk_live_xxxxxxxx" \ -H "Content-Type: application/json" \ -d '{"device_id": "057b55c8-1234-5678-abcd-123456789abc"}' ``` ## Response ```json { "object": "deactivation", "activation_id": 123, "deactivated_at": "2024-01-15T12:00:00Z" } ``` ## Response Fields | Field | Description | |-------|-------------| | `activation_id` | ID of the deactivated activation | | `deactivated_at` | Timestamp of deactivation | ## Error Codes | Code | HTTP | Meaning | |------|------|---------| | `not_found` | 404 | No active activation for this device | | `license_not_found` | 404 | License key doesn't exist | | `revoked` | 422 | License revoked, activations already disabled | | `suspended` | 422 | License suspended | | `expired` | 422 | License expired | ## Deactivation Scenarios ### User-Initiated - "Transfer to another computer" button - Account settings in web portal - In-app device management ### Automatic - App uninstall handler (network permitting) - User logout - Before hardware upgrades ### Handling Failures If deactivation fails (e.g., network error during uninstall): - Seat remains consumed until manually cleared - Users can deactivate via LicenseSeat customer portal - Admins can deactivate via dashboard - Auto-release after inactivity (if configured) ## See Also - [[api-reference/activate|Activate Device]] — Register a device - [Full API Schema →](/api/) — Request/response details, try it live --- ## Generate Download Token # Generate Download Token Creates a time-limited, cryptographically signed token for secure release downloads. ``` POST /api/v1/products/{slug}/releases/{version}/download_token ``` > [Try it in the Interactive API Docs →](/api/) ## When to Use - Gating software downloads to valid license holders - Implementing secure auto-update functionality - Preventing unauthorized distribution ## Path Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `slug` | Yes | Product slug (e.g., `my-app`) | | `version` | Yes | Release version (e.g., `2.1.0`) | ## Request Body | Parameter | Required | Description | |-----------|----------|-------------| | `license_key` | Yes | License key to authorize the download | | `platform` | No | Target platform (e.g., `macos`, `windows`, `linux`) | ## Example Request ```bash curl -X POST https://licenseseat.com/api/v1/products/my-app/releases/2.1.0/download_token \ -H "Authorization: Bearer pk_live_xxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "license_key": "LS-ABCD-1234-EFGH-5678", "platform": "macos" }' ``` ## Response ```json { "object": "download_token", "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...", "expires_at": "2026-01-21T12:05:00Z" } ``` ## Response Fields | Field | Description | |-------|-------------| | `token` | Signed JWT token for download authorization | | `expires_at` | Token expiration timestamp (typically 5 minutes) | ## Error Codes | Code | HTTP | Meaning | |------|------|---------| | `license_not_found` | 404 | License key doesn't exist | | `release_not_found` | 404 | Release version doesn't exist | | `expired` | 422 | License has expired | | `revoked` | 422 | License has been revoked | | `download_token_not_configured` | 400 | Download tokens not enabled for this product | ## Using the Download Token Your download server should verify the token before serving files: 1. Extract the token from the request (query param or header) 2. Verify the Ed25519 signature using your signing key 3. Check token hasn't expired (`exp` claim) 4. Verify product and version match the requested file 5. Serve the file if valid ### Client-Side Usage ```javascript // Get download token const response = await fetch( '/api/v1/products/my-app/releases/2.1.0/download_token', { method: 'POST', headers: { 'Authorization': 'Bearer pk_live_xxxxxxxx', 'Content-Type': 'application/json' }, body: JSON.stringify({ license_key: 'LS-ABCD-1234-EFGH-5678', platform: 'macos' }) } ); const { token, expires_at } = await response.json(); // Use token to download const downloadUrl = `https://downloads.example.com/my-app/2.1.0/installer.dmg?token=${token}`; window.location.href = downloadUrl; ``` ## See Also - [[api-reference/list-releases|List Releases]] — Get available releases - [[api-reference/latest-release|Get Latest Release]] — Check for updates - [[api-reference/signing-keys|Signing Keys]] — Get verification keys - [Full API Schema →](/api/) — Request/response details, try it live --- ## Error Codes # Error Codes All LicenseSeat API errors follow a structured format for programmatic handling. > [Try it in the Interactive API Docs →](/api/) ## Response Format ```json { "error": { "code": "license_not_found", "message": "License key not found.", "details": {} } } ``` | Field | Description | |-------|-------------| | `error.code` | Machine-readable error code | | `error.message` | Human-readable error description | | `error.details` | Additional context (optional) | ## HTTP Status Codes | Code | Meaning | |------|---------| | `200` | Success | | `201` | Resource created | | `400` | Invalid request | | `401` | Authentication required or failed | | `403` | Insufficient permissions | | `404` | Resource not found | | `422` | Request understood but cannot process | | `429` | Rate limit exceeded | | `500` | Server error | ## Error Codes ### Authentication | Code | HTTP | Resolution | |------|------|------------| | `authentication_required` | 401 | Add `Authorization: Bearer KEY` header | | `invalid_api_key` | 401 | Check API key in dashboard | ### License | Code | HTTP | Resolution | |------|------|------------| | `license_not_found` | 404 | Verify license key is correct | | `expired` | 422 | Renew the license | | `revoked` | 422 | Contact support or purchase new | | `suspended` | 422 | Contact support | | `not_active` | 422 | Check license status in dashboard | | `product_mismatch` | 422 | Use correct license for this product | ### Activation | Code | HTTP | Resolution | |------|------|------------| | `device_not_activated` | 422 | Call activate endpoint first | | `seat_limit_exceeded` | 422 | Deactivate another device or upgrade | | `already_deactivated` | 422 | Device was already deactivated | | `not_found` | 404 | No activation found for this device | ### Request | Code | HTTP | Resolution | |------|------|------------| | `parameter_missing` | 400 | Include all required parameters | | `invalid_arguments` | 400 | Check parameter format and values | ## User-Friendly Messages Map technical codes to user-facing messages: | Code | Suggested Message | |------|-------------------| | `license_not_found` | "We couldn't find that license key. Please double-check and try again." | | `expired` | "Your license has expired. Renew now to continue using all features." | | `revoked` | "This license is no longer valid. Please contact support." | | `seat_limit_exceeded` | "You've reached the maximum devices. Deactivate another device first." | ## Error Response Examples ### Missing Parameter ```json { "error": { "code": "parameter_missing", "message": "Required parameter 'device_id' is missing.", "details": { "parameter": "device_id" } } } ``` ### Seat Limit Exceeded ```json { "error": { "code": "seat_limit_exceeded", "message": "All seats are in use. Deactivate another device first.", "details": { "seat_limit": 3, "active_seats": 3 } } } ``` ### Rate Limited ```json { "error": { "code": "rate_limited", "message": "Too many requests. Please try again later.", "details": { "retry_after": 60 } } } ``` ## Retry Logic For server errors (5xx), use exponential backoff: ``` Attempt 1: immediate Attempt 2: wait 1s Attempt 3: wait 2s Attempt 4: wait 4s ``` Don't retry client errors (4xx) — they require fixing the request. ## See Also - [[api-reference/authentication|Authentication]] — API key setup - [Full API Schema →](/api/) — Complete error details per endpoint --- ## API Reference # API Reference Complete reference documentation for the LicenseSeat API. > **Interactive API Docs** — Explore our [Interactive API Reference](/api/) for a beautiful, interactive experience with request testing, full schemas, and live examples. ## Quick Reference | Category | Endpoints | |----------|-----------| | **Licenses** | [[api-reference/validate-license\|Validate]], [[api-reference/activate\|Activate]], [[api-reference/deactivate\|Deactivate]], [[api-reference/offline-token\|Offline Token]] | | **Releases** | [[api-reference/list-releases\|List]], [[api-reference/latest-release\|Latest]], [[api-reference/download-token\|Download Token]] | | **Utilities** | [[api-reference/signing-keys\|Signing Keys]], [[api-reference/health\|Health]] | ## Essentials - **Base URL:** `https://licenseseat.com/api/v1` - **Auth:** Bearer token in `Authorization` header - **Format:** JSON request/response bodies - **Errors:** Structured format with `error.code` and `error.message` All license operations are **product-scoped**. The product slug and license key are in the URL path: ```bash curl -X POST https://licenseseat.com/api/v1/products/my-app/licenses/XXXX-XXXX-XXXX-XXXX/validate \ -H "Authorization: Bearer pk_live_xxxxxxxx" \ -H "Content-Type: application/json" \ -d '{"device_id": "device-unique-id"}' ``` ## Response Format All responses include an `object` field identifying the type: ```json { "object": "validation_result", "valid": true, "license": { ... }, "activation": { ... } } ``` ## Rate Limits | Plan | Requests/min | Requests/day | |------------|--------------|--------------| | Free | 60 | 1,000 | | Pro | 300 | 50,000 | | Enterprise | Unlimited | Unlimited | Rate limit headers included in all responses: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset` ## Learn More - [[api-reference/authentication|Authentication]] — API key setup and security - [[api-reference/errors|Error Codes]] — Complete error reference - [Interactive API Docs](/api/) — Try requests in your browser --- ## Get Latest Release # Get Latest Release Gets the most recent release for a product. ``` GET /api/v1/products/{slug}/releases/latest ``` > **Note:** This endpoint does not require authentication. > [Try it in the Interactive API Docs →](/api/) ## When to Use - Quick update check without fetching all releases - Display "New version available" notifications - Pre-download latest version info ## Path Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `slug` | Yes | Product slug (e.g., `my-app`) | ## Query Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `channel` | No | Release channel: `stable`, `beta`, `alpha` | | `platform` | No | Target platform: `macos`, `windows`, `linux` | ## Example Request ```bash curl "https://licenseseat.com/api/v1/products/my-app/releases/latest?platform=macos&channel=stable" ``` No authentication required. ## Response ```json { "object": "release", "version": "2.1.0", "channel": "stable", "platform": "macos", "published_at": "2024-01-15T12:00:00Z", "release_notes": "Bug fixes and performance improvements" } ``` ## Response Fields | Field | Description | |-------|-------------| | `version` | Semantic version string | | `channel` | Release channel (`stable`, `beta`, `alpha`) | | `platform` | Target platform | | `published_at` | Publication timestamp | | `release_notes` | Human-readable release notes | ## Error Codes | Code | HTTP | Meaning | |------|------|---------| | `not_found` | 404 | No matching release exists | ## Update Check Pattern ```javascript async function checkForUpdate(currentVersion) { const response = await fetch( `/api/v1/products/my-app/releases/latest?platform=macos&channel=stable` ); if (response.status === 404) { return null; // No releases available } const latest = await response.json(); if (isNewerVersion(latest.version, currentVersion)) { return latest; } return null; } ``` ## See Also - [[api-reference/list-releases|List Releases]] — Get all available releases - [Full API Schema →](/api/) — Request/response details, try it live --- ## List Releases # List Releases Lists available releases for a product. ``` GET /api/v1/products/{slug}/releases ``` > **Note:** This endpoint does not require authentication. > [Try it in the Interactive API Docs →](/api/) ## When to Use - Check for available updates - Display version history to users - Build auto-update functionality - Show release notes ## Path Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `slug` | Yes | Product slug (e.g., `my-app`) | ## Query Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `channel` | No | Filter by channel: `stable`, `beta`, `alpha` | | `platform` | No | Filter by platform: `macos`, `windows`, `linux` | | `limit` | No | Maximum results to return (default: 10) | ## Example Request ```bash curl "https://licenseseat.com/api/v1/products/my-app/releases?platform=macos&channel=stable&limit=5" ``` No authentication required. ## Response ```json { "object": "list", "data": [ { "object": "release", "version": "2.1.0", "channel": "stable", "platform": "macos", "published_at": "2024-01-15T12:00:00Z", "release_notes": "Bug fixes and performance improvements" }, { "object": "release", "version": "2.0.0", "channel": "stable", "platform": "macos", "published_at": "2024-01-01T12:00:00Z", "release_notes": "Major update with new features" } ], "has_more": false, "next_cursor": null } ``` ## Response Fields | Field | Description | |-------|-------------| | `data` | Array of release objects | | `has_more` | Whether more results are available | | `next_cursor` | Cursor for pagination (if `has_more` is true) | ### Release Object | Field | Description | |-------|-------------| | `version` | Semantic version string | | `channel` | Release channel (`stable`, `beta`, `alpha`) | | `platform` | Target platform | | `published_at` | Publication timestamp | | `release_notes` | Human-readable release notes | ## Error Codes | Code | HTTP | Meaning | |------|------|---------| | `not_found` | 404 | Product doesn't exist | ## Auto-Update Pattern ```javascript async function checkForUpdate(currentVersion, platform) { const response = await fetch( `/api/v1/products/my-app/releases?platform=${platform}&channel=stable&limit=1` ); const data = await response.json(); if (data.data.length > 0) { const latest = data.data[0]; if (isNewerVersion(latest.version, currentVersion)) { showUpdateAvailable(latest); } } } ``` ## See Also - [[api-reference/latest-release|Get Latest Release]] — Shortcut for newest release - [Full API Schema →](/api/) — Request/response details, try it live --- ## 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) | ```bash 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 ```json { "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 ```python 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: ```python 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 - [[api-reference/signing-keys|Signing Keys]] — Fetch public verification keys - [[sdks/index|SDKs]] — Built-in offline support in all SDKs --- ## Get Signing Key # Get Signing Key Retrieves an Ed25519 public key for verifying offline token signatures. ``` GET /api/v1/signing_keys/{key_id} ``` > **Note:** This endpoint does not require authentication. > [Try it in the Interactive API Docs →](/api/) ## When to Use - Fetching verification keys for offline tokens - Supporting key rotation - Dynamically loading unknown key IDs ## Path Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `key_id` | Yes | Key ID from offline token `signature.key_id` or `token.kid` | ## Example Request ```bash curl https://licenseseat.com/api/v1/signing_keys/org-xxx-offline-v1 ``` No authentication required. ## Response ```json { "object": "signing_key", "key_id": "org-xxx-offline-v1", "algorithm": "Ed25519", "public_key": "base64url_encoded_32_byte_public_key", "created_at": "2024-01-01T00:00:00Z", "status": "active" } ``` ## Response Fields | Field | Description | |-------|-------------| | `key_id` | Unique key identifier | | `algorithm` | Signature algorithm (`Ed25519`) | | `public_key` | Base64url-encoded 32-byte Ed25519 public key | | `created_at` | When the key was created | | `status` | Key status (`active`, `rotated`) | ## Error Codes | Code | HTTP | Meaning | |------|------|---------| | `signing_key_not_found` | 404 | Key ID doesn't exist | ## Key Rotation LicenseSeat supports key rotation for security: 1. New offline tokens use the latest key 2. Old keys remain valid for existing tokens 3. The `key_id` field identifies which key was used 4. Your app should fetch keys it doesn't recognize ## Best Practices ### Embed Known Keys For performance and offline support, embed known public keys in your app: ```javascript const KNOWN_KEYS = { 'org-xxx-offline-v1': 'base64url_encoded_key_1', 'org-xxx-offline-v2': 'base64url_encoded_key_2', }; async function getPublicKey(keyId) { // Use embedded key if known if (KNOWN_KEYS[keyId]) return KNOWN_KEYS[keyId]; // Fetch unknown keys from API const response = await fetch(`/api/v1/signing-keys/${keyId}`); const data = await response.json(); return data.public_key; } ``` ### Security - **Validate key length** — Decoded key must be exactly 32 bytes - **Always verify signatures** — Even with embedded keys - **Handle new key IDs** — Be prepared for key rotation - **Use constant-time comparison** — Prevent timing attacks ## See Also - [[api-reference/offline-token|Generate Offline Token]] — Create signed tokens - [[api-reference/offline-token|Offline Licensing]] — Complete verification examples - [Full API Schema →](/api/) — Request/response details, try it live --- ## Validate License # Validate License Validates a license key and returns its current status. ``` POST /api/v1/products/{slug}/licenses/{key}/validate ``` > [Try it in the Interactive API Docs →](/api/) ## When to Use - App startup to check license validity - Periodic background checks - After user enters a license key - Before enabling premium features ## Path Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `slug` | Yes | Product slug (e.g., `my-app`) | | `key` | Yes | License key to validate | ## Request Body | Parameter | Required | Description | |-----------|----------|-------------| | `device_id` | Conditional | **Required for `hardware_locked` licenses.** Unique device ID for seat validation. Optional for `floating` licenses. | > **Important:** For `hardware_locked` licenses, you **must** provide a `device_id` to verify the device is activated. Without it, validation returns `valid: false` with code `device_not_activated`. ## Example Request ```bash curl -X POST https://licenseseat.com/api/v1/products/my-app/licenses/XXXX-XXXX-XXXX-XXXX/validate \ -H "Authorization: Bearer pk_live_xxxxxxxx" \ -H "Content-Type: application/json" \ -d '{"device_id": "device-unique-id"}' ``` ## Response ```json { "object": "validation_result", "valid": true, "code": null, "message": null, "license": { "object": "license", "key": "XXXX-XXXX-XXXX-XXXX", "status": "active", "mode": "hardware_locked", "starts_at": "2024-01-01T00:00:00Z", "expires_at": null, "seat_limit": 3, "active_seats": 1, "plan_key": "pro", "active_entitlements": [ { "key": "updates", "expires_at": "2025-01-01T00:00:00Z", "metadata": null } ], "metadata": {}, "product": { "slug": "my-app", "name": "My App" } }, "activation": { "object": "activation", "id": 123, "device_id": "device-unique-id", "device_name": "John's MacBook Pro", "activated_at": "2024-01-15T10:30:00Z" } } ``` ## Response Fields | Field | Description | |-------|-------------| | `valid` | Boolean indicating license validity | | `code` | Error code if invalid (null when valid) | | `message` | Human-readable message if invalid | | `warnings` | Array of non-fatal advisories (e.g., `license_expiring_soon`) | | `license.status` | `active`, `expired`, `revoked`, `suspended`, `pending` | | `license.expires_at` | Expiration timestamp (null for perpetual) | | `license.seat_limit` | Max simultaneous activations | | `license.active_seats` | Currently active activations | | `license.active_entitlements` | Features granted by this license | | `activation` | Device activation details (if `device_id` provided) | ## Response with Warnings When a license is valid but has advisories: ```json { "object": "validation_result", "valid": true, "warnings": [ { "code": "license_expiring_soon", "message": "License expires in 7 days" } ], "license": { ... } } ``` ## Invalid License Response When a license is invalid, the response still returns HTTP 200 but with `valid: false`: ```json { "object": "validation_result", "valid": false, "code": "expired", "message": "License has expired.", "license": { ... } } ``` ## Error Codes | Code | Meaning | |------|---------| | `license_not_found` | License key doesn't exist | | `expired` | License has expired | | `revoked` | License has been revoked | | `suspended` | License temporarily suspended | | `device_not_activated` | Device not activated (when `device_id` provided) | | `seat_limit_exceeded` | No available seats for this device | | `product_mismatch` | License is for a different product | ## Best Practices 1. **Cache results** — Don't call on every user action; cache for ~1 hour 2. **Handle network failures** — Use cached results or allow a grace period when offline 3. **Pass device_id** — For hardware-locked licenses, always include to verify activation 4. **Check entitlements** — Use `active_entitlements` to enable/disable features 5. **Monitor expiration** — Warn users before `expires_at` date ## See Also - [[api-reference/activate|Activate Device]] — Register a device for the license - [[getting-started/2-quickstart|Quickstart]] — Complete validation workflow - [Full API Schema →](/api/) — Request/response details, try it live --- ## C++ SDK # C++ SDK Official C++ SDK for LicenseSeat. Add license validation to native applications, Unreal Engine games, and VST/AU audio plugins. > **Building a VST plugin or Unreal game?** We provide a [single-header JUCE integration](#juce-vst--au--aax) and an [Unreal Engine plugin](#unreal-engine-plugin) with zero external dependencies. ## Features - **License activation & deactivation** - Automatic device fingerprinting - **Online & offline validation** - Ed25519 cryptographic verification - **Entitlement checking** - `has_entitlement()` and `check_entitlement()` - **Local caching** - File-based caching with clock tamper detection - **Auto-validation** - Background validation at configurable intervals - **Event system** - Subscribe to license events - **Thread-safe** - All public methods safe from any thread - **Exception-free** - Uses `Result` pattern ## Installation ### CMake (FetchContent) ```cmake include(FetchContent) FetchContent_Declare( licenseseat GIT_REPOSITORY https://github.com/licenseseat/licenseseat-cpp.git GIT_TAG main ) FetchContent_MakeAvailable(licenseseat) target_link_libraries(your_target PRIVATE licenseseat::licenseseat) ``` ### Manual Build ```bash git clone https://github.com/licenseseat/licenseseat-cpp.git cd licenseseat-cpp cmake -B build -DCMAKE_BUILD_TYPE=Release cmake --build build sudo cmake --install build ``` ### Dependencies - **nlohmann/json** - JSON parsing - **cpp-httplib** - HTTP client (with OpenSSL for HTTPS) Cryptographic operations (Ed25519, SHA-256) use vendored libraries with no external dependencies. ## Quick Start ```cpp #include int main() { licenseseat::Config config; config.api_key = "pk_live_xxxxxxxx"; config.product_slug = "your-product"; licenseseat::Client client(config); // Validate a license auto result = client.validate("XXXX-XXXX-XXXX-XXXX"); if (result.is_ok()) { const auto& validation = result.value(); if (validation.valid) { std::cout << "License is valid!\n"; std::cout << "Plan: " << validation.license.plan_key() << "\n"; } else { std::cout << "Invalid: " << validation.code << " - " << validation.message << "\n"; } } else { std::cerr << "Error: " << result.error_message() << "\n"; } // Check entitlements if (client.has_entitlement("pro")) { // Enable pro features } return 0; } ``` ## Configuration ```cpp licenseseat::Config config; // Required config.api_key = "pk_live_xxxxxxxx"; config.product_slug = "your-product"; // Optional - API settings config.api_url = "https://licenseseat.com/api/v1"; // Default config.timeout_seconds = 30; config.max_retries = 3; // Optional - Device identification config.device_id = ""; // Auto-generated if empty // Optional - Offline support config.signing_public_key = "base64-ed25519-public-key"; // Pre-configure for offline config.max_offline_days = 30; // Optional - Caching config.storage_path = ""; // Path for license cache (empty = no persistence) // Optional - Auto-validation config.auto_validate_interval = 3600.0; // Seconds between background validations ``` ### Configuration Options | Option | Type | Default | Description | | ------ | ---- | ------- | ----------- | | `api_key` | `string` | *required* | Your publishable API key | | `product_slug` | `string` | *required* | Product identifier | | `api_url` | `string` | `https://licenseseat.com/api/v1` | API endpoint | | `timeout_seconds` | `int` | `30` | HTTP request timeout | | `max_retries` | `int` | `3` | Retry attempts for failed requests | | `device_id` | `string` | `""` | Device identifier (auto-generated if empty) | | `signing_public_key` | `string` | `""` | Ed25519 public key for offline verification | | `max_offline_days` | `int` | `0` | Maximum days offline (0 = disabled) | | `storage_path` | `string` | `""` | Path for license cache (empty = no persistence) | | `auto_validate_interval` | `double` | `3600.0` | Seconds between auto-validation cycles | ## Validation Validation checks if a license is valid. The API always returns HTTP 200 for validation - check the `valid` field to determine validity. ```cpp auto result = client.validate("LICENSE-KEY"); if (result.is_ok()) { const auto& validation = result.value(); if (validation.valid) { // License is valid and usable std::cout << "Valid! Plan: " << validation.license.plan_key() << "\n"; } else { // License exists but isn't valid for use // Common codes: expired, revoked, suspended, seat_limit_exceeded std::cout << "Code: " << validation.code << "\n"; std::cout << "Message: " << validation.message << "\n"; } // License data is always available (even when invalid) const auto& license = validation.license; std::cout << "Key: " << license.key() << "\n"; std::cout << "Status: " << license_status_to_string(license.status()) << "\n"; std::cout << "Seats: " << license.active_seats() << "/" << license.seat_limit() << "\n"; } else { // API error (license not found, network error, auth failed) std::cerr << "Error: " << result.error_message() << "\n"; } ``` > **Note:** For **hardware-locked** licenses, you must provide a `device_id` to validate: > ```cpp > auto result = client.validate("LICENSE-KEY", device_id); > ``` > Without it, validation may return `valid: false` with code `device_not_activated`. ### Async Validation ```cpp client.validate_async("LICENSE-KEY", [](licenseseat::Result result) { if (result.is_ok() && result.value().valid) { // License is valid } }); ``` ## Activation Activation binds a license to a device, consuming a seat. ```cpp auto result = client.activate("LICENSE-KEY", device_id, "My MacBook Pro"); if (result.is_ok()) { const auto& activation = result.value(); std::cout << "Activation ID: " << activation.id() << "\n"; std::cout << "Device: " << activation.device_name() << "\n"; } else { switch (result.error_code()) { case licenseseat::ErrorCode::SeatLimitExceeded: std::cerr << "No seats available\n"; break; case licenseseat::ErrorCode::DeviceAlreadyActivated: std::cerr << "Device already activated\n"; break; default: std::cerr << "Error: " << result.error_message() << "\n"; } } ``` ## Deactivation Deactivation removes a device from a license, freeing a seat. ```cpp auto result = client.deactivate("LICENSE-KEY", device_id); if (result.is_ok()) { std::cout << "Device deactivated\n"; } else if (result.error_code() == licenseseat::ErrorCode::ActivationNotFound) { std::cout << "Device was not activated\n"; } ``` ## Entitlements ```cpp // Simple boolean check (uses cached license data) if (client.has_entitlement("pro")) { enable_pro_features(); } // Detailed check with reason auto entitlement = client.check_entitlement("feature-key"); if (entitlement.active) { // Feature unlocked } else { std::cout << "Not available: " << entitlement.reason << "\n"; } ``` ## Status Get the current cached license status without making a network request. ```cpp auto status = client.get_status(); std::cout << "Valid: " << (status.valid ? "yes" : "no") << "\n"; std::cout << "Code: " << status.code << "\n"; ``` ## Offline Support The SDK supports offline license validation using Ed25519 cryptographic signatures. ### Offline Token Workflow **Step 1: Generate and cache offline token while online** ```cpp // Generate offline token (requires network) auto token_result = client.generate_offline_token("LICENSE-KEY"); if (token_result.is_error()) { std::cerr << "Failed to generate token: " << token_result.error_message() << "\n"; return; } auto offline_token = token_result.value(); // Fetch signing key (requires network) auto key_result = client.fetch_signing_key(offline_token.token.kid); if (key_result.is_error()) { std::cerr << "Failed to fetch key: " << key_result.error_message() << "\n"; return; } std::string public_key = key_result.value(); // Store both for offline use save_to_disk(offline_token, public_key); ``` **Step 2: Verify offline (no network required)** ```cpp // Load cached data auto [offline_token, public_key] = load_from_disk(); // Verify signature locally auto verify_result = client.verify_offline_token(offline_token, public_key); if (verify_result.is_ok() && verify_result.value()) { // Token is valid - license data available in offline_token.token std::cout << "License: " << offline_token.token.license_key << "\n"; std::cout << "Plan: " << offline_token.token.plan_key << "\n"; std::cout << "Expires: " << offline_token.token.exp << "\n"; // Check entitlements from token for (const auto& ent : offline_token.token.entitlements) { std::cout << "Entitlement: " << ent.key << "\n"; } } ``` ### Pre-configured Public Key For simpler deployments, pre-configure the signing public key: ```cpp licenseseat::Config config; config.api_key = "pk_live_xxxxxxxx"; config.product_slug = "your-product"; config.signing_public_key = "MCowBQYDK2VwAyEA..."; // Your public key config.max_offline_days = 30; licenseseat::Client client(config); // Now verify_offline_token can use the pre-configured key auto result = client.verify_offline_token(offline_token); // No key param needed ``` ### Offline Token Fields | Field | Type | Description | | ----- | ---- | ----------- | | `license_key` | `string` | The license key | | `product_slug` | `string` | Product identifier | | `plan_key` | `string` | Plan identifier | | `mode` | `string` | License mode (`hardware_locked` or `floating`) | | `seat_limit` | `int` | Maximum allowed activations | | `device_id` | `string` | Device this token is bound to (if hardware_locked) | | `iat` | `int64` | Issued at (Unix timestamp) | | `exp` | `int64` | Expires at (Unix timestamp) | | `nbf` | `int64` | Not valid before (Unix timestamp) | | `kid` | `string` | Key ID for fetching the signing public key | | `entitlements` | `array` | List of entitlements with keys and expiration | | `metadata` | `object` | Custom metadata attached to the license | ## Auto-Validation Background validation at configurable intervals. ```cpp // Configure interval (in seconds) config.auto_validate_interval = 3600.0; // Every hour licenseseat::Client client(config); // Start auto-validation client.start_auto_validation("LICENSE-KEY"); // Check if running if (client.is_auto_validating()) { std::cout << "Auto-validation is active\n"; } // Stop when done client.stop_auto_validation(); ``` ## Events Subscribe to license events for reactive updates. ```cpp #include // Subscribe to validation success auto sub1 = client.on(licenseseat::events::VALIDATION_SUCCESS, [](const std::any& data) { std::cout << "License validated successfully!\n"; }); // Subscribe to validation failure auto sub2 = client.on(licenseseat::events::VALIDATION_FAILED, [](const std::any& data) { std::cout << "License validation failed\n"; }); // Subscribe to offline token ready auto sub3 = client.on(licenseseat::events::OFFLINE_TOKEN_READY, [](const std::any& data) { std::cout << "Offline token generated\n"; }); // Later: cancel subscriptions sub1.cancel(); sub2.cancel(); sub3.cancel(); ``` ### Available Events | Event | Description | | ----- | ----------- | | `LICENSE_LOADED` | License data loaded from cache | | `ACTIVATION_START` | Activation request starting | | `ACTIVATION_SUCCESS` | Device activated successfully | | `ACTIVATION_ERROR` | Activation failed | | `VALIDATION_START` | Validation request starting | | `VALIDATION_SUCCESS` | License validated successfully | | `VALIDATION_FAILED` | License validation returned invalid | | `VALIDATION_ERROR` | Validation request failed (network, etc.) | | `VALIDATION_OFFLINE_SUCCESS` | Offline token verified successfully | | `VALIDATION_OFFLINE_FAILED` | Offline token verification failed | | `DEACTIVATION_START` | Deactivation request starting | | `DEACTIVATION_SUCCESS` | Device deactivated successfully | | `DEACTIVATION_ERROR` | Deactivation failed | | `NETWORK_ONLINE` | Network connectivity restored | | `NETWORK_OFFLINE` | Network connectivity lost | | `AUTOVALIDATION_CYCLE` | Auto-validation cycle completed | | `AUTOVALIDATION_STOPPED` | Auto-validation stopped | | `OFFLINE_TOKEN_READY` | Offline token generated | | `OFFLINE_TOKEN_VERIFIED` | Offline token verified | | `SDK_RESET` | SDK state reset | ## Error Handling The SDK uses a `Result` pattern instead of exceptions. ```cpp auto result = client.validate("LICENSE-KEY"); if (result.is_ok()) { auto& validation = result.value(); // Success - check validation.valid for license validity } else { // Error - network, auth, or API error std::cerr << "Error: " << result.error_message() << "\n"; switch (result.error_code()) { case licenseseat::ErrorCode::NetworkError: // No network connectivity break; case licenseseat::ErrorCode::LicenseNotFound: // Invalid license key break; case licenseseat::ErrorCode::AuthenticationFailed: // Invalid API key break; case licenseseat::ErrorCode::SeatLimitExceeded: // Too many activations break; // ... handle other cases } } ``` ### Error Codes | Code | Description | | ---- | ----------- | | `Success` | Operation completed successfully | | `NetworkError` | HTTP request failed (no connectivity) | | `ConnectionTimeout` | Request timed out | | `SSLError` | SSL/TLS error | | `InvalidLicenseKey` | License key format is invalid | | `LicenseNotFound` | License key not found in system | | `LicenseExpired` | License has expired | | `LicenseRevoked` | License has been revoked | | `LicenseSuspended` | License is suspended | | `LicenseNotActive` | License is not active | | `LicenseNotStarted` | License hasn't started yet | | `SeatLimitExceeded` | Maximum activations reached | | `ActivationNotFound` | Device activation not found | | `DeviceAlreadyActivated` | Device is already activated | | `ProductNotFound` | Product slug not found | | `AuthenticationFailed` | Invalid or missing API key | | `PermissionDenied` | API key lacks required permissions | | `ServerError` | Server-side error (5xx) | | `SigningNotConfigured` | Offline signing not configured | | `InvalidSignature` | Cryptographic signature invalid | | `FileError` | File read/write error | ## Unreal Engine Plugin A complete UE plugin using native `FHttpModule` and `FJsonObject`. No external dependencies. ```cpp auto* LicenseSeat = GetGameInstance()->GetSubsystem(); FLicenseSeatConfig Config; Config.ApiKey = TEXT("pk_live_xxxxxxxx"); Config.ProductSlug = TEXT("your-game"); LicenseSeat->InitializeWithConfig(Config); LicenseSeat->ValidateAsync(TEXT("LICENSE-KEY"), FOnValidationComplete::CreateLambda([](const FLicenseValidationResult& Result) { if (Result.bValid) { // License valid } })); ``` **Location:** `integrations/unreal/LicenseSeat/` **Features:** - Blueprint support via `UFUNCTION`/`UPROPERTY`/`USTRUCT` - `GameInstanceSubsystem` for automatic lifecycle management - Async API (non-blocking) - Auto-validation timer - Ed25519 offline verification (ThirdParty folder pre-populated) ## JUCE: VST / AU / AAX A single-header integration using only JUCE's native HTTP (`juce::URL`) and JSON (`juce::JSON`), without any dependency on cpp-httplib, nlohmann/json, or OpenSSL. ```cpp #include "LicenseSeatJuceStandalone.h" LicenseSeatJuceStandalone license("pk_live_xxxxxxxx", "your-plugin"); // Audio thread safe (reads std::atomic) void processBlock(juce::AudioBuffer& buffer, juce::MidiBuffer&) { if (!license.isValid()) { buffer.clear(); return; } // Process audio } // Async validation (callback on message thread) license.validateAsync("LICENSE-KEY", [](auto& result) { if (result.valid) { // Update UI } }); ``` **Location:** `integrations/juce/Source/LicenseSeatJuceStandalone.h` **Features:** - Single header file - `std::atomic` for lock-free status checks in audio thread - `MessageManager::callAsync` for thread-safe UI callbacks - Multi-instance safe (no global state) > **Note:** The standalone integration avoids OpenSSL symbol conflicts that occur when multiple plugins in the same DAW link different OpenSSL versions. ## Thread Safety All public methods are thread-safe. The SDK uses internal mutexes to protect shared state. ```cpp // Safe from multiple threads std::thread t1([&client]() { client.validate("KEY"); }); std::thread t2([&client]() { bool valid = client.is_valid(); }); ``` For audio plugins, `isValid()` uses `std::atomic` for lock-free reads in real-time contexts. ## Platform Support | Platform | Compiler | Status | | -------- | -------- | ------ | | Linux | GCC 9+, Clang 10+ | Supported | | macOS | Apple Clang 12+ (ARM & Intel) | Supported | | Windows | MSVC 2019+ | Supported | ### Device Identification The SDK automatically generates a unique device identifier: - **macOS**: IOKit Platform UUID - **Windows**: Machine GUID from registry - **Linux**: `/etc/machine-id` or hostname-based fallback ## Reset Clear all cached data (license, offline tokens, etc.). ```cpp client.reset(); ``` ## Next Steps - [[sdks/csharp|C# SDK]] - For .NET applications - [[sdks/swift|Swift SDK]] - For Apple platforms - [[api-reference/offline-token|Offline Licensing]] - Air-gapped validation - [[api-reference/index|API Reference]] - Direct API access --- ## C# SDK # C# SDK Official C# SDK for LicenseSeat. Add license validation to your .NET apps, Unity games, and Godot projects in minutes. > **Building a Unity game?** We have a dedicated Unity SDK with full IL2CPP, WebGL, iOS, and Android support. No DLLs - just install via Unity Package Manager. ## Installation ### NuGet (.NET, Godot) ```bash dotnet add package LicenseSeat ``` **Requirements:** .NET Standard 2.0+ (.NET Framework 4.6.1+, .NET Core 2.0+, .NET 5+) ### Unity **Option 1: Git URL (Recommended)** 1. Open **Window > Package Manager** 2. Click **+** > **Add package from git URL...** 3. Paste: ``` https://github.com/licenseseat/licenseseat-csharp.git?path=src/LicenseSeat.Unity ``` **Option 2: manifest.json** Add to `Packages/manifest.json`: ```json { "dependencies": { "com.licenseseat.sdk": "https://github.com/licenseseat/licenseseat-csharp.git?path=src/LicenseSeat.Unity" } } ``` **Option 3: OpenUPM** ```bash openupm add com.licenseseat.sdk ``` **Pin to a version:** ``` https://github.com/licenseseat/licenseseat-csharp.git?path=src/LicenseSeat.Unity#v0.2.0 ``` ## Quick Start ```csharp using LicenseSeat; var client = new LicenseSeatClient(new LicenseSeatClientOptions { ApiKey = "pk_live_xxxxxxxx", ProductSlug = "your-product" // Required }); // Activate a license var license = await client.ActivateAsync("XXXX-XXXX-XXXX-XXXX"); // Check entitlements if (client.HasEntitlement("pro-features")) { // Enable pro features } ``` ## Static API (Singleton) For desktop apps where you want global access: ```csharp using LicenseSeat; // Configure once at startup LicenseSeat.LicenseSeat.Configure("pk_live_xxxxxxxx", "your-product", options => { options.AutoValidateInterval = TimeSpan.FromHours(1); }); // Use anywhere in your app await LicenseSeat.LicenseSeat.Activate("LICENSE-KEY"); if (LicenseSeat.LicenseSeat.HasEntitlement("premium")) { // Premium features } var status = LicenseSeat.LicenseSeat.GetStatus(); var license = LicenseSeat.LicenseSeat.GetCurrentLicense(); // Cleanup on exit LicenseSeat.LicenseSeat.Shutdown(); ``` ## Configuration ### Basic Configuration ```csharp var client = new LicenseSeatClient(new LicenseSeatClientOptions { ApiKey = "pk_live_xxxxxxxx", ProductSlug = "your-product" }); ``` ### Advanced Configuration ```csharp var client = new LicenseSeatClient(new LicenseSeatClientOptions { ApiKey = "pk_live_xxxxxxxx", ProductSlug = "your-product", ApiBaseUrl = "https://licenseseat.com/api/v1", AutoValidateInterval = TimeSpan.FromHours(1), MaxRetries = 3, RetryDelay = TimeSpan.FromSeconds(1), OfflineFallbackMode = OfflineFallbackMode.NetworkOnly, MaxOfflineDays = 7, MaxClockSkew = TimeSpan.FromMinutes(5), HttpTimeout = TimeSpan.FromSeconds(30), Debug = true }); ``` ### Configuration Options | Option | Type | Default | Description | | ------ | ---- | ------- | ----------- | | `ApiKey` | `string` | — | **Required.** Your publishable API key | | `ProductSlug` | `string` | — | **Required.** Your product identifier | | `ApiBaseUrl` | `string` | `https://licenseseat.com/api/v1` | API endpoint | | `AutoValidateInterval` | `TimeSpan` | 1 hour | Background validation interval (0 = disabled) | | `MaxRetries` | `int` | 3 | Retry attempts for failed requests | | `RetryDelay` | `TimeSpan` | 1 second | Base delay between retries | | `OfflineFallbackMode` | `OfflineFallbackMode` | `Disabled` | Offline validation mode | | `MaxOfflineDays` | `int` | 0 | Offline grace period (0 = disabled) | | `MaxClockSkew` | `TimeSpan` | 5 minutes | Clock tamper tolerance | | `HttpTimeout` | `TimeSpan` | 30 seconds | Request timeout | | `Debug` | `bool` | `false` | Enable debug logging | ### Offline Fallback Modes | Mode | Description | | ---- | ----------- | | `Disabled` | Offline fallback disabled. Network failures throw exceptions. | | `NetworkOnly` | Fall back to offline only for network errors (not 4xx/5xx). **Recommended.** | | `Always` | Fall back to offline on any validation failure. | ## License Lifecycle ### Activation ```csharp var license = await client.ActivateAsync("LICENSE-KEY"); Console.WriteLine($"Activated: {license.Key}"); Console.WriteLine($"Status: {license.Status}"); Console.WriteLine($"Plan: {license.PlanKey}"); ``` ### Validation ```csharp var result = await client.ValidateAsync("LICENSE-KEY"); if (result.Valid) { Console.WriteLine("License is valid!"); Console.WriteLine($"Active Seats: {result.License?.ActiveSeats}/{result.License?.SeatLimit}"); } else { Console.WriteLine($"Invalid: {result.Code} - {result.Message}"); } ``` ### Deactivation ```csharp await client.DeactivateAsync(); ``` ### Get Status ```csharp var status = client.GetStatus(); Console.WriteLine($"Status: {status.StatusType}"); ``` ## Entitlements ### Simple Check ```csharp if (client.HasEntitlement("premium")) { // Unlock premium features } ``` ### Detailed Check ```csharp var entitlement = client.CheckEntitlement("pro-features"); if (entitlement.Active) { EnableProFeatures(); } else { switch (entitlement.Reason) { case EntitlementInactiveReason.Expired: ShowRenewalPrompt(); break; case EntitlementInactiveReason.NotFound: ShowUpgradePrompt(); break; case EntitlementInactiveReason.NoLicense: ShowActivationPrompt(); break; } } ``` ## Event Handling ```csharp // Subscribe to license events client.Events.On(LicenseSeatEvents.LicenseValidated, _ => Console.WriteLine("License validated!")); client.Events.On(LicenseSeatEvents.ValidationFailed, _ => Console.WriteLine("Validation failed!")); client.Events.On(LicenseSeatEvents.EntitlementChanged, _ => Console.WriteLine("Entitlements updated!")); client.Events.On(LicenseSeatEvents.LicenseActivated, license => Console.WriteLine($"Activated: {((License)license).Key}")); client.Events.On(LicenseSeatEvents.LicenseDeactivated, _ => Console.WriteLine("License deactivated")); ``` ## Offline Validation ```csharp var client = new LicenseSeatClient(new LicenseSeatClientOptions { ApiKey = "pk_live_xxxxxxxx", ProductSlug = "your-product", OfflineFallbackMode = OfflineFallbackMode.NetworkOnly, MaxOfflineDays = 7 // Allow 7 days offline }); // Validate - falls back to cached offline token if network fails var result = await client.ValidateAsync("LICENSE-KEY"); if (result.Offline) { Console.WriteLine("Validated offline with cached license"); } ``` The SDK automatically fetches and caches Ed25519-signed offline tokens after activation. When offline: - Validates token signature cryptographically - Checks token expiration (`exp` timestamp) - Detects clock tampering - Returns cached entitlements ## ASP.NET Core Integration ### Dependency Injection ```csharp // Program.cs builder.Services.AddLicenseSeatClient("pk_live_xxxxxxxx", "your-product"); // Or with full options: builder.Services.AddLicenseSeatClient(options => { options.ApiKey = "pk_live_xxxxxxxx"; options.ProductSlug = "your-product"; options.AutoValidateInterval = TimeSpan.FromMinutes(30); }); ``` ### Using in Controllers ```csharp public class LicenseController : ControllerBase { private readonly ILicenseSeatClient _client; public LicenseController(ILicenseSeatClient client) => _client = client; [HttpPost("activate")] public async Task Activate([FromBody] string licenseKey) { var license = await _client.ActivateAsync(licenseKey); return Ok(new { license.Key, license.Status }); } [HttpGet("status")] public IActionResult GetStatus() { var status = _client.GetStatus(); return Ok(new { status.StatusType, status.Message }); } } ``` ## Unity Integration ```csharp using UnityEngine; using LicenseSeat; public class LicenseController : MonoBehaviour { private LicenseSeatManager _manager; void Start() { _manager = FindObjectOfType(); // Subscribe to events _manager.Client.Events.On(LicenseSeatEvents.LicenseValidated, _ => Debug.Log("License validated!")); } public void ActivateLicense(string licenseKey) { StartCoroutine(_manager.ActivateCoroutine(licenseKey, (license, error) => { if (error != null) { Debug.LogError($"Failed: {error.Message}"); return; } Debug.Log($"Activated: {license.Key}"); })); } } ``` **Unity SDK Features:** - **Pure C#** - No native DLLs, works everywhere - **IL2CPP Ready** - Automatic link.xml injection - **WebGL Support** - Uses UnityWebRequest - **Editor Tools** - Settings window, inspectors - **Samples** - Import from Package Manager ## Godot Integration ```csharp using Godot; using LicenseSeat; public partial class LicenseManager : Node { private LicenseSeatClient _client; public override void _Ready() { _client = new LicenseSeatClient(new LicenseSeatClientOptions { ApiKey = "pk_live_xxxxxxxx", ProductSlug = "your-product" }); } public async void ValidateLicense(string licenseKey) { var result = await _client.ValidateAsync(licenseKey); if (result.Valid) GD.Print("License is valid!"); else GD.Print($"Invalid: {result.Code}"); } public override void _ExitTree() => _client?.Dispose(); } ``` ## Error Handling ```csharp try { var license = await client.ActivateAsync("INVALID-KEY"); } catch (ApiException ex) when (ex.Code == "license_not_found") { Console.WriteLine("License key not found"); } catch (ApiException ex) when (ex.Code == "seat_limit_exceeded") { Console.WriteLine($"All {ex.Details?["seat_limit"]} seats are in use"); } catch (ApiException ex) { Console.WriteLine($"API Error: {ex.Code} - {ex.Message}"); Console.WriteLine($"Status: {ex.StatusCode}"); Console.WriteLine($"Retryable: {ex.IsRetryable}"); } ``` ### Common Error Codes - `license_not_found` - Invalid license key - `license_expired` - License has expired - `license_suspended` - License is suspended - `seat_limit_exceeded` - All seats are in use - `device_not_activated` - Device not activated for this license - `invalid_api_key` - Invalid API key ## API Reference ### Client Methods | Method | Description | | ------ | ----------- | | `ActivateAsync(licenseKey)` | Activate a license on this device | | `ValidateAsync(licenseKey)` | Validate a license (check if valid) | | `DeactivateAsync()` | Deactivate the current license | | `HasEntitlement(key)` | Check if an entitlement is active | | `CheckEntitlement(key)` | Get detailed entitlement status | | `GetStatus()` | Get current license status | | `GetCurrentLicense()` | Get the cached license | | `TestAuthAsync()` | Test API connectivity | ### ValidationResult Properties | Property | Type | Description | | -------- | ---- | ----------- | | `Valid` | `bool` | Whether the license is valid | | `Code` | `string?` | Error code if invalid | | `Message` | `string?` | Error message if invalid | | `Offline` | `bool` | True if validated offline | | `License` | `License?` | License data | | `ActiveEntitlements` | `List?` | Active entitlements | ### License Properties | Property | Type | Description | | -------- | ---- | ----------- | | `Key` | `string` | The license key | | `Status` | `string?` | License status (active, expired, etc.) | | `ExpiresAt` | `DateTimeOffset?` | When the license expires | | `PlanKey` | `string?` | Associated plan | | `SeatLimit` | `int?` | Maximum allowed seats | | `ActiveSeats` | `int` | Currently used seats | | `ActiveEntitlements` | `List?` | Active entitlements | ## Platform Support | Platform | Package | Install | | -------- | ------- | ------- | | **.NET** (Console, ASP.NET, WPF, MAUI) | NuGet | `dotnet add package LicenseSeat` | | **Godot 4** | NuGet | `dotnet add package LicenseSeat` | | **Unity** | UPM | [See Unity section](#unity-integration) | ## Next Steps - [[sdks/cpp|C++ SDK]] - For native applications - [[sdks/javascript|JavaScript SDK]] - For web applications - [[api-reference/offline-token|Offline Licensing]] - Air-gapped validation - [[api-reference/index|API Reference]] - Direct API access --- ## Interactive Demo # Interactive SDK Demo Experience the LicenseSeat [[sdks/javascript|JavaScript SDK]] in action. This live demo simulates a real application with license activation, validation, and entitlement checking. ## What You'll See - **Simulated App Window** — A mock desktop application with license activation UI - **SDK Internals** — Real-time view of the SDK's internal state and configuration - **Event Log** — Live stream of SDK events as they fire - **Offline Tools** — Test offline license verification with Ed25519 signatures ## Try It Enter your publishable API key below to get started. Don't have one? [Sign up](https://licenseseat.com/users/sign_up) to create a free account. ## What's Happening Under the Hood When you interact with the demo, you're using the actual [[sdks/javascript|JavaScript SDK]]: 1. **Activation** — `sdk.activate('LICENSE-KEY')` registers your license with a device identifier 2. **Validation** — `sdk.validateLicense()` checks the license status against the server 3. **Entitlements** — `sdk.hasEntitlement('feature')` gates features based on your license 4. **Events** — The SDK emits events like `activation:success` that you can subscribe to ## Next Steps - [[sdks/javascript|JavaScript SDK Documentation]] — Full API reference and integration guides - [[getting-started/2-quickstart|Quickstart]] — Get started in 5 minutes - [[api-reference/offline-token|Offline Licensing]] — Enable offline validation with Ed25519 --- ## SDKs # Official SDKs LicenseSeat provides official SDKs for popular platforms to make integration fast and secure. ## Available SDKs | Platform | Package | Features | | -------- | ------- | -------- | | [[sdks/swift\|Swift]] | `licenseseat-swift` | macOS, iOS, tvOS, watchOS, Linux | | [[sdks/javascript\|JavaScript/TypeScript]] | `@licenseseat/js` | Browsers, Node.js 18+, Full TS support | | [[sdks/csharp\|C#]] | `LicenseSeat` | Unity, Godot, Windows desktop, .NET 6+ | | [[sdks/cpp\|C++]] | `licenseseat` | Unreal Engine, VST/AU plugins, Native apps | ## Feature Parity All SDKs share the same core functionality: - **License Operations**: Activation, deactivation, online/offline validation - **Entitlements**: Check feature access with expiration support - **Offline Support**: Ed25519 signature verification, clock tamper detection - **Resilience**: Automatic retries, network monitoring, background re-validation - **Events**: Subscribe to license lifecycle events - **Security**: Constant-time comparison, secure device fingerprinting Each SDK also includes platform-native integrations (SwiftUI for Swift, React/Vue examples for JS, Unity/Godot for C#, Unreal/JUCE for C++). > **Note**: Release management (querying releases, download tokens) is currently only available in the C++ SDK. ## Choosing an SDK **Use the Swift SDK if you're building:** - macOS applications - iOS/iPadOS apps - tvOS apps - watchOS apps - Cross-platform Swift applications (including Linux servers) **Use the JavaScript SDK if you're building:** - Web applications - Browser extensions - Electron apps - Node.js applications (18+) **Use the C# SDK if you're building:** - Unity games - Godot games (with .NET) - Windows desktop applications (WPF, WinForms, MAUI) - Cross-platform .NET applications - ASP.NET services requiring license validation **Use the C++ SDK if you're building:** - Unreal Engine games - VST/AU/AAX audio plugins - Adobe Photoshop/Illustrator plugins - Native desktop applications - Performance-critical applications ## Core Concepts All SDKs share common concepts and workflows: ### License Lifecycle ``` ┌─────────────┐ ┌────────────┐ ┌────────────┐ │ Inactive │────▶│ Activate │────▶│ Active │ └─────────────┘ └────────────┘ └────────────┘ │ ┌────────────┐ │ │ Deactivate │◀───────────┘ └────────────┘ ``` ### Validation Flow ``` ┌─────────────┐ ┌────────────┐ ┌────────────┐ │ Validate │────▶│ Online? │─Yes─▶│ Server │ └─────────────┘ └────────────┘ │ Validate │ │ └────────────┘ No │ ┌─────▼──────┐ │ Offline │ │ Validation │ └────────────┘ ``` ### Entitlements Entitlements are feature flags attached to licenses. Use them to: - Gate premium features - Implement tiered pricing - Control feature access per license ``` License: "XXXX-YYYY-ZZZZ" ├── Entitlement: "pro-features" (active) ├── Entitlement: "api-access" (active, expires: 2025-12-31) └── Entitlement: "team-seats" (inactive) ``` ## Common Configuration All SDKs accept similar configuration options: | Option | Description | Default | | ------ | ----------- | ------- | | `apiKey` | Your publishable API key | **Required** | | `productSlug` | Your product identifier | **Required** | | `apiBaseUrl` | API endpoint | `https://licenseseat.com/api/v1` | | `autoValidateInterval` | Background re-validation interval | 1 hour | | `maxRetries` | Retry attempts for failed requests | 3 | | `maxOfflineDays` | Offline grace period (0 = disabled) | 0 | | `deviceId` | Custom device ID | Auto-generated | | `debug` | Enable debug logging | false | > **Note**: Time units vary by language convention—Swift uses seconds, JavaScript uses milliseconds, C# uses TimeSpan. See each SDK's documentation for specifics. ## Security Features All SDKs implement: - **Ed25519 Signatures**: Offline licenses are cryptographically signed - **Clock Tamper Detection**: Detects system clock manipulation - **Constant-Time Comparison**: Prevents timing attacks on license keys - **Secure Storage**: Platform-appropriate secure storage mechanisms ## Next Steps - [[sdks/swift|Swift SDK Documentation]] - [[sdks/javascript|JavaScript SDK Documentation]] - [[sdks/csharp|C# SDK Documentation]] - [[sdks/cpp|C++ SDK Documentation]] - [[getting-started/2-quickstart|Quickstart]] - [[api-reference/offline-token|Offline Licensing]] --- ## JavaScript SDK # JavaScript SDK Official JavaScript/TypeScript SDK for LicenseSeat. Full TypeScript support with auto-generated type definitions. ## Installation ### Package Managers ```bash # npm npm install @licenseseat/js # yarn yarn add @licenseseat/js # pnpm pnpm add @licenseseat/js ``` ### CDN (Browser) ```html ``` For version pinning (recommended for production): ```html ``` ## Quick Start ### JavaScript ```javascript import LicenseSeat from '@licenseseat/js'; // Create SDK instance const sdk = new LicenseSeat({ apiKey: 'pk_live_xxxxxxxx', productSlug: 'your-product', // Required debug: true }); // Activate a license await sdk.activate('YOUR-LICENSE-KEY'); // Check entitlements (simple boolean) if (sdk.hasEntitlement('pro')) { enableProFeatures(); } // Get current status const status = sdk.getStatus(); console.log(status); // { status: 'active', license: '...', entitlements: [...] } ``` ### TypeScript ```typescript import LicenseSeat, { type LicenseSeatConfig, type ValidationResult, type EntitlementCheckResult, type LicenseStatus } from '@licenseseat/js'; const config: LicenseSeatConfig = { apiKey: 'pk_live_xxxxxxxx', productSlug: 'your-product', debug: true }; const sdk = new LicenseSeat(config); // Full type inference const result: ValidationResult = await sdk.validateLicense('LICENSE-KEY'); const status: LicenseStatus = sdk.getStatus(); const hasPro: boolean = sdk.hasEntitlement('pro'); ``` TypeScript users get full type support automatically - the package includes generated `.d.ts` declaration files. ## Configuration ```javascript const sdk = new LicenseSeat({ // Required productSlug: 'your-product', // Your product slug from dashboard // Required for authenticated operations apiKey: 'pk_live_xxxxxxxx', // API Configuration apiBaseUrl: 'https://licenseseat.com/api/v1', // Default // Storage storagePrefix: 'licenseseat_', // localStorage key prefix // Initialization autoInitialize: true, // Auto-validate cached license on init // Auto-Validation autoValidateInterval: 3600000, // 1 hour (in ms) // Offline Support offlineFallbackEnabled: false, // Enable offline validation fallback maxOfflineDays: 0, // Max days offline (0 = disabled) offlineLicenseRefreshInterval: 259200000, // 72 hours maxClockSkewMs: 300000, // 5 minutes // Network maxRetries: 3, // Retry attempts for failed requests retryDelay: 1000, // Initial retry delay (ms) networkRecheckInterval: 30000, // Check connectivity every 30s when offline // Debug debug: false // Enable console logging }); ``` ### Configuration Options | Option | Type | Default | Description | | ------ | ---- | ------- | ----------- | | `productSlug` | `string` | — | **Required.** Your product slug from the dashboard | | `apiKey` | `string` | `null` | Your publishable API key | | `apiBaseUrl` | `string` | `https://licenseseat.com/api/v1` | API base URL | | `storagePrefix` | `string` | `licenseseat_` | Prefix for localStorage keys | | `autoInitialize` | `boolean` | `true` | Auto-initialize on construction | | `autoValidateInterval` | `number` | `3600000` | Auto-validation interval (ms) | | `offlineFallbackEnabled` | `boolean` | `false` | Enable offline validation | | `maxOfflineDays` | `number` | `0` | Max offline days (0 = disabled) | | `offlineLicenseRefreshInterval` | `number` | `259200000` | Offline token refresh (72h) | | `maxClockSkewMs` | `number` | `300000` | Max clock skew (5 min) | | `maxRetries` | `number` | `3` | Max API retry attempts | | `retryDelay` | `number` | `1000` | Base retry delay (exponential backoff) | | `networkRecheckInterval` | `number` | `30000` | Network check interval when offline | | `debug` | `boolean` | `false` | Enable debug logging | ## Core Methods ### Activation ```javascript // Basic activation (device ID auto-generated) const result = await sdk.activate('LICENSE-KEY'); // With options const result = await sdk.activate('LICENSE-KEY', { deviceId: 'custom-device-id', // Optional: auto-generated if not provided deviceName: "John's MacBook Pro", // Optional: human-readable device name metadata: { version: '1.0.0' } // Optional: custom metadata }); console.log(result); // { // license_key: 'LICENSE-KEY', // device_id: 'web-abc123', // activated_at: '2024-01-15T10:30:00Z', // activation: { // object: 'activation', // id: 123, // device_id: 'web-abc123', // license_key: 'LICENSE-KEY', // activated_at: '2024-01-15T10:30:00Z', // license: { ... } // } // } ``` ### Deactivation ```javascript const result = await sdk.deactivate(); console.log(result); // { // object: 'deactivation', // activation_id: 123, // deactivated_at: '2024-01-15T12:00:00Z' // } ``` ### Validation ```javascript const result = await sdk.validateLicense('LICENSE-KEY', { deviceId: 'device-id' // Optional: required for hardware_locked mode }); console.log(result); // { // valid: true, // license: { // key: 'LICENSE-KEY', // status: 'active', // mode: 'hardware_locked', // plan_key: 'pro', // active_seats: 1, // seat_limit: 3, // active_entitlements: [ // { key: 'pro', expires_at: null, metadata: null }, // { key: 'beta', expires_at: '2024-12-31T23:59:59Z', metadata: null } // ], // product: { slug: 'your-product', name: 'Your Product' } // }, // active_entitlements: [...] // } ``` ## Entitlements Entitlements are optional. A license may have zero entitlements if the associated plan has none configured. ### Simple Check (Boolean) ```javascript if (sdk.hasEntitlement('pro')) { enableProFeatures(); } if (sdk.hasEntitlement('beta')) { showBetaUI(); } ``` ### Detailed Check ```javascript const result = sdk.checkEntitlement('pro'); if (result.active) { console.log('Entitlement:', result.entitlement); console.log('Expires:', result.entitlement.expires_at); } else { console.log('Reason:', result.reason); // Possible reasons: 'no_license', 'not_found', 'expired' } ``` ### EntitlementCheckResult Type | Property | Type | Description | | -------- | ---- | ----------- | | `active` | `boolean` | Whether the entitlement is active | | `reason` | `string?` | Why inactive: `no_license`, `not_found`, `expired` | | `expires_at` | `string?` | ISO8601 expiration date | | `entitlement` | `Entitlement?` | Full entitlement object if active | ## Status ### Get Current Status ```javascript const status = sdk.getStatus(); switch (status.status) { case 'inactive': showActivationScreen(); break; case 'pending': showLoadingIndicator(); break; case 'active': enableFeatures(status.entitlements); break; case 'offline-valid': enableFeatures(status.entitlements); showOfflineBanner(); break; case 'invalid': showErrorScreen(status.message); break; case 'offline-invalid': showRenewalScreen(); break; } ``` ### Status Values | Status | Description | | ------ | ----------- | | `inactive` | No license activated | | `pending` | License pending validation | | `active` | License valid (online) | | `offline-valid` | License valid (offline verification) | | `invalid` | License invalid | | `offline-invalid` | License invalid (offline) | ### LicenseStatus Type | Property | Type | Description | | -------- | ---- | ----------- | | `status` | `string` | Status value (see above) | | `message` | `string?` | Status message | | `license` | `string?` | License key (if active) | | `device` | `string?` | Device identifier (if active) | | `activated_at` | `string?` | ISO8601 activation timestamp | | `last_validated` | `string?` | ISO8601 last validation timestamp | | `entitlements` | `Entitlement[]?` | Active entitlements | ## Events Subscribe to SDK lifecycle events for reactive UIs. ```javascript // Subscribe const unsubscribe = sdk.on('activation:success', (data) => { console.log('License activated:', data); }); // Unsubscribe unsubscribe(); // or sdk.off('activation:success', handler); ``` ### Available Events | Event | Data | Description | | ----- | ---- | ----------- | | **Lifecycle** | | | | `license:loaded` | `CachedLicense` | Cached license loaded on init | | `sdk:reset` | — | SDK was reset | | `sdk:destroyed` | — | SDK was destroyed | | `sdk:error` | `{ message, error? }` | General SDK error | | **Activation** | | | | `activation:start` | `{ licenseKey, deviceId }` | Activation started | | `activation:success` | `CachedLicense` | Activation succeeded | | `activation:error` | `{ licenseKey, error }` | Activation failed | | **Deactivation** | | | | `deactivation:start` | `CachedLicense` | Deactivation started | | `deactivation:success` | `DeactivationResponse` | Deactivation succeeded | | `deactivation:error` | `{ error, license }` | Deactivation failed | | **Validation** | | | | `validation:start` | `{ licenseKey }` | Validation started | | `validation:success` | `ValidationResult` | Online validation succeeded | | `validation:failed` | `ValidationResult` | Validation failed (invalid) | | `validation:error` | `{ licenseKey, error }` | Validation error (network) | | `validation:offline-success` | `ValidationResult` | Offline validation succeeded | | `validation:offline-failed` | `ValidationResult` | Offline validation failed | | `validation:auth-failed` | `{ licenseKey, error, cached }` | Auth failed during validation | | **Auto-Validation** | | | | `autovalidation:cycle` | `{ nextRunAt: Date }` | Auto-validation scheduled | | `autovalidation:stopped` | — | Auto-validation stopped | | **Network** | | | | `network:online` | — | Network connectivity restored | | `network:offline` | `{ error }` | Network connectivity lost | | **Offline Token** | | | | `offlineToken:fetching` | `{ licenseKey }` | Fetching offline token | | `offlineToken:fetched` | `{ licenseKey, data }` | Offline token fetched | | `offlineToken:fetchError` | `{ licenseKey, error }` | Fetch failed | | `offlineToken:ready` | `{ kid, exp_at }` | Offline assets synced | | `offlineToken:verified` | `{ payload }` | Signature verified | | `offlineToken:verificationFailed` | `{ payload }` | Signature invalid | ## Singleton Pattern For applications that need a shared SDK instance across modules: ```javascript import { configure, getSharedInstance, resetSharedInstance } from '@licenseseat/js'; // Configure once at app startup configure({ apiKey: 'pk_live_xxxxxxxx', productSlug: 'your-product' }); // Use anywhere in your app const sdk = getSharedInstance(); await sdk.activate('LICENSE-KEY'); // Reset if needed (clears all state) resetSharedInstance(); ``` ## Lazy Initialization By default, the SDK initializes immediately and validates any cached license. To disable this: ```javascript const sdk = new LicenseSeat({ apiKey: 'pk_live_xxxxxxxx', productSlug: 'your-product', autoInitialize: false // Don't auto-initialize }); // Later, when ready: sdk.initialize(); ``` This is useful when you need to: - Delay network requests until user interaction - Set up event listeners before initialization - Control exactly when validation occurs ## Offline Support The SDK supports offline license validation using Ed25519 cryptographic signatures. ### Enable Offline Fallback ```javascript const sdk = new LicenseSeat({ apiKey: 'pk_live_xxxxxxxx', productSlug: 'your-product', offlineFallbackEnabled: true, // Enable offline fallback maxOfflineDays: 7 // Allow 7 days offline }); ``` ### How It Works 1. **Online**: License validated against server 2. **Activation**: Offline token + public key automatically cached 3. **Offline**: Cached token verified cryptographically (Ed25519) 4. **Clock Tamper Detection**: Prevents users from rolling back system clock ### Manual Offline Methods ```javascript // Sync offline assets after activation await sdk.syncOfflineAssets(); // Get offline token from server const token = await sdk.getOfflineToken(); console.log(token); // { // object: 'offline_token', // token: { license_key, product_slug, plan_key, ... }, // signature: { algorithm: 'Ed25519', key_id, value }, // canonical: '...' // } // Get signing key const signingKey = await sdk.getSigningKey('key-id-001'); // Verify signature locally const isValid = await sdk.verifyOfflineToken(token, signingKey.public_key); ``` ### Offline Token Structure ```javascript { object: 'offline_token', token: { schema_version: 1, license_key: 'LICENSE-KEY', product_slug: 'your-product', plan_key: 'pro', mode: 'hardware_locked', device_id: 'web-abc123', iat: 1704067200, // Issued at (Unix timestamp) exp: 1706659200, // Expires at (Unix timestamp) nbf: 1704067200, // Not before (Unix timestamp) license_expires_at: null, kid: 'key-id-001', entitlements: [ { key: 'pro', expires_at: null } ], metadata: {} }, signature: { algorithm: 'Ed25519', key_id: 'key-id-001', value: 'base64url-encoded-signature' }, canonical: '{"entitlements":[...],"exp":...}' } ``` ### Offline Validation Result When offline, `validateLicense()` returns with `offline: true`: ```javascript const result = await sdk.validateLicense('LICENSE-KEY'); if (result.offline) { console.log('Validated offline'); } ``` ## Error Handling The SDK exports custom error classes for precise error handling: ```javascript import LicenseSeat, { APIError, LicenseError, ConfigurationError, CryptoError } from '@licenseseat/js'; try { await sdk.activate('INVALID-KEY'); } catch (error) { if (error instanceof APIError) { console.log('HTTP Status:', error.status); console.log('Error Code:', error.data?.error?.code); console.log('Error Message:', error.data?.error?.message); } else if (error instanceof LicenseError) { console.log('License error:', error.code); } else if (error instanceof CryptoError) { console.log('Crypto error:', error.message); } } ``` ### Error Types | Error | Properties | Description | | ----- | ---------- | ----------- | | `APIError` | `status`, `data` | HTTP request failures | | `LicenseError` | `code` | License operation failures | | `ConfigurationError` | — | SDK misconfiguration | | `CryptoError` | — | Cryptographic operation failures | ### API Error Format ```javascript { error: { code: 'license_not_found', // Machine-readable error code message: 'License not found.', // Human-readable message details: { ... } // Optional additional details } } ``` ### Common Error Codes - `unauthorized` - Invalid or missing API key - `license_not_found` - License key doesn't exist - `license_expired` - License has expired - `license_suspended` - License is suspended - `license_revoked` - License has been revoked - `seat_limit_reached` - No more seats available - `device_already_activated` - Device is already activated - `activation_not_found` - Activation doesn't exist (for deactivation) ## Utility Methods ### Test Authentication ```javascript try { const result = await sdk.testAuth(); console.log('Authenticated:', result.authenticated); // Always true if succeeds console.log('Healthy:', result.healthy); // API health status console.log('API Version:', result.api_version); // e.g., '1.0.0' } catch (error) { console.error('Auth failed:', error); } ``` > **Note:** This tests API connectivity, not API key validity. A successful response means the API is reachable. ### Reset SDK Clears all cached data and stops timers: ```javascript sdk.reset(); ``` ### Destroy SDK Fully destroys the instance and releases all resources: ```javascript sdk.destroy(); // Do not use sdk after this ``` ## TypeScript Types All types are exported from the package: ```typescript import type { LicenseSeatConfig, ActivationOptions, ValidationOptions, ValidationResult, EntitlementCheckResult, LicenseStatus, Entitlement, CachedLicense, ActivationResponse, DeactivationResponse, OfflineToken } from '@licenseseat/js'; ``` ## React Integration ```jsx import { useState, useEffect, createContext, useContext } from 'react'; import LicenseSeat from '@licenseseat/js'; // Context const LicenseContext = createContext(null); // Provider export function LicenseProvider({ children, config }) { const [sdk] = useState(() => new LicenseSeat(config)); const [status, setStatus] = useState(sdk.getStatus()); useEffect(() => { const events = [ 'activation:success', 'deactivation:success', 'validation:success', 'validation:failed', 'validation:offline-success', 'validation:offline-failed' ]; const unsubscribers = events.map(event => sdk.on(event, () => setStatus(sdk.getStatus())) ); return () => { unsubscribers.forEach(unsub => unsub()); sdk.destroy(); // Clean up on unmount }; }, [sdk]); return ( {children} ); } // Hook export function useLicense() { return useContext(LicenseContext); } // Usage function App() { return ( ); } function MainApp() { const { sdk, status } = useLicense(); if (status.status === 'active') { return ; } return ; } ``` ## Browser Usage (No Build Tools) ```html LicenseSeat Demo
``` ## Node.js Usage The SDK is designed for browsers but works in Node.js with polyfills: ```javascript // Required polyfills for Node.js const storage = {}; globalThis.localStorage = { getItem(key) { return Object.prototype.hasOwnProperty.call(storage, key) ? storage[key] : null; }, setItem(key, value) { storage[key] = String(value); }, removeItem(key) { delete storage[key]; }, clear() { for (const key in storage) delete storage[key]; }, }; const originalKeys = Object.keys; Object.keys = function(obj) { if (obj === globalThis.localStorage) return originalKeys(storage); return originalKeys(obj); }; globalThis.document = { createElement: () => ({ getContext: () => null }), querySelector: () => null }; globalThis.window = { navigator: {}, screen: {} }; globalThis.navigator = { userAgent: 'Node.js', language: 'en', hardwareConcurrency: 4 }; // Now import the SDK const { default: LicenseSeat } = await import('@licenseseat/js'); ``` > **Note:** In Node.js, device fingerprinting uses fallback values. For consistent device identification, pass an explicit `deviceId` to `activate()`. ## Platform Support | Platform | Version | Notes | | -------- | ------- | ----- | | Chrome | 80+ | Full support | | Firefox | 75+ | Full support | | Safari | 14+ | Full support | | Edge | 80+ | Full support | | Node.js | 18+ | Requires polyfills | | Electron | Latest | Full support | ## Next Steps - [[sdks/swift|Swift SDK]] - For Apple platforms - [[api-reference/offline-token|Offline Licensing]] - Air-gapped validation - [[api-reference/index|API Reference]] - Direct API access --- ## Swift SDK # Swift SDK The official Swift SDK for LicenseSeat provides a comprehensive, type-safe API for managing software licenses on Apple platforms. ## Installation ### Swift Package Manager Add to your `Package.swift`: ```swift dependencies: [ .package(url: "https://github.com/licenseseat/licenseseat-swift.git", from: "0.3.1") ] ``` Then add the dependency to your target: ```swift .target( name: "YourApp", dependencies: [ .product(name: "LicenseSeat", package: "licenseseat-swift") ] ) ``` ### Xcode 1. File > Add Package Dependencies 2. Enter: `https://github.com/licenseseat/licenseseat-swift.git` 3. Select your version requirements and add to your target ## Quick Start ```swift import LicenseSeat // 1. Configure at app launch LicenseSeatStore.shared.configure( apiKey: "pk_live_xxxxxxxx", productSlug: "your-product" // Required ) // 2. Activate a license let license = try await LicenseSeatStore.shared.activate("USER-LICENSE-KEY") print("Device ID: \(license.deviceId)") // 3. Check status switch LicenseSeatStore.shared.status { case .active(let details): print("Licensed! Device: \(details.device)") case .inactive: print("No license activated") default: break } ``` ## Configuration ### Basic Configuration ```swift LicenseSeatStore.shared.configure( apiKey: "pk_live_xxxxxxxx", productSlug: "your-product" ) ``` ### Advanced Configuration ```swift let config = LicenseSeatConfig( apiBaseUrl: "https://licenseseat.com/api/v1", // v1 API endpoint apiKey: "pk_live_xxxxxxxx", productSlug: "your-product", // Required for all operations storagePrefix: "myapp_", autoValidateInterval: 3600, // Re-validate every hour maxRetries: 3, retryDelay: 1, offlineFallbackMode: .networkOnly, // Offline fallback strategy maxOfflineDays: 7, // 7-day grace period maxClockSkewMs: 300000, // 5-minute clock tolerance debug: true ) let licenseSeat = LicenseSeat(config: config) ``` Or using the builder-style API: ```swift LicenseSeatStore.shared.configure( apiKey: "pk_live_xxxxxxxx", productSlug: "your-product" ) { config in config.autoValidateInterval = 3600 config.offlineFallbackMode = .networkOnly config.maxOfflineDays = 7 config.debug = true } ``` ### Configuration Options | Option | Type | Default | Description | | ------ | ---- | ------- | ----------- | | `apiBaseUrl` | `String` | `https://licenseseat.com/api/v1` | v1 API endpoint | | `apiKey` | `String?` | `nil` | Your publishable API key | | `productSlug` | `String?` | `nil` | **Required.** Product identifier | | `storagePrefix` | `String` | `licenseseat_` | Prefix for cache keys | | `deviceIdentifier` | `String?` | Auto-generated | Custom device ID | | `autoValidateInterval` | `TimeInterval` | `3600` (1 hour) | Background validation interval | | `networkRecheckInterval` | `TimeInterval` | `30` | Offline connectivity check interval | | `maxRetries` | `Int` | `3` | API retry attempts | | `retryDelay` | `TimeInterval` | `1` | Base retry delay (exponential backoff) | | `offlineFallbackMode` | `OfflineFallbackMode` | `.networkOnly` | Offline fallback strategy | | `offlineTokenRefreshInterval` | `TimeInterval` | `259200` (72 hours) | Offline token refresh interval | | `maxOfflineDays` | `Int` | `0` | Grace period for offline use | | `maxClockSkewMs` | `TimeInterval` | `300000` (5 min) | Clock tamper tolerance | | `debug` | `Bool` | `false` | Enable debug logging | ### Offline Fallback Modes | Mode | Description | | ---- | ----------- | | `.networkOnly` | Falls back to offline validation only for network errors (timeouts, connectivity issues, 5xx). Business logic errors (4xx) immediately invalidate. **Recommended.** | | `.always` | Always attempts offline validation on any failure. | ## License Lifecycle ### Activation ```swift do { let license = try await LicenseSeatStore.shared.activate("USER-LICENSE-KEY") print("Activated: \(license.licenseKey)") print("Device ID: \(license.deviceId)") print("Activation ID: \(license.activationId)") } catch let error as APIError { print("Activation failed: \(error.code ?? "unknown") - \(error.message)") } ``` With options: ```swift let license = try await LicenseSeatStore.shared.activate( "USER-LICENSE-KEY", options: ActivationOptions( deviceId: "custom-device-id", deviceName: "User's MacBook Pro", metadata: ["version": "2.0.0", "environment": "production"] ) ) ``` ### Deactivation ```swift try await LicenseSeatStore.shared.deactivate() ``` ### Validation ```swift let result = try await LicenseSeatStore.shared.validate(licenseKey: "USER-LICENSE-KEY") if result.valid { print("License is valid") print("Plan: \(result.license.planKey)") print("Status: \(result.license.status)") // Check entitlements from validation for entitlement in result.license.activeEntitlements { print("Entitlement: \(entitlement.key)") if let expiresAt = entitlement.expiresAt { print(" Expires: \(expiresAt)") } } } else { print("Invalid: \(result.code ?? "unknown")") print("Message: \(result.message ?? "")") } ``` ## Status Checking ### Get Current Status ```swift switch LicenseSeatStore.shared.status { case .inactive(let message): print("No license: \(message)") showActivationScreen() case .pending(let message): print("Validating: \(message)") showLoadingIndicator() case .active(let details): print("Licensed to: \(details.license)") print("Device: \(details.device)") enableFeatures() case .offlineValid(let details): print("Valid offline until next sync") enableFeatures() showOfflineBanner() case .invalid(let message): print("Invalid: \(message)") showErrorScreen(message) case .offlineInvalid(let message): print("Expired offline: \(message)") showRenewalScreen() } ``` ### Status Types | Status | Description | | ------ | ----------- | | `.inactive` | No license activated | | `.pending` | License pending validation | | `.active` | License is valid (online) | | `.offlineValid` | License is valid (offline check) | | `.invalid` | License is invalid | | `.offlineInvalid` | License invalid (offline, e.g., expired) | ## Entitlements Check feature access based on license entitlements: ```swift let status = LicenseSeatStore.shared.entitlement("premium-features") switch status.reason { case nil where status.active: enablePremiumFeatures() case .expired: showRenewalPrompt(expiresAt: status.expiresAt) case .notFound: showUpgradePrompt() case .noLicense: showActivationPrompt() default: disablePremiumFeatures() } // Access entitlement details if let entitlement = status.entitlement { print("Entitlement key: \(entitlement.key)") if let metadata = entitlement.metadata { print("Metadata: \(metadata)") } } ``` ### Reactive Entitlement Monitoring ```swift LicenseSeatStore.shared.entitlementPublisher(for: "api-access") .receive(on: DispatchQueue.main) .sink { status in apiAccessEnabled = status.active if let expiresAt = status.expiresAt { scheduleExpirationWarning(at: expiresAt) } } .store(in: &cancellables) ``` ## SwiftUI Integration ### Property Wrappers The SDK provides property wrappers for reactive SwiftUI apps: ```swift import SwiftUI import LicenseSeat @main struct MyApp: App { init() { LicenseSeatStore.shared.configure( apiKey: ProcessInfo.processInfo.environment["LICENSESEAT_API_KEY"] ?? "", productSlug: "my-app" ) } var body: some Scene { WindowGroup { ContentView() } } } struct ContentView: View { @LicenseState private var status // Auto-updates on license changes @EntitlementState("pro") private var hasPro // Feature flag var body: some View { switch status { case .active, .offlineValid: MainAppView() .environment(\.proEnabled, hasPro) case .inactive: ActivationView() case .invalid(let message): ErrorView(message: message) case .pending: ProgressView("Validating...") case .offlineInvalid: ExpiredView() } } } struct ActivationView: View { @State private var licenseKey = "" @State private var isLoading = false @State private var error: String? var body: some View { Form { TextField("License Key", text: $licenseKey) Button("Activate") { Task { isLoading = true defer { isLoading = false } do { try await LicenseSeatStore.shared.activate(licenseKey) } catch let apiError as APIError { self.error = apiError.message } catch { self.error = error.localizedDescription } } } .disabled(licenseKey.isEmpty || isLoading) if let error { Text(error).foregroundColor(.red) } } } } ``` ## UIKit / AppKit Integration Use Combine publishers for reactive updates: ```swift import LicenseSeat import Combine class LicenseManager: ObservableObject { @Published var isLicensed = false @Published var hasProFeatures = false private var cancellables = Set() init() { LicenseSeatStore.shared.configure( apiKey: "pk_live_xxxxxxxx", productSlug: "your-product" ) // React to license status changes LicenseSeatStore.shared.$status .receive(on: DispatchQueue.main) .sink { [weak self] status in switch status { case .active, .offlineValid: self?.isLicensed = true default: self?.isLicensed = false } } .store(in: &cancellables) // Monitor specific entitlements LicenseSeatStore.shared.entitlementPublisher(for: "pro-features") .map { $0.active } .receive(on: DispatchQueue.main) .assign(to: &$hasProFeatures) } func activate(_ key: String) async throws { try await LicenseSeatStore.shared.activate(key) } func deactivate() async throws { try await LicenseSeatStore.shared.deactivate() } } ``` ## Event System Subscribe to SDK events for analytics, UI updates, or custom logic: ```swift // Subscribe with closure (returns AnyCancellable) let cancellable = licenseSeat.on("activation:success") { data in print("License activated!") Analytics.track("license_activated") } // Unsubscribe by cancelling cancellable.cancel() // Or use Combine publishers licenseSeat.eventPublisher .filter { $0.name.hasPrefix("validation:") } .sink { event in switch event.name { case "validation:success": updateUI() case "validation:offline-success": showOfflineBanner() case "license:revoked": lockFeatures() default: break } } .store(in: &cancellables) ``` ### Available Events | Event | Description | | ----- | ----------- | | `activation:start/success/error` | License activation lifecycle | | `validation:start/success/failed/error` | Online validation | | `validation:offline-success/offline-failed` | Offline validation | | `deactivation:start/success/error` | License deactivation | | `license:loaded` | Cached license loaded at startup | | `license:revoked` | License revoked by server | | `offlineToken:verified` | Offline token signature verified | | `offlineToken:verificationFailed` | Offline token verification failed | | `autovalidation:cycle` | Auto-validation cycle triggered | | `network:online/offline` | Connectivity changes | | `sdk:reset` | SDK state cleared | ## Offline Validation The SDK provides seamless offline support with Ed25519 cryptographic verification: ```swift LicenseSeatStore.shared.configure( apiKey: "pk_live_xxxxxxxx", productSlug: "your-product" ) { config in config.offlineFallbackMode = .networkOnly // Network-first fallback mode config.maxOfflineDays = 7 // 7-day grace period config.offlineTokenRefreshInterval = 259200 // Refresh every 72 hours } ``` ### How Offline Validation Works 1. On activation, the SDK fetches a signed offline token from the server 2. The token contains license data, entitlements, and an Ed25519 signature 3. When offline, the SDK verifies the signature locally 4. Clock tamper detection prevents users from bypassing expiration ### Security Features - **Ed25519 Signatures**: Offline licenses are cryptographically signed - **Clock Tamper Detection**: Detects system clock manipulation - **Constant-Time Comparison**: Prevents timing attacks on license keys - **Grace Period**: Configurable offline validity period ## API Response Format The v1 API uses Stripe-style conventions with `object` fields identifying response types. ### Activation Response ```json { "object": "activation", "id": 12345, "device_id": "mac_abc123", "device_name": "User's MacBook", "license_key": "LICENSE-KEY", "activated_at": "2025-01-15T10:30:00Z", "license": { "object": "license", "key": "LICENSE-KEY", "status": "active", "mode": "hardware_locked", "plan_key": "pro", "seat_limit": 5, "active_seats": 1, "active_entitlements": [ {"key": "premium", "expires_at": null, "metadata": null} ], "product": {"slug": "my-app", "name": "My App"} } } ``` ### Deactivation Response ```json { "object": "deactivation", "activation_id": 12345, "deactivated_at": "2025-01-15T12:00:00Z" } ``` ## Error Handling ```swift do { try await LicenseSeatStore.shared.activate("INVALID-KEY") } catch let error as APIError { print("Error code: \(error.code ?? "unknown")") print("Message: \(error.message)") print("Details: \(error.details ?? [:])") } ``` ### Common Error Codes | Code | Description | | ---- | ----------- | | `license_not_found` | License key doesn't exist | | `license_expired` | License has expired | | `license_suspended` | License has been suspended | | `seat_limit_exceeded` | No available seats | | `device_mismatch` | Device ID doesn't match activation | | `product_mismatch` | License not valid for this product | ## Platform Support | Platform | Minimum Version | Notes | | -------- | --------------- | ----- | | macOS | 12.0+ | Full support including hardware UUID | | iOS | 13.0+ | Full support | | tvOS | 13.0+ | Full support | | watchOS | 8.0+ | Core features (no Network.framework) | ## Testing Authentication Test that your API key is configured correctly: ```swift do { try await LicenseSeatStore.shared.testAuth() print("API key is valid") } catch { print("Auth test failed: \(error)") } ``` ## Debug Report Generate a diagnostic report for support: ```swift let report = LicenseSeatStore.shared.debugReport() print(report) // Contains: SDK version, status, license key prefix (redacted), timestamps, etc. ``` ## Reset SDK Clear all cached data and reset to initial state: ```swift LicenseSeatStore.shared.reset() ``` ## Next Steps - [[sdks/javascript|JavaScript SDK]] - For web applications - [[api-reference/offline-token|Offline Licensing]] - Air-gapped validation - [[api-reference/index|API Reference]] - Direct API access ---