# LicenseSeat Documentation > Complete documentation for LicenseSeat - Easy, secure licensing server and license keys for your games, plugins, and apps ## Documentation - [LicenseSeat Documentation](/) - [Air-Gapped Licensing](/guides-air-gapped-licensing/) - [Offline Licensing](/guides-offline-licensing/) - [Guides](/guides/) - [Entitlements](/entitlements/) - [Generate Download Token](/api-reference-download-token/) - [API Quickstart](/api-reference/) - [Get Signing Key](/api-reference-signing-keys/) - [Telemetry](/api-reference-telemetry/) - [C++ SDK](/sdks-cpp/) - [C# SDK](/sdks-csharp/) - [Interactive SDK Demo](/sdks-demo/) - [SDKs](/sdks/) - [JavaScript SDK](/sdks-javascript/) - [Rust SDK](/sdks-rust/) - [Swift SDK](/sdks-swift/) --- ## Full Content ## LicenseSeat Documentation LicenseSeat is a licensing server for games, plugins, and apps. It allows you to easily issue license keys, validate them in your software, and manage activations, all without rolling your own infrastructure. ## The end goal The end goal is simple: to have a box in your app where users can enter their license key: ``` ┌────────────────────────────────────────────┐ │ Enter your license key: │ │ ┌──────────────────────────────────────┐ │ │ │ XXXX-XXXX-XXXX-XXXX │ │ │ └──────────────────────────────────────┘ │ │ [Activate] │ └────────────────────────────────────────────┘ ```

License activation dialog in the SynthDemo app
Example: License activation in the SynthDemo app (C++ SDK)

Which only allows access to legitimate users who have bought the product and have an active license. You just need to provide the UI box, and we'll handle everything else! ## Connect your app to LicenseSeat To get everything wired and ready, you just need to add the LicenseSeat SDK to your app / game / plugin! It's super easy and takes less than 10 lines of code: ```javascript import LicenseSeat from '@licenseseat/js'; // 1. Initialize the SDK const sdk = new LicenseSeat({ apiKey: 'pk_live_xxxxxxxxxxxxxxxxxxxxx', productSlug: 'my-product' }); // 2. Activate a license const license = await sdk.activate('TEST-XXXX-XXXX-XXXX'); console.log('License activated!', license); ``` We provide SDKs for the most popular platforms for apps, games, and plugins: | Platform | Package | | ------------------------- | --------------------- | | **JavaScript / TypeScript** | [[sdks/javascript\|@licenseseat/js]] | | **Swift** | [[sdks/swift\|licenseseat-swift]] | | **C#** | [[sdks/csharp\|licenseseat-csharp]] | | **C++** | [[sdks/cpp\|licenseseat-cpp]] | All SDKs support: - License activation and deactivation - Automatic background checks - Heartbeats + app telemetry so you can get analytics about your product and users - [[guides/offline-licensing|Offline validation]] — works on planes, in China, anywhere without internet - [[entitlements|Entitlements]] — feature flags tied to licenses - And much more See the [[sdks/index\|SDK documentation]] for detailed integration guides. ## Or use the API directly If we don't have an SDK for your language, or if you just want to use the API directly, you can do so easily! There's extensive API documentation, along with the OpenAPI spec etc available at the [LicenseSeat API docs](/api). ## One last thing: connect your payments platform! You're probably already selling your product through Stripe, Gumroad, Lemon Squeezy, Shopify, etc. You can automate license issuing so that every time a new customer purchases your product, LicenseSeat automatically issues and emails a new license for them! ![Payment platforms supported by LicenseSeat](images/payment-platforms.webp) Here's how it works: 1. User purchases your app → You receive the payment via Stripe/Gumroad/etc. as usual 2. LicenseSeat receives the purchase event from Stripe/Gumroad/etc. → We issue a license key automatically 3. User receives license key via email with your own product branding Here's what your fully branded email (sent by LicenseSeat to your customer) looks like: ![Branded license purchase email sent by LicenseSeat to your customer](images/branded-license-purchase-email.webp) ## You're ready to get started! If you haven't already, [sign up on LicenseSeat](https://licenseseat.com/users/sign_up), go through the onboarding, and you'll get your API key and everything needed to add LicenseSeat to your app! ## Next steps When you're ready: - [[sdks/index|SDK Documentation]] — Integrate your app with LicenseSeat - [[guides/index|Guides]] — In-depth guides for offline licensing, payment integrations, and more - [[api-reference/index|API Reference]] — Direct API access for custom integrations --- ## Air-Gapped Licensing # Air-Gapped Licensing For devices that never connect to the internet, LicenseSeat currently supports an **operator-assisted machine-file workflow**: 1. Collect the target machine fingerprint on the air-gapped device. 2. Use a connected admin/service machine to call `activate` for that fingerprint. 3. Call `machine-file` for the same fingerprint. 4. Transfer the resulting machine file back via USB or other approved media. 5. Verify it locally on the air-gapped device. This works today. It consumes a seat correctly, preserves device binding, and does not require a dedicated challenge-response protocol. > **Important:** LicenseSeat does **not** currently ship a first-class challenge-response product flow, QR ceremony, offline-activation endpoint, or dashboard wizard for air-gapped provisioning. If you need those, build the current API-assisted workflow first and treat challenge-response as future product work. ## Current Supported Workflow ### Step 1: Collect the fingerprint on the target machine Your app, helper CLI, or provisioning tool should print or export the machine fingerprint from the target device. Examples: - A small helper app that displays the fingerprint on screen - A CLI that writes the fingerprint to a text file for USB transfer - A local admin page that exports fingerprint + optional device name ### Step 2: Activate from a connected machine Activation is what consumes the seat. ```bash curl -X POST "https://licenseseat.com/api/v1/products/my-app/licenses/LICENSE-KEY/activate" \ -H "Authorization: Bearer pk_live_xxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "fingerprint": "target-machine-fingerprint", "device_name": "Factory PLC 07" }' ``` ### Step 3: Issue the machine file Machine-file issuance does not consume an extra seat. It requires the activation from the previous step to already exist. ```bash curl -X POST "https://licenseseat.com/api/v1/products/my-app/licenses/LICENSE-KEY/machine-file" \ -H "Authorization: Bearer pk_live_xxxxxxxx" \ -H "Content-Type: application/json" \ -d '{ "fingerprint": "target-machine-fingerprint", "ttl": 365, "include": ["license"] }' ``` The response contains the PEM-like machine-file certificate. Save it to disk and transfer it to the target system. ### Step 4: Import or place the machine file on the target machine What this looks like depends on your SDK/app: - Newer SDKs such as C++ can verify the machine file directly. - Older SDKs may still use signed offline tokens instead of machine files and have not migrated yet. - A custom integration can store the certificate in your own secure local storage and verify it on startup. ## Why this is secure - **Seat consumption happens first**: you cannot mint unlimited offline credentials without activating. - **Fingerprint binding is preserved**: the machine file is encrypted using the license key and target fingerprint. - **Tamper detection is built in**: the machine file is signed with Ed25519. - **Machine files stay finite**: use longer TTLs for true air-gapped systems, but not infinite ones. ## Choosing TTLs for air-gapped systems Recommended starting points: | Environment | Suggested TTL | Suggested grace period | |-------------|---------------|------------------------| | Consumer / laptops | 30 days | 3-7 days | | Professional field equipment | 90-365 days | 7-30 days | | Strictly air-gapped industrial systems | 365-3650 days | 7-30 days | Long TTLs are a tradeoff: - better operational convenience - slower revocation visibility - slower entitlement/metadata change visibility The underlying license expiry still wins. A machine file does not outlive the license itself. ## What is not productized yet These are sensible future improvements, but they are **not** current built-in features: - Challenge-response encoded blobs - QR-based offline activation/import ceremonies - One-click dashboard action: “paste fingerprint → activate → download machine file” - Batch renewal packages from the dashboard - Dedicated offline-activation endpoints separate from `activate` + `machine-file` ## Best Practices 1. Keep a documented record of which physical device maps to which fingerprint. 2. Use a friendly `device_name` when activating so audit logs stay readable. 3. Prefer machine files over legacy offline tokens for new air-gapped deployments. 4. Choose a TTL intentionally instead of asking for “never expires”. 5. Test the full USB/manual provisioning workflow before shipping to a remote site. --- ## Offline Licensing # Offline Licensing LicenseSeat provides robust offline licensing that lets your users work without an internet connection while maintaining security and preventing license abuse. Whether your user is on a 14-hour flight, touring China for a month with unreliable internet, or operating equipment in a remote location—their software keeps working. > **Current offline model:** machine files are the preferred offline artifact. They are encrypted, activation-bound, and fingerprint-bound. Older SDKs may still use signed offline tokens until they migrate, but new integrations should treat tokens as a legacy compatibility path. ## The Mental Model Think of offline licensing like a boarding pass: 1. **Check-in (Activation)**: Your user activates their license online. This "checks them in" and reserves their seat. 2. **Boarding Pass (Machine File)**: They receive a cryptographically signed "boarding pass" that proves they're authorized. 3. **Offline Travel**: They can board the plane (use your software) without needing to call the airline (your server) again—the boarding pass is proof enough. 4. **Expiration**: The boarding pass has an expiration. Eventually, they need to check in again. The key insight: **the seat is consumed at activation, not when generating offline credentials**. This prevents users from getting unlimited offline artifacts without using their seat allocation. --- ## How It Works ``` ┌──────────────────────────────────────────────────────────────────────────────┐ │ ONLINE (Internet Available) │ ├──────────────────────────────────────────────────────────────────────────────┤ │ │ │ 1. User enters license key │ │ │ │ │ ▼ │ │ 2. SDK calls: POST /activate │ │ │ │ │ ▼ │ │ 3. Server validates license, consumes seat, creates activation │ │ │ │ │ ▼ │ │ 4. SDK automatically fetches "machine file" (offline credentials) │ │ │ │ │ ▼ │ │ 5. Machine file stored securely on device │ │ │ └──────────────────────────────────────────────────────────────────────────────┘ │ │ User goes offline ▼ ┌──────────────────────────────────────────────────────────────────────────────┐ │ OFFLINE (No Internet) │ ├──────────────────────────────────────────────────────────────────────────────┤ │ │ │ 1. App launches, SDK tries online validation → fails (no network) │ │ │ │ │ ▼ │ │ 2. SDK falls back to offline validation │ │ │ │ │ ▼ │ │ 3. Machine file signature verified (Ed25519 cryptography) │ │ │ │ │ ▼ │ │ 4. Machine file decrypted using device fingerprint │ │ (Only works on the ORIGINAL device—not transferable!) │ │ │ │ │ ▼ │ │ 5. Expiry checked → License valid! │ │ │ └──────────────────────────────────────────────────────────────────────────────┘ ``` ### Why This Is Secure 1. **Activation required first**: You can't skip straight to offline. The seat must be consumed. 2. **Device-bound**: The machine file is encrypted with a key derived from your device's unique fingerprint. Copy it to another computer? It won't decrypt. 3. **Cryptographically signed**: The server signs the machine file. Tamper with it? The signature fails. 4. **Time-limited**: Machine files expire. Users must eventually reconnect. --- ## Real-World Scenarios ### Scenario 1: Hustl User on a Flight **Context**: Sarah uses [Hustl](https://gohustl.co) (a macOS productivity app) and is about to board a 14-hour flight to Tokyo. **What happens:** | Timeline | What Sarah Does | What LicenseSeat Does | |----------|-----------------|----------------------| | **Before flight** | Opens Hustl, already activated | SDK refreshes machine file in background (valid for 30 days) | | **On the plane** | Enables airplane mode, opens Hustl | SDK tries online validation → timeout → falls back to offline | | **During flight** | Works for 14 hours | SDK verifies machine file signature locally, checks expiry → valid! | | **Lands in Tokyo** | Connects to hotel WiFi | SDK auto-validates online, refreshes machine file | **Sarah's experience**: Completely seamless. She didn't even notice the offline/online transitions. ### Scenario 2: Music Producer on a China Tour **Context**: Alex is a DJ with a $299 music plugin (like a synth or effects processor). He's touring China for 30 days where internet is unreliable and VPNs often get blocked. **What happens:** | Timeline | What Alex Does | What LicenseSeat Does | |----------|-----------------|----------------------| | **Before trip** | Opens plugin, clicks "Prepare for Offline" | SDK requests extended machine file (30-day TTL) | | **Day 1-15** | Performs at venues, no reliable internet | Offline validation succeeds every time | | **Day 20** | Plugin shows "Offline license expires in 10 days" | SDK warning based on machine file expiry | | **Day 25** | Finds working VPN for 5 minutes | SDK opportunistically refreshes machine file | | **Day 26-30** | Back to no internet | Continues working with refreshed credentials | **Alex's experience**: One minor warning notification. Zero interruption to his performances. ### Scenario 3: Industrial Equipment in Remote Location **Context**: A robotics company deploys autonomous equipment with your licensed software. The equipment operates in locations with zero internet access for months at a time. **What happens:** | Timeline | What Operator Does | What LicenseSeat Does | |----------|-----------------|----------------------| | **At HQ** | Provisions device with license | Standard activation → gets 365-day machine file | | **Deployment** | Ships to remote site, no internet | N/A | | **Day 1-180** | Equipment operates autonomously | Offline validation on each startup | | **Day 300** | Equipment shows "License expires in 65 days" | Warning based on machine file expiry | | **Before expiry** | Technician visits site with laptop | Repeats the manual air-gapped renewal flow: activate/check machine file from a connected admin system, then transfer the refreshed certificate | **Operator's experience**: Set it and forget it for almost a year. --- ## Configuration Options You control offline behavior per license plan in your LicenseSeat dashboard: | Setting | Description | Recommended | |---------|-------------|-------------| | **Offline TTL** | How long the machine file is valid | 30 days for consumer apps, 90-365 days for enterprise | | **Grace Period** | Extra time after TTL expires before hard lockout | 7 days (gives users time to reconnect) | | **Require Activation** | Must activate online before getting offline credentials | Always enabled for security | ### Example Plan Configurations **Consumer App (Hustl-style)**: - Offline TTL: 30 days - Grace Period: 7 days - Seat Limit: 2 devices **Professional Plugin (Music production)**: - Offline TTL: 45 days - Grace Period: 30 days - Seat Limit: 2 devices **Enterprise/Industrial**: - Offline TTL: 365 days - Grace Period: 30 days - Seat Limit: 10+ devices --- ## SDK Integration ### Automatic (Recommended) The SDK handles everything automatically. Just configure and forget. Depending on the SDK generation: - newer SDKs such as C++ cache machine files - older SDKs may still cache signed offline tokens until they migrate ```cpp licenseseat::Config config; config.api_key = "pk_live_xxxxxxxx"; config.product_slug = "my-app"; config.storage_path = "/path/to/cache"; licenseseat::Client client(config); auto activation = client.activate("USER-KEY"); ``` ### Manual Control For advanced or air-gapped use cases, you can manage the offline artifact manually. The currently supported manual flow is: 1. collect the fingerprint on the target machine 2. call `activate` 3. call `machine-file` 4. transfer the returned certificate manually See [[guides/air-gapped-licensing|Air-Gapped Licensing]] for the full operator-assisted workflow. > **Important:** LicenseSeat does not currently ship a built-in challenge-response product flow, QR ceremony, or dedicated offline-activation endpoint. Those are future workflow possibilities, not current product behavior. --- ## What Users See ### Normal Operation Users don't see anything special. The app just works, online or offline. ### Near Expiry Warning When offline credentials are about to expire and the user hasn't connected: ``` ┌─────────────────────────────────────────────────────────────────┐ │ ⚠️ Offline license expires in 3 days │ │ │ │ Connect to the internet to extend your offline access. │ │ │ │ [Remind Me Later] [Connect Now] │ └─────────────────────────────────────────────────────────────────┘ ``` ### Expired (with Grace Period) If expired but still in grace period: ``` ┌─────────────────────────────────────────────────────────────────┐ │ ⏰ Offline license expired │ │ │ │ Your offline access has expired. Please connect to the │ │ internet to continue using all features. │ │ │ │ Limited mode: You can still open existing projects. │ │ │ │ [Connect Now] │ └─────────────────────────────────────────────────────────────────┘ ``` ### Fully Expired ``` ┌─────────────────────────────────────────────────────────────────┐ │ 🔒 License validation required │ │ │ │ Please connect to the internet to validate your license. │ │ │ │ [Try Again] [Enter New Key] │ └─────────────────────────────────────────────────────────────────┘ ``` --- ## Security Details ### Device Binding Machine files are encrypted with a key derived from: ``` encryption_key = SHA256(license_key + device_fingerprint) ``` This means: - ✅ Works on the original device - ❌ Won't decrypt on a different device (different fingerprint = different key) - ❌ Won't decrypt with a different license key The device fingerprint is derived from hardware identifiers: - **macOS**: IOPlatformUUID (hardware UUID) - **Windows**: SMBIOS system UUID - **Linux**: /etc/machine-id or DMI system UUID - **iOS**: Stable app-scoped identifier ### Signature Verification Every machine file is signed with Ed25519: ``` signature = Ed25519_Sign("machine/" + encrypted_payload, server_private_key) ``` The SDK verifies this signature using the public key (embedded in your app or fetched once). If anyone modifies the machine file, the signature check fails. ### Clock Tamper Detection The SDK tracks the last time it successfully validated. If the system clock suddenly jumps backwards (user trying to extend expired license), the SDK detects this and flags it as suspicious. --- ## FAQ ### Q: What if a user copies the machine file to another computer? **A**: It won't work. The machine file is encrypted with a key that includes the device fingerprint. Different device = different fingerprint = decryption fails. ### Q: What if a user never connects after activation? **A**: Their offline credentials will eventually expire (based on your configured TTL). After expiry + grace period, they'll need to reconnect. You control how long this window is. ### Q: Can I revoke a license while someone is offline? **A**: Not immediately—that's the tradeoff of offline support. The machine file is self-contained. However: - The machine file will expire at its TTL - When they reconnect, the revocation takes effect immediately - For high-security needs, use shorter TTLs ### Q: What happens if the user's hardware changes (new motherboard)? **A**: The fingerprint changes, so the machine file won't decrypt. They'll need to re-activate online. If they're at their seat limit, they may need to deactivate an old device first. ### Q: Can users game the system by staying offline forever? **A**: No. Machine files have a maximum TTL (you configure this). Even a 365-day TTL eventually expires. Most apps use 30 days. ### Q: Does offline validation work in VMs or containers? **A**: Yes, but you should ensure stable fingerprints: - **VMs**: The fingerprint should remain stable across reboots - **Containers**: Use a persistent volume to store the fingerprint, or inject it via environment variable --- ## Next Steps - [[guides/air-gapped-licensing|Air-Gapped Licensing]] - For devices that never connect - [[sdks/swift|Swift SDK]] - Detailed offline configuration for Apple platforms - [[sdks/cpp|C++ SDK]] - Offline support for native applications - [[api-reference/signing-keys|Signing Keys API]] - For advanced integrations --- ## Guides # Guides In-depth guides for common LicenseSeat use cases. ## Offline & Air-Gapped Licensing - [[guides/offline-licensing|Offline Licensing]] — Let users work without internet using machine files or, on older SDKs, legacy signed offline tokens - [[guides/air-gapped-licensing|Air-Gapped Licensing]] — Operator-assisted fingerprint → activate → machine-file workflows for devices that never connect ## Coming Soon - Payment Integration (Stripe, Lemon Squeezy, Gumroad) - License Tiers & Upgrades - Team & Organization Licenses - White-Label Email Templates --- ## Entitlements Entitlements are feature flags or capabilities tied to a license. They allow you to gate specific features, modules, or tiers within your software without creating separate products or licenses. ## What are Entitlements? Think of entitlements as fine-grained permissions attached to a license: - A regular license might include `downloads` only for 1 year - A "Pro" license might include entitlements for `advanced-export`, `cloud-sync`, and `priority-support` - A "Team" license might add `multi-user` and `admin-panel` entitlements - A trial might grant `full-access` with a 14-day expiration Each entitlement has: - **Key** — A unique identifier (e.g., `pro-features`, `api-access`, `beta-mode`) - **Expiration** — Optional expiry date (perpetual if not set) - **Metadata** — Optional custom data attached to the entitlement ## Setting Up Entitlements Entitlements are configured on **License Plans** in the LicenseSeat dashboard. ### 1. Create or Edit a License Plan Go to your product → **License Types** → Edit a plan (or create a new one). ### 2. Add Entitlements In the "Entitlements" section, add the feature keys you want to grant: | Feature Slug | When does it expire? | Duration | |--------------|---------------------|----------| | `pro-features` | Never | — | | `beta-access` | Expires after... | 90 days | | `updates` | With the license | — | ### 3. Expiration Options Each entitlement can have one of three expiration modes: | Mode | Description | |------|-------------| | **Never** | Perpetual access — the entitlement never expires | | **With the license** | Expires when the license expires | | **Expires after...** | Fixed duration from license issuance (e.g., 1 year, 90 days) | > **Note:** Fixed-duration entitlements start counting from the license's `starts_at` date, not from the first activation. ### 4. Entitlement Keys Keys must be lowercase alphanumeric with hyphens or underscores: - `pro-features` - `api_access` - `beta2024` Invalid: `Pro Features`, `api access`, `PRO-FEATURES` ## Checking Entitlements in Your App All LicenseSeat SDKs provide methods to check entitlements. ### JavaScript ```javascript // Simple boolean check if (sdk.hasEntitlement('pro-features')) { enableProFeatures(); } // Detailed check with expiration info const result = sdk.checkEntitlement('beta-access'); if (result.active) { console.log('Expires:', result.entitlement.expires_at); } else { console.log('Reason:', result.reason); // 'no_license' | 'not_found' | 'expired' } ``` ### Swift ```swift // Simple check let status = LicenseSeat.shared.checkEntitlement("pro-features") if status.active { enableProFeatures() } // SwiftUI property wrapper @EntitlementState("pro-features") private var hasPro var body: some View { if hasPro { ProFeaturesView() } } // Reactive publisher LicenseSeat.shared.entitlementPublisher(for: "beta-access") .sink { status in updateUI(for: status) } ``` ### C# ```csharp // Simple boolean check if (LicenseSeat.HasEntitlement("pro-features")) { EnableProFeatures(); } // Detailed check var status = LicenseSeat.Entitlement("beta-access"); if (status.Active) { Console.WriteLine($"Expires: {status.ExpiresAt}"); } else { switch (status.Reason) { case EntitlementInactiveReason.Expired: ShowRenewalPrompt(); break; case EntitlementInactiveReason.NotFound: ShowUpgradePrompt(); break; } } ``` ### C++ ```cpp // Simple boolean check if (client.has_entitlement("pro-features")) { enable_pro_features(); } // Detailed check with expiration and metadata auto status = client.check_entitlement("beta-access"); if (status.active) { std::cout << "Active until: " << status.expires_at.value_or(0) << "\n"; // Access metadata if needed if (status.entitlement) { for (const auto& [key, value] : status.entitlement->metadata) { std::cout << key << ": " << value << "\n"; } } } else { std::cout << "Inactive: " << status.reason << "\n"; // Reasons: "no_license", "not_found", "expired" } ``` ## API Response Entitlements are returned in all license validation responses: ```json { "valid": true, "license": { "key": "XXXX-XXXX-XXXX-XXXX", "status": "active", "active_entitlements": [ { "key": "pro-features", "expires_at": null, "metadata": {} }, { "key": "beta-access", "expires_at": "2024-12-31T23:59:59Z", "metadata": { "beta_version": "2.0" } } ] } } ``` ## Offline Support Entitlements are included in offline machine files, allowing you to check them without network access: ```json { "license": { "key": "XXXX-XXXX-XXXX-XXXX", "entitlements": [ { "key": "pro-features", "expires_at": null }, { "key": "beta-access", "expires_at": "2024-12-31T23:59:59Z" } ] } } ``` For SDKs that already support machine files, entitlements are read from the cached machine file when the network is unavailable. Older SDKs may still use signed offline tokens as a legacy compatibility path. ## Granting Entitlements to Individual Licenses Beyond plan-level entitlements, you can grant additional entitlements to specific licenses: 1. Go to the license detail page in the dashboard 2. In the "Entitlements" section, click "Grant new entitlement" 3. Enter the feature key and expiration This is useful for: - Granting beta access to specific customers - Extending a feature for a loyal customer - Adding promotional features ## Best Practices ### Use Descriptive Keys ``` ✓ pro-export, cloud-sync, api-access ✗ feat1, pro, x ``` ### Check Entitlements, Not Plans Instead of checking the plan name: ```javascript // ✗ Fragile - breaks if you rename plans if (license.plan_key === 'pro') { ... } ``` Check for specific capabilities: ```javascript // ✓ Flexible - works regardless of plan structure if (sdk.hasEntitlement('advanced-export')) { ... } ``` ### Handle Missing Entitlements Gracefully ```javascript const result = sdk.checkEntitlement('new-feature'); if (!result.active && result.reason === 'not_found') { // Feature not in their plan - show upgrade prompt showUpgradeModal(); } ``` ### Use Expiring Entitlements for Trials Instead of separate trial licenses, use time-limited entitlements: | Plan | Entitlement | Expiration | |------|-------------|------------| | Trial | `full-access` | 14 days | | Pro | `full-access` | Never | This way, trial users automatically lose access after 14 days without requiring license revocation. ## Next Steps - [[sdks/javascript|JavaScript SDK]] — Full entitlement API reference - [[sdks/swift|Swift SDK]] — SwiftUI integration with `@EntitlementState` - [[sdks/csharp|C# SDK]] — Events and reactive patterns - [[sdks/cpp|C++ SDK]] — Thread-safe entitlement checking --- ## 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; ``` --- ## API Quickstart Introduction to the LicenseSeat API. > [!IMPORTANT] **Full API Docs** — To see the full LicenseSeat API docs, together with the LicenseSeat OpenAPI spec, head to the [LicenseSeat API Reference page](/api/). It also has an interactive experience with request testing, error codes, full schemas, and live examples. ## 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 '{"fingerprint": "device-unique-id"}' ``` `fingerprint` is the canonical device-binding field. `device_id` remains accepted as a legacy alias for compatibility with older SDKs and integrations. ## 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` --- ## Get Signing Key # Get Signing Key Retrieves an Ed25519 public key for verifying offline machine-file signatures and legacy 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 machine files - Fetching verification keys for legacy offline tokens - Supporting key rotation - Dynamically loading unknown key IDs ## Path Parameters | Parameter | Required | Description | |-----------|----------|-------------| | `key_id` | Yes | Key ID from a machine file envelope `kid` or a legacy offline token `signature.key_id` / `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 machine files and offline tokens use the latest key 2. Old keys remain valid for existing artifacts 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 --- ## Telemetry # Telemetry Every API request can include an optional `telemetry` object with device and app information. LicenseSeat uses this data to power per-product analytics: DAU/MAU, version adoption, platform distribution, geographic breakdown, and stale device detection. Telemetry is captured automatically by the official SDKs. If you're building a custom integration, this page describes the telemetry format and what each field is used for. ## The Telemetry Object Include a `telemetry` key in the JSON body of any POST request: ```json { "fingerprint": "mac-a1b2c3d4-e5f6g7h8", "telemetry": { "sdk_name": "swift", "sdk_version": "0.4.0", "os_name": "macOS", "os_version": "15.2.0", "platform": "native", "device_model": "MacBookPro18,1", "app_version": "2.1.0", "app_build": "42", "device_type": "desktop", "architecture": "arm64", "cpu_cores": 10, "memory_gb": 32, "locale": "en_US", "language": "en", "timezone": "America/New_York", "screen_resolution": "3456x2234", "display_scale": 2.0 } } ``` ## Fields ### Promoted Columns These fields are stored as dedicated database columns and are directly queryable in the analytics dashboard: | Field | Type | Description | Used for | |-------|------|-------------|----------| | `sdk_name` | String | SDK identifier: `swift`, `js`, `cpp`, `csharp` | SDK distribution | | `sdk_version` | String | Version of the LicenseSeat SDK | SDK adoption tracking | | `os_name` | String | Operating system name (`macOS`, `iOS`, `Windows`, `Linux`, `Android`) | Platform distribution | | `os_version` | String | OS version (e.g., `15.2.0`) | OS distribution | | `platform` | String | Runtime environment: `native`, `node`, `browser`, `electron`, `react-native`, `deno`, `bun`, `unity` | Platform analytics | | `device_model` | String | Hardware model (e.g., `MacBookPro18,1`, `iPhone15,1`) | Device analytics | | `app_version` | String | Host app version | Version adoption charts | | `app_build` | String | Host app build number | Build tracking | | `device_type` | String | Device form factor: `phone`, `tablet`, `desktop`, `watch`, `tv`, `server`, `unknown` | Device type distribution | | `architecture` | String | CPU architecture: `arm64`, `x64`, `x86` | Architecture distribution | | `cpu_cores` | Integer | Number of CPU cores | Hardware segmentation | | `memory_gb` | Integer | Total RAM in GB (rounded) | Hardware segmentation | | `locale` | String | Full locale (e.g., `en_US`, `pt_BR`) | Localization insights | | `language` | String | 2-letter ISO 639-1 code (e.g., `en`, `pt`, `es`) -- extracted from locale | Language distribution | ### Additional Metadata These fields are stored in the JSONB metadata column: | Field | Type | Description | SDKs | |-------|------|-------------|------| | `screen_resolution` | String | Screen resolution as `WIDTHxHEIGHT` | Swift, JS (browser), C++ | | `display_scale` | Number | Display pixel ratio (1.0, 2.0, 3.0) | Swift, JS (browser) | | `browser_name` | String | Browser name (Chrome, Safari, Firefox, Edge) | JS (browser only) | | `browser_version` | String | Browser version | JS (browser only) | | `runtime_version` | String | Runtime version (e.g., `.NET 9.0.0`, `Node 20.11.0`) | JS, C# | | `timezone` | String | IANA timezone (e.g., `America/New_York`) | All SDKs | All fields are optional. Send what you have -- partial telemetry is better than none. ## Which Endpoints Accept Telemetry Telemetry is captured on these endpoints: | Endpoint | Event type | |----------|------------| | [[api-reference/activate\|Activate]] | `activation` | | [[api-reference/deactivate\|Deactivate]] | `deactivation` | | [[api-reference/validate-license\|Validate]] | `validation` | | [[api-reference/heartbeat\|Heartbeat]] | `heartbeat` | Every successful request to these endpoints creates a footprint with the telemetry data, the request IP (for geolocation), and the event type. ## How the SDKs Handle It The official SDKs collect and attach telemetry automatically -- no configuration needed. ### Swift SDK The Swift SDK collects telemetry on every API call: ```swift // Automatic — no code needed. // The SDK injects telemetry into every POST request. // What gets collected: // - sdk_name: "swift" // - sdk_version: from LicenseSeatConfig.sdkVersion // - os_name: "macOS", "iOS", "tvOS", "watchOS", "visionOS" // - os_version: from ProcessInfo.operatingSystemVersion // - platform: "native" // - device_model: from sysctlbyname("hw.model") // - app_version: from CFBundleShortVersionString // - app_build: from CFBundleVersion // - device_type: "desktop", "phone", "tablet", "watch", "tv", "headset" // - architecture: "arm64" or "x64" // - cpu_cores: from ProcessInfo.processorCount // - memory_gb: from ProcessInfo.physicalMemory (rounded) // - locale: from Locale.current.identifier // - language: 2-letter code from locale // - timezone: from TimeZone.current.identifier // - screen_resolution: native pixel resolution // - display_scale: backingScaleFactor / UIScreen.scale ``` The SDK also generates a stable device fingerprint automatically (for example, a hardware UUID on macOS and a platform-specific stable identifier on iOS) and sends it as a top-level parameter alongside telemetry. Legacy clients may still use the `device_id` field name, but `fingerprint` is the canonical term. ### Custom Integrations If you're building a custom SDK or integration, collect whatever fields are available on your platform and include them in the `telemetry` object: ```python import platform import requests requests.post( "https://licenseseat.com/api/v1/products/my-app/licenses/XXXX/validate", headers={"Authorization": "Bearer pk_live_xxx"}, json={ "fingerprint": get_device_fingerprint(), "telemetry": { "sdk_name": "python", "sdk_version": "0.1.0", "os_name": platform.system(), "os_version": platform.release(), "platform": "native", "device_type": "desktop", "architecture": platform.machine(), "app_version": "1.0.0" } } ) ``` ```csharp var body = new { fingerprint = GetDeviceFingerprint(), telemetry = new { sdk_name = "csharp", sdk_version = "0.1.0", os_name = Environment.OSVersion.Platform.ToString(), os_version = Environment.OSVersion.Version.ToString(), platform = "native", device_type = "desktop", architecture = RuntimeInformation.ProcessArchitecture.ToString(), app_version = Assembly.GetExecutingAssembly().GetName().Version.ToString() } }; ``` ## What Powers the Analytics Dashboard The telemetry data feeds directly into your product's analytics dashboard in LicenseSeat. The dashboard offers **7, 30, and 90 day** time range selectors. ### KPI Cards - **DAU** — Daily Active Users (unique devices today) - **MAU** — Monthly Active Users (unique devices this month) - **Unique Devices** — Over selected time range - **Total Events** — All telemetry events in range ### Charts & Distributions | Section | Metrics | Based on | |---------|---------|----------| | **Daily Active Devices** | Bar chart of unique devices per day | `fingerprint` counts | | **Geographic Distribution** | World map + country breakdown | IP geolocation (automatic) | | **Version Adoption** | Donut + stacked area chart | `app_version` | | **Runtime Environment** | OS versions, platforms, device types | `os_name`, `os_version`, `platform`, `device_type` | | **Hardware** | Architecture, CPU cores histogram, memory histogram | `architecture`, `cpu_cores`, `memory_gb` | ### LicenseSeat Section | Metric | Description | |--------|-------------| | **Seats** | Utilization percentage (used/total across all licenses) | | **Stale Devices** | Devices with no heartbeat in **7+ days** | | **SDK Versions** | Distribution of `sdk_version` values | | **SDK Platforms** | Distribution of `sdk_name` (swift, js, cpp, csharp) | ## Geolocation In addition to the telemetry fields you send, LicenseSeat automatically resolves geolocation from the request IP address. This provides country, city, region, coordinates, and timezone -- no extra work needed from the SDK. ## Privacy If your app needs to comply with GDPR or similar regulations, you can skip telemetry entirely by not including the `telemetry` key in your requests. The API works the same with or without it. --- ## C++ SDK Official C++ SDK for LicenseSeat. Add license validation to native applications, Unreal Engine games, and VST/AU audio plugins.

SynthDemo - Example app with LicenseSeat licensing License activation dialog
SynthDemo: FREE tier with PRO feature gating and license activation

> **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** - Fingerprint-aware validation with signed offline artifacts - **Machine files** - AES-256-GCM + Ed25519 machine files for preferred offline validation - **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 v0.4.0 ) 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 **All bundled (no installation needed):** - nlohmann/json – JSON parsing - cpp-httplib – HTTP client - ed25519 – Cryptographic signatures - PicoSHA2 – SHA-256 hashing **External (the only thing you need to install):** - OpenSSL – for HTTPS and machine-file AES-256-GCM verification in the full SDK path ```bash # Ubuntu/Debian sudo apt install libssl-dev # macOS (usually pre-installed) brew install openssl # Windows vcpkg install openssl ``` That's it. Clone and build — no other dependencies to install. ## 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; } ``` ## Integration Guide Step-by-step guide to integrating LicenseSeat into your C++ application. ### 1. Get Your Credentials From your [LicenseSeat Dashboard](https://licenseseat.com/dashboard): 1. **API Key** — Go to Settings → API Keys → Copy your `pk_live_*` key (publishable, safe to embed in apps) 2. **Product Slug** — Go to Products → Click your product → Copy the slug from the URL > **Note:** Use `pk_*` (publishable) keys in client applications. Keep `sk_*` (secret) keys server-side only. ### 2. Add the SDK to Your Project **Option A: CMake FetchContent (recommended)** ```cmake include(FetchContent) FetchContent_Declare( licenseseat GIT_REPOSITORY https://github.com/licenseseat/licenseseat-cpp.git GIT_TAG v0.4.0 ) FetchContent_MakeAvailable(licenseseat) target_link_libraries(your_target PRIVATE licenseseat::licenseseat) ``` **Option B: Manual** ```bash git clone https://github.com/licenseseat/licenseseat-cpp.git cd licenseseat-cpp cmake -B build && cmake --build build sudo cmake --install build ``` ### 3. Initialize the Client ```cpp #include licenseseat::Config config; config.api_key = "pk_live_xxxxxxxx"; // Your publishable key config.product_slug = "your-product"; // From dashboard config.storage_path = "/path/to/cache"; // Optional: enables persistence config.app_version = "1.0.0"; // Your app version licenseseat::Client client(config); ``` ### 4. Validate the License ```cpp auto result = client.validate("XXXX-XXXX-XXXX-XXXX"); if (result.is_ok() && result.value().valid) { const auto& license = result.value().license; std::cout << "Plan: " << license.plan_key() << "\n"; // Store the key for future use save_license_key("XXXX-XXXX-XXXX-XXXX"); } else if (result.is_ok()) { // License exists but invalid std::cout << "License issue: " << result.value().message << "\n"; } else { // Network or API error std::cerr << "Error: " << result.error_message() << "\n"; } ``` ### 5. Activate the Device (Optional) For hardware-locked licenses, activate the device to consume a seat: ```cpp std::string device_id = licenseseat::generate_device_id(); // Auto-generated fingerprint std::string device_name = "User's MacBook Pro"; // Friendly name auto result = client.activate("LICENSE-KEY", device_id, device_name); if (result.is_ok()) { std::cout << "Activated on device: " << result.value().device_name() << "\n"; } else if (result.error_code() == licenseseat::ErrorCode::SeatLimitExceeded) { std::cout << "No seats available. Deactivate another device first.\n"; } ``` ### 6. Check Features (Entitlements) Gate features based on the license plan: ```cpp // Simple boolean check if (client.has_entitlement("pro-features")) { enable_pro_mode(); } if (client.has_entitlement("export-pdf")) { show_export_button(); } // Detailed check with expiration info auto status = client.check_entitlement("beta-access"); if (status.active) { if (status.expires_at) { show_beta_with_countdown(*status.expires_at); } else { show_beta_perpetual(); } } ``` ### 7. Enable Offline Support (Optional) The current C++ SDK is machine-file-first. Set a cache path, activate online once, and the SDK automatically syncs an encrypted machine file for offline use: ```cpp licenseseat::Config config; config.api_key = "pk_live_xxxxxxxx"; config.product_slug = "your-product"; config.storage_path = "/path/to/cache"; licenseseat::Client client(config); auto activation = client.activate("LICENSE-KEY"); if (activation.is_ok()) { std::cout << "Activated and machine file cached\n"; } // Later, when offline: auto restore = client.restore_license(); if (restore.success && restore.status == licenseseat::ClientStatus::OfflineValid) { std::cout << "Restored from cached machine file\n"; } ``` If you need full manual control, fetch and verify the machine file explicitly: ```cpp client.activate("LICENSE-KEY"); // Required once for the same fingerprint auto machine_file = client.checkout_machine_file("LICENSE-KEY").value(); auto verify = client.verify_machine_file(machine_file); if (verify.is_ok() && verify.value().valid) { std::cout << "Offline license verified from machine file\n"; } ``` Legacy offline tokens still exist for older integrations, but they are no longer the default path. Enable them explicitly only if you still depend on them: ```cpp licenseseat::Config config; config.api_key = "pk_live_xxxxxxxx"; config.product_slug = "your-product"; config.enable_legacy_offline_tokens = true; ``` ### 8. Handle License Changes Subscribe to events for reactive updates: ```cpp client.on(licenseseat::events::VALIDATION_SUCCESS, [](const std::any& data) { update_ui_licensed(); }); client.on(licenseseat::events::VALIDATION_FAILED, [](const std::any& data) { show_license_expired_dialog(); }); // Start background validation (every hour) client.start_auto_validation("LICENSE-KEY"); ``` ### Complete Example ```cpp #include #include int main() { // 1. Configure licenseseat::Config config; config.api_key = "pk_live_xxxxxxxx"; config.product_slug = "my-desktop-app"; config.storage_path = get_app_data_path() + "/license_cache"; config.app_version = "2.0.0"; licenseseat::Client client(config); // 2. Load saved license key (if any) std::string license_key = load_saved_license_key(); if (license_key.empty()) { license_key = show_license_entry_dialog(); } // 3. Validate auto result = client.validate(license_key); if (result.is_ok() && result.value().valid) { save_license_key(license_key); // 4. Check what features they have bool is_pro = client.has_entitlement("pro"); bool has_export = client.has_entitlement("export"); // 5. Launch app with appropriate features launch_app(is_pro, has_export); // 6. Keep validating in background client.start_auto_validation(license_key); } else { show_license_invalid_dialog( result.is_ok() ? result.value().message : result.error_message() ); } return 0; } ``` ## Metadata Access Metadata lets you attach custom key-value data to licenses and entitlements. Use it for customer info, feature limits, configuration, or any app-specific data. ### License Metadata Access metadata from the license object after validation: ```cpp auto result = client.validate("LICENSE-KEY"); if (result.is_ok() && result.value().valid) { const auto& license = result.value().license; const auto& metadata = license.metadata(); // std::map // Access specific keys if (metadata.count("customer_id")) { std::string customer_id = metadata.at("customer_id"); load_customer_preferences(customer_id); } if (metadata.count("max_projects")) { int max_projects = std::stoi(metadata.at("max_projects")); enforce_project_limit(max_projects); } // Iterate all metadata for (const auto& [key, value] : metadata) { std::cout << key << " = " << value << "\n"; } } ``` ### Entitlement Metadata Each entitlement can have its own metadata: ```cpp auto result = client.validate("LICENSE-KEY"); if (result.is_ok() && result.value().valid) { // Get all active entitlements for (const auto& ent : result.value().license.active_entitlements()) { std::cout << "Entitlement: " << ent.key << "\n"; // Access entitlement-specific metadata for (const auto& [key, value] : ent.metadata) { std::cout << " " << key << " = " << value << "\n"; } // Example: Feature limits per entitlement if (ent.key == "api-access" && ent.metadata.count("rate_limit")) { int rate_limit = std::stoi(ent.metadata.at("rate_limit")); configure_api_rate_limit(rate_limit); } } } ``` ### Offline Metadata Metadata is included in machine files too: ```cpp auto machine_file = client.checkout_machine_file("LICENSE-KEY").value(); auto verify = client.verify_machine_file(machine_file); if (verify.is_ok() && verify.value().valid && verify.value().payload.has_value()) { const auto& payload = *verify.value().payload; // Activation metadata captured in the machine file if (payload.metadata.count("device_name")) { std::cout << "Device name: " << payload.metadata.at("device_name") << "\n"; } // Embedded license metadata and entitlements if (payload.license.has_value()) { const auto& license = *payload.license; if (license.metadata().count("customer_name")) { std::cout << "Licensed to: " << license.metadata().at("customer_name") << "\n"; } } } ``` If you still rely on legacy offline tokens, metadata is accessible there too through `offline_token.token.metadata` and `offline_token.token.entitlements`. ### Common Metadata Use Cases | Use Case | Metadata Key | Example Value | |----------|--------------|---------------| | Customer identification | `customer_id` | `"cust_123abc"` | | Feature limits | `max_projects` | `"10"` | | API rate limits | `rate_limit` | `"1000"` | | White-label branding | `company_name` | `"Acme Corp"` | | Seat allocation | `assigned_seats` | `"5"` | | Custom expiration | `support_expires` | `"2026-12-31"` | > **Tip:** Set metadata when creating licenses via the Dashboard or API. The SDK only reads metadata — modifications require admin access. ## 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 = ""; // Legacy config name; auto-generates the device fingerprint if empty // Optional - App info (for telemetry) config.app_version = "2.1.0"; config.app_build = "42"; // Optional - Offline support config.signing_public_key = "base64-ed25519-public-key"; // Pre-configure machine-file verification config.max_offline_days = 30; // Optional - Caching config.storage_path = ""; // Path for license cache (empty = no persistence) // Optional - Auto-validation & heartbeat config.auto_validate_interval = 3600.0; // Seconds between background validations config.heartbeat_interval = 300; // Seconds between heartbeats (5 minutes) ``` ### 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 fingerprint (legacy config name, auto-generated if empty) | | `signing_public_key` | `string` | `""` | Ed25519 public key for machine files and legacy offline tokens | | `max_offline_days` | `int` | `0` | Local offline restore limit in days (0 = disabled/unlimited) | | `app_version` | `string` | `""` | Your app version for telemetry | | `app_build` | `string` | `""` | Your app build number for telemetry | | `storage_path` | `string` | `""` | Path for license cache (empty = no persistence) | | `auto_validate_interval` | `double` | `3600.0` | Seconds between auto-validation cycles | | `heartbeat_interval` | `int` | `300` | Seconds between standalone heartbeats (0 = disabled) | ## 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 fingerprint. In the current public C++ API that parameter is still named `device_id` for compatibility: > ```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 Entitlements are feature flags tied to a license. They allow you to gate specific features without creating separate products. ### Simple Check (Boolean) ```cpp // Uses cached license data - no network request if (client.has_entitlement("pro-features")) { enable_pro_features(); } if (client.has_entitlement("beta-access")) { show_beta_ui(); } ``` ### Detailed Check ```cpp auto status = client.check_entitlement("pro-features"); if (status.active) { // Entitlement is active std::cout << "Feature unlocked!\n"; // Check expiration (if set) if (status.expires_at) { auto expires = *status.expires_at; auto now = std::chrono::system_clock::now(); auto days_left = std::chrono::duration_cast(expires - now).count() / 24; std::cout << "Expires in " << days_left << " days\n"; } else { std::cout << "Never expires (perpetual)\n"; } // Access metadata if needed if (status.entitlement) { for (const auto& [key, value] : status.entitlement->metadata) { std::cout << " " << key << ": " << value << "\n"; } } } else { // Entitlement not active - check reason if (status.reason == "no_license") { show_activation_screen(); } else if (status.reason == "not_found") { show_upgrade_prompt(); // Feature not in their plan } else if (status.reason == "expired") { show_renewal_prompt(); } } ``` ### EntitlementStatus Fields | Field | Type | Description | |-------|------|-------------| | `active` | `bool` | Whether the entitlement is currently active | | `reason` | `string` | Why inactive: `no_license`, `not_found`, `expired` | | `expires_at` | `optional` | Expiration time (empty if perpetual) | | `entitlement` | `optional` | Full entitlement data if found | ### Entitlements in Validation Response After validation, entitlements are available on the license object: ```cpp auto result = client.validate("LICENSE-KEY"); if (result.is_ok() && result.value().valid) { const auto& license = result.value().license; std::cout << "Active entitlements:\n"; for (const auto& ent : license.active_entitlements()) { std::cout << " - " << ent.key; if (ent.expires_at) { std::cout << " (expires: " << *ent.expires_at << ")"; } else { std::cout << " (perpetual)"; } std::cout << "\n"; } } ``` ### Offline Entitlement Checking Entitlements are included in offline machine files too: ```cpp auto machine_file = client.checkout_machine_file("LICENSE-KEY").value(); auto verify = client.verify_machine_file(machine_file); if (verify.is_ok() && verify.value().valid && verify.value().payload.has_value()) { const auto& payload = *verify.value().payload; if (payload.license.has_value()) { for (const auto& ent : payload.license->active_entitlements()) { std::cout << "Entitlement: " << ent.key << "\n"; } } if (client.check_entitlement("pro-features").active) { enable_pro_features(); } } ``` If you still rely on legacy offline tokens, you can read entitlements from `offline_token.token.entitlements` too. See [[entitlements|Entitlements]] for more details on setting up and managing entitlements. ## 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"; ``` ## Test API Health Test API key authentication and API health: ```cpp auto result = client.health(); if (result.is_ok()) { std::cout << "API is reachable and healthy\n"; } else { std::cerr << "Health check failed: " << result.error_message() << "\n"; } ``` Returns `Result` where a successful result indicates the API is reachable and the API key is valid. ## Offline Support The SDK supports offline validation with encrypted machine files signed using Ed25519. Machine files are the preferred path for new integrations because they are: - Activation-bound - Fingerprint-bound - AES-256-GCM encrypted - Signed for tamper detection ### Preferred Workflow: Machine Files **Step 1: Activate online and let the SDK cache a machine file** ```cpp licenseseat::Config config; config.api_key = "pk_live_xxxxxxxx"; config.product_slug = "your-product"; config.storage_path = "/path/to/cache"; licenseseat::Client client(config); client.activate("LICENSE-KEY"); // Syncs machine file automatically ``` **Step 2: Restore offline with no manual file handling** ```cpp auto restore = client.restore_license(); if (restore.success) { std::cout << restore.message << "\n"; } ``` **Step 3: Optional manual machine-file handling** ```cpp auto machine_file = client.checkout_machine_file("LICENSE-KEY").value(); save_to_disk(machine_file.certificate); auto loaded = machine_file; loaded.certificate = load_from_disk(); auto verify = client.verify_machine_file(loaded); ``` ### 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_machine_file can use the pre-configured key auto result = client.verify_machine_file(machine_file); // No key param needed ``` ### Legacy Offline Tokens Legacy offline tokens are still available for compatibility, but only use them when you explicitly need portable JSON tokens for an older integration: ```cpp config.enable_legacy_offline_tokens = true; client.activate("LICENSE-KEY"); // Offline tokens also require an existing activation auto token = client.generate_offline_token("LICENSE-KEY").value(); std::string token_json = licenseseat::json::offline_token_to_json(token); ``` ### Machine File Payload | Field | Type | Description | | ----- | ---- | ----------- | | `license_key` | `string` | License key embedded in the encrypted payload | | `fingerprint` | `string` | Device fingerprint the machine file was issued for | | `fingerprint_components` | `object` | Structured fingerprint metadata captured during checkout | | `iat` / `exp` / `nbf` | `int64` | Issued-at, expiry, and not-before timestamps | | `ttl` | `int64` | Machine-file lifetime in seconds | | `grace_period` | `int64` | Extra offline grace after expiry | | `kid` | `string` | Signing key ID | | `license` | `object` | Embedded license snapshot with entitlements and metadata | ### Long-Lived Offline Deployments Machine files are finite by design, but the server can intentionally allow long TTLs for deployments that are rarely connected: - consumer apps: usually 30-45 days - professional/field deployments: often 90-365 days - intentionally air-gapped systems: sometimes multi-year TTLs The tradeoff is operational, not cryptographic: the longer the TTL, the slower offline revocation, entitlement changes, and metadata changes become visible until the machine file is refreshed. ## 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 machine-file ready auto sub3 = client.on(licenseseat::events::MACHINE_FILE_READY, [](const std::any& data) { std::cout << "Machine file cached\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` | Cached machine file (or legacy token) verified successfully | | `VALIDATION_OFFLINE_FAILED` | Offline 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 | | `HEARTBEAT_SUCCESS` | Heartbeat acknowledged by server | | `HEARTBEAT_ERROR` | Heartbeat request failed | | `AUTOVALIDATION_CYCLE` | Auto-validation cycle completed | | `AUTOVALIDATION_STOPPED` | Auto-validation stopped | | `MACHINE_FILE_READY` | Machine file cached | | `MACHINE_FILE_VERIFIED` | Machine file verified | | `OFFLINE_TOKEN_READY` | Legacy offline token generated | | `OFFLINE_TOKEN_VERIFIED` | Legacy 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 | ## Telemetry The SDK automatically collects and sends the following telemetry fields on every API call: | Field | macOS | Windows | Linux | |-------|-------|---------|-------| | `sdk_name` | `cpp` | `cpp` | `cpp` | | `sdk_version` | Yes | Yes | Yes | | `os_name` | `macOS` | `Windows` | `Linux` | | `os_version` | `kern.osproductversion` | `RtlGetVersion` | `uname` | | `platform` | `native` | `native` | `native` | | `device_model` | `hw.model` | Registry BIOS | `/sys/class/dmi` | | `device_type` | `desktop` | `desktop` | `desktop`/`server` | | `architecture` | `arm64`/`x64` (compile-time) | `arm64`/`x64` | `arm64`/`x64` | | `cpu_cores` | `hardware_concurrency` | `hardware_concurrency` | `hardware_concurrency` | | `memory_gb` | `hw.memsize` | `GlobalMemoryStatusEx` | `/proc/meminfo` | | `locale` | `LANG` env | `LANG` env | `LANG` env | | `language` | 2-letter code | 2-letter code | 2-letter code | | `timezone` | `/etc/localtime` | `GetTimeZoneInformation` | `/etc/localtime` | | `screen_resolution` | `CGDisplay` | `GetSystemMetrics` | DRM subsystem | | `app_version` | From config | From config | From config | | `app_build` | From config | From config | From config | See [[api-reference/telemetry|Telemetry]] for the full field reference. ## 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 stable fingerprint per device: - **macOS**: IOKit Platform UUID - **Windows**: Machine GUID from registry - **Linux**: `/etc/machine-id`, D-Bus machine ID, DMI product UUID, or hostname fallback ## Reset Clear all cached data (license, machine files, legacy offline tokens, etc.). ```cpp client.reset(); ``` ## Demo App The SDK includes [SynthDemo](https://github.com/licenseseat/licenseseat-cpp/tree/main/demo), a compact audio synthesizer that demonstrates real-world licensing integration: - **Feature gating** — FREE tier (sine wave) vs PRO (sawtooth, square, noise) - **License activation** — Modal dialog with activate/deactivate - **Session restore** — Cached license on app restart - **Admin view** — Runtime status, heartbeat, machine file caching, entitlements

SDK Admin view showing runtime status and license details
Admin view: runtime status, auto-validation, machine file caching, and entitlements

Built with [raylib](https://www.raylib.com/) + raygui. See the [demo README](https://github.com/licenseseat/licenseseat-cpp/tree/main/demo) for build instructions. ## 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 on GitHub](https://github.com/licenseseat/licenseseat-cpp) - Source code, issues, and contributions --- ## 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.4.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), HeartbeatInterval = TimeSpan.FromMinutes(5), AppVersion = "2.1.0", AppBuild = "42", 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) | | `HeartbeatInterval` | `TimeSpan` | 5 minutes | Standalone heartbeat interval (0 = disabled) | | `AppVersion` | `string?` | `null` | Your app version for telemetry | | `AppBuild` | `string?` | `null` | Your app build number for telemetry | | `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")); client.Events.On(LicenseSeatEvents.HeartbeatSuccess, _ => Console.WriteLine("Heartbeat sent")); client.Events.On(LicenseSeatEvents.HeartbeatError, _ => Console.WriteLine("Heartbeat failed")); ``` ## 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 the SDK's cached offline artifact if network fails var result = await client.ValidateAsync("LICENSE-KEY"); if (result.Offline) { Console.WriteLine("Validated offline with cached license"); } ``` > **Offline model note:** the current C# SDK still uses signed offline tokens today. Machine files are the newer preferred offline artifact at the API level, but this SDK has not migrated yet. 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 ## Test Authentication Test API key authentication and API health: ```csharp try { var result = await client.TestAuthAsync(); Console.WriteLine($"Authenticated: {result.Authenticated}"); // true if API key is valid Console.WriteLine($"Healthy: {result.Healthy}"); // API health status Console.WriteLine($"API Version: {result.ApiVersion}"); // e.g., "1.0.0" } catch (ApiException ex) { Console.WriteLine($"Auth test failed: {ex.Message}"); } ``` Synchronous version (for Unity or contexts without async): ```csharp var result = client.TestAuth(); ``` ### AuthTestResult Properties | Property | Type | Description | | -------- | ---- | ----------- | | `Authenticated` | `bool` | Whether the API key is valid | | `Healthy` | `bool` | Whether the API is healthy | | `ApiVersion` | `string` | The API version (e.g., `1.0.0`) | ## 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 key authentication and API health | ### 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 | ## Telemetry The SDK automatically collects and sends the following telemetry fields on every API call: | Field | Source | |-------|--------| | `sdk_name` | Always `csharp` | | `sdk_version` | `LicenseSeatClient.SdkVersion` | | `os_name` | `RuntimeInformation.IsOSPlatform` (`Windows`, `macOS`, `Linux`) | | `os_version` | `Environment.OSVersion.Version` | | `platform` | `native` (or `unity` if Unity runtime detected) | | `device_model` | `Environment.MachineName` | | `device_type` | `desktop`, `server`, or Unity device type | | `architecture` | `RuntimeInformation.ProcessArchitecture` | | `cpu_cores` | `Environment.ProcessorCount` | | `memory_gb` | `GC.GetGCMemoryInfo().TotalAvailableMemoryBytes` (rounded) | | `locale` | `CultureInfo.CurrentCulture.Name` | | `language` | `CultureInfo.CurrentUICulture.TwoLetterISOLanguageName` | | `timezone` | IANA timezone (auto-converted from Windows timezone IDs) | | `runtime_version` | `RuntimeInformation.FrameworkDescription` (e.g., `.NET 9.0.0`) | | `app_version` | From config, or `Assembly.GetEntryAssembly` version | | `app_build` | From config, or `AssemblyInformationalVersion` | The SDK automatically converts Windows timezone IDs (e.g., `Eastern Standard Time`) to IANA format (e.g., `America/New_York`) for consistency across platforms. See [[api-reference/telemetry|Telemetry]] for the full field reference. ## 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 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 legacy offline-token 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 against a device fingerprint 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 - [[guides/offline-licensing|Offline Licensing]] — Machine-file-first offline model and legacy token compatibility notes --- ## SDKs LicenseSeat has SDKs for popular platforms to make integration with your app 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 | | [[sdks/rust\|Rust]] | `licenseseat` + `tauri-plugin-licenseseat` | Tauri v2 with JS/TS bindings, native Rust apps | ## First things first: create a license activation window When integrating with the LicenseSeat SDK, you should create your own UI / window for users to input the license, so you can design it to match the rest of your app. It should be pretty straightforward: just a text input box and a button you can connect to the SDK actions: ![Payment platforms supported by LicenseSeat](../images/demo-app-license-box.webp) ## Then, add the LicenseSeat SDK to your app Adding the LicenseSeat SDK to your app is very easy and take just a few lines of code. The SDKs for all languages all work in a similar way. In JavaScript, for example, you just need to import it: ```javascript import LicenseSeat from '@licenseseat/js'; ``` Then just initialize the `LicenseSeat` object with your API Key (you can get it in your LicenseSeat dashboard) and your product slug: ```javascript const sdk = new LicenseSeat({ apiKey: 'pk_live_xxxxxxxxxxxxxxxxxxxxx', productSlug: 'my-product' }); ``` And then you can just wire your UI to any of the methods the SDK give you: ```javascript const license = await sdk.activate('TEST-XXXX-XXXX-XXXX'); ``` On top of this, the SDK will do periodic checks in the background to ensure the license is active (so if you revoke it manually via the LicenseSeat dashboard, it actually stops working for the user). The SDK also sends telemetry data automatically, so you can track usage and user analytics in the dashboard: ![Product dashboard with usage analytics on LicenseSeat](../images/licenseseat-demo-product-dashboard-telemetry-usage-analytics.webp) ## SDK features All SDKs share the same core functionality: - **License Operations**: Activation, deactivation, online/offline validation - **Offline Support**: Signed offline artifacts, clock tamper detection, and a gradual transition toward machine-file-first offline validation - **Entitlements**: Check specific features access each license may give access to, with expiration support - **Resilience**: Automatic retries, network monitoring, background re-validation - **Heartbeat**: Standalone 5-minute heartbeat for device liveness detection - **Telemetry**: Automatic collection of device, OS, hardware, and app info - **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++, Tauri v2 for Rust). > **Offline model note:** the API and newest SDK work converge on **machine files** as the preferred offline artifact because they are encrypted, activation-bound, and fingerprint-bound. Some older SDKs still use signed offline tokens today; their individual docs call that out explicitly instead of pretending every SDK has already migrated. ## 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: "updates" (inactive) ``` See [[entitlements|Entitlements]] for setup instructions and best practices. ## 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 | | `heartbeatInterval` | Standalone heartbeat interval | 5 minutes | | `appVersion` | Your app version (for telemetry) | Auto-detected or `null` | | `appBuild` | Your app build number (for telemetry) | Auto-detected or `null` | | `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 | Read the exact configuration options each SDK accepts in each of their READMEs. ## Security Features All SDKs implement: - **Ed25519 Signatures**: Offline artifacts 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]] - [[sdks/rust|Rust SDK Documentation]] - [[getting-started/2-quickstart|Quickstart]] - [[guides/offline-licensing|Offline Licensing Guide]] --- ## 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) // Heartbeat heartbeatInterval: 300000, // 5 minutes (in ms) // App Info appVersion: null, // Your app version (e.g., '2.1.0') appBuild: null, // Your app build number (e.g., '42') // 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) | | `heartbeatInterval` | `number` | `300000` | Standalone heartbeat interval (ms, 0 = disabled) | | `appVersion` | `string` | `null` | Your app version for telemetry | | `appBuild` | `string` | `null` | Your app build number for telemetry | | `offlineFallbackEnabled` | `boolean` | `false` | Enable offline validation | | `maxOfflineDays` | `number` | `0` | Max offline days (0 = disabled) | | `offlineLicenseRefreshInterval` | `number` | `259200000` | Legacy 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 fingerprint (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 | | **Heartbeat** | | | | `heartbeat:success` | `{ received_at }` | Heartbeat acknowledged by server | | `heartbeat:error` | `{ error }` | Heartbeat request failed | | **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 legacy offline token | | `offlineToken:fetched` | `{ licenseKey, data }` | Legacy 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. > **Note:** The current JavaScript SDK offline flow still uses signed offline tokens. Machine files are the newer preferred offline artifact at the API level, but the JavaScript SDK has not migrated to them yet. Treat offline tokens as the current JS implementation detail, not the long-term product direction. ### 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**: Legacy 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 (downloads the legacy token + signing key, caches them) await sdk.syncOfflineAssets(); // Verify cached offline token (use when offline) const result = await sdk.verifyCachedOffline(); // { valid: true, offline: true, license: {...}, activation: {...} } // Get offline token from server const token = await sdk.getOfflineToken(); // Get signing key const signingKey = await sdk.getSigningKey('key-id-001'); // Verify a specific token manually 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()`. ## Telemetry The SDK automatically collects and sends the following telemetry fields on every API call: | Field | Browser | Node.js | Electron | |-------|---------|---------|----------| | `sdk_name` | `js` | `js` | `js` | | `sdk_version` | Yes | Yes | Yes | | `os_name` | Yes | Yes | Yes | | `os_version` | Yes | Yes | Yes | | `platform` | `browser` | `node` | `electron` | | `device_model` | Via userAgentData | -- | -- | | `device_type` | `desktop`/`phone`/`tablet` | `server` | `desktop` | | `architecture` | Via userAgentData | `process.arch` | `process.arch` | | `cpu_cores` | `navigator.hardwareConcurrency` | `os.cpus().length` | Yes | | `memory_gb` | `navigator.deviceMemory` | `os.totalmem()` | Yes | | `locale` | `navigator.language` | `process.env.LANG` | Yes | | `language` | 2-letter code from locale | 2-letter code from locale | Yes | | `timezone` | `Intl.DateTimeFormat` | `Intl.DateTimeFormat` | Yes | | `screen_resolution` | `screen.width`x`screen.height` | -- | Yes | | `display_scale` | `window.devicePixelRatio` | -- | Yes | | `browser_name` | Chrome, Safari, Firefox, Edge | -- | -- | | `browser_version` | Detected from UA/brands | -- | -- | | `runtime_version` | -- | `process.versions.node` | `process.versions.electron` | | `app_version` | From config | From config | From config | | `app_build` | From config | From config | From config | See [[api-reference/telemetry|Telemetry]] for the full field reference. ## 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 --- ## Rust SDK The official Rust SDK for LicenseSeat provides a comprehensive API for managing software licenses in native Rust applications and Tauri v2 apps. The SDK includes first-class TypeScript bindings for Tauri frontends. ## Packages This SDK provides two crates: | Crate | Description | Links | | ----- | ----------- | ----- | | `licenseseat` | Core Rust SDK for any Rust application | [crates.io](https://crates.io/crates/licenseseat) | | `tauri-plugin-licenseseat` | Tauri v2 plugin with TypeScript bindings | [crates.io](https://crates.io/crates/tauri-plugin-licenseseat) / [npm](https://www.npmjs.com/package/@licenseseat/tauri-plugin) | ## Installation ### Tauri Apps **1. Add the Rust plugin:** ```bash cd src-tauri cargo add tauri-plugin-licenseseat ``` **2. Add the TypeScript bindings:** ```bash # npm npm add @licenseseat/tauri-plugin # pnpm pnpm add @licenseseat/tauri-plugin # yarn yarn add @licenseseat/tauri-plugin # bun bun add @licenseseat/tauri-plugin ``` ### Pure Rust ```bash cargo add licenseseat ``` For offline validation support: ```bash cargo add licenseseat --features offline ``` ## Quick Start (Tauri) ### 1. Register the Plugin ```rust // src-tauri/src/main.rs (or lib.rs) fn main() { tauri::Builder::default() .plugin(tauri_plugin_licenseseat::init()) .run(tauri::generate_context!()) .expect("error while running tauri application"); } ``` ### 2. Add Configuration ```json // tauri.conf.json { "plugins": { "licenseseat": { "apiKey": "pk_live_xxxxxxxx", "productSlug": "your-product" } } } ``` ### 3. Add Permissions ```json // src-tauri/capabilities/default.json { "identifier": "default", "windows": ["main"], "permissions": [ "core:default", "licenseseat:default" ] } ``` ### 4. Use in Your Frontend ```typescript import { activate, hasEntitlement, getStatus } from '@licenseseat/tauri-plugin'; // Activate a license const license = await activate('USER-LICENSE-KEY'); console.log(`Device ID: ${license.deviceId}`); // Check entitlements if (await hasEntitlement('pro-features')) { enableProFeatures(); } // Get current status const status = await getStatus(); console.log(`Status: ${status.status}`); ``` ## Quick Start (Pure Rust) ```rust use licenseseat::{LicenseSeat, Config}; #[tokio::main] async fn main() -> licenseseat::Result<()> { // 1. Initialize the SDK let sdk = LicenseSeat::new(Config::new("api-key", "product-slug")); // 2. Activate a license let license = sdk.activate("USER-LICENSE-KEY").await?; println!("Activated! Device ID: {}", license.device_id); // 3. Validate the license let result = sdk.validate().await?; if result.valid { println!("License is valid!"); } // 4. Check entitlements if sdk.has_entitlement("pro-features") { println!("Pro features enabled!"); } // 5. Deactivate when done sdk.deactivate().await?; Ok(()) } ``` ## Configuration ### Tauri Plugin Configuration ```json // tauri.conf.json { "plugins": { "licenseseat": { "apiKey": "pk_live_xxxxxxxx", "productSlug": "your-product", "apiBaseUrl": "https://licenseseat.com/api/v1", "autoValidateInterval": 3600, "heartbeatInterval": 300, "offlineFallbackMode": "network_only", "maxOfflineDays": 0, "telemetryEnabled": true, "debug": false } } } ``` ### Pure Rust Configuration ```rust use licenseseat::{Config, OfflineFallbackMode}; use std::time::Duration; let config = Config { api_key: "pk_live_xxxxxxxx".into(), product_slug: "your-product".into(), api_base_url: "https://licenseseat.com/api/v1".into(), auto_validate_interval: Duration::from_secs(3600), heartbeat_interval: Duration::from_secs(300), offline_fallback_mode: OfflineFallbackMode::NetworkOnly, max_offline_days: 7, telemetry_enabled: true, app_version: Some("1.0.0".into()), debug: false, ..Default::default() }; let sdk = LicenseSeat::new(config); ``` ### Configuration Options | Option | Type | Default | Description | | ------ | ---- | ------- | ----------- | | `apiKey` / `api_key` | `String` | — | Your LicenseSeat API key (required) | | `productSlug` / `product_slug` | `String` | — | Your product slug (required) | | `apiBaseUrl` / `api_base_url` | `String` | `https://licenseseat.com/api/v1` | API base URL | | `autoValidateInterval` / `auto_validate_interval` | `number` / `Duration` | `3600` (1 hour) | Background validation interval | | `heartbeatInterval` / `heartbeat_interval` | `number` / `Duration` | `300` (5 min) | Heartbeat interval | | `offlineFallbackMode` / `offline_fallback_mode` | `string` / `OfflineFallbackMode` | `network_only` | Offline validation behavior | | `maxOfflineDays` / `max_offline_days` | `number` / `u32` | `0` | Grace period for offline mode (days) | | `telemetryEnabled` / `telemetry_enabled` | `boolean` / `bool` | `true` | Send device telemetry | | `debug` | `boolean` / `bool` | `false` | Enable debug logging | ### Offline Fallback Modes | Mode | Description | | ---- | ----------- | | `network_only` / `NetworkOnly` | Always require network validation (default) | | `allow_offline` / `AllowOffline` | Fall back to the cached offline artifact when network is unavailable (currently a signed offline token) | | `offline_first` / `OfflineFirst` | Prefer offline validation, sync when online | ## License Lifecycle ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Activate │────▶│ Validate │────▶│ Deactivate │ └─────────────┘ └─────────────┘ └─────────────┘ │ ▼ ┌─────────────┐ │ Heartbeat │ (periodic) └─────────────┘ ``` ### TypeScript API ```typescript import { activate, validate, deactivate, getStatus, heartbeat } from '@licenseseat/tauri-plugin'; // Activate a license const license = await activate('USER-LICENSE-KEY'); console.log(`Device ID: ${license.deviceId}`); console.log(`Activation ID: ${license.activationId}`); // Validate the current license const result = await validate(); if (result.valid) { console.log(`Plan: ${result.license.planKey}`); } else { console.log(`Invalid: ${result.code} - ${result.message}`); } // Get current status const status = await getStatus(); switch (status.status) { case 'active': enableFeatures(); break; case 'expired': showRenewalPrompt(); break; case 'inactive': case 'invalid': showActivationPrompt(); break; } // Send manual heartbeat await heartbeat(); // Deactivate (release the seat) await deactivate(); ``` ### Rust API ```rust // Activate let license = sdk.activate("USER-LICENSE-KEY").await?; // Validate let result = sdk.validate().await?; if result.valid { println!("Plan: {}", result.license.plan_key); } // Heartbeat let response = sdk.heartbeat().await?; println!("Received at: {}", response.received_at); // Deactivate sdk.deactivate().await?; ``` ## Entitlements Check feature access based on license entitlements. ### TypeScript ```typescript import { hasEntitlement, checkEntitlement } from '@licenseseat/tauri-plugin'; // Simple check if (await hasEntitlement('cloud-sync')) { enableCloudSync(); } // Detailed status const status = await checkEntitlement('pro-features'); if (status.active) { enableProFeatures(); } else { switch (status.reason) { case 'expired': showRenewalPrompt(); break; case 'notfound': showUpgradePrompt(); break; case 'nolicense': showActivationPrompt(); break; } } ``` ### Rust ```rust // Simple check if sdk.has_entitlement("cloud-sync") { enable_cloud_sync(); } // Detailed status let status = sdk.check_entitlement("pro-features"); match status.reason { EntitlementReason::Active => println!("Active!"), EntitlementReason::Expired => println!("Expired at {:?}", status.expires_at), EntitlementReason::NotFound => println!("Not included in plan"), EntitlementReason::NoLicense => println!("No active license"), } // List all entitlements for entitlement in sdk.entitlements() { println!("{}: {:?}", entitlement.key, entitlement.expires_at); } ``` ## React Integration ```tsx import { useState, useEffect } from 'react'; import { getStatus, hasEntitlement, activate, LicenseStatus } from '@licenseseat/tauri-plugin'; function useLicense() { const [status, setStatus] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { getStatus() .then(setStatus) .finally(() => setLoading(false)); }, []); return { status, loading }; } function useEntitlement(key: string) { const [active, setActive] = useState(false); useEffect(() => { hasEntitlement(key).then(setActive); }, [key]); return active; } function App() { const { status, loading } = useLicense(); const hasProFeatures = useEntitlement('pro-features'); if (loading) return ; if (status?.status !== 'active') { return ; } return (

Welcome!

{hasProFeatures && }
); } ``` ## Vue Integration ```vue ``` ## Svelte Integration ```svelte {#if status?.status === 'active'}

Welcome!

{#if hasProFeatures} {/if} {:else} {/if} ``` ## Event System ### TypeScript (Tauri Events) ```typescript import { listen } from '@tauri-apps/api/event'; // Listen for license events await listen('licenseseat://activation-success', (event) => { console.log('License activated!', event.payload); }); await listen('licenseseat://validation-success', (event) => { console.log('License validated!', event.payload); refreshUI(); }); await listen('licenseseat://validation-failed', (event) => { console.log('Validation failed:', event.payload); showLicenseError(); }); await listen('licenseseat://heartbeat-success', () => { updateConnectionStatus(true); }); await listen('licenseseat://heartbeat-error', () => { updateConnectionStatus(false); }); ``` ### Rust ```rust use licenseseat::{LicenseSeat, EventKind}; let sdk = LicenseSeat::new(config); let mut events = sdk.subscribe(); tokio::spawn(async move { while let Ok(event) = events.recv().await { match event.kind { EventKind::ActivationSuccess => println!("License activated!"), EventKind::ValidationSuccess => println!("Validation succeeded"), EventKind::ValidationFailed => println!("Validation failed"), EventKind::HeartbeatSuccess => println!("Heartbeat OK"), EventKind::HeartbeatError => println!("Heartbeat failed"), EventKind::DeactivationSuccess => println!("Deactivated"), _ => {} } } }); ``` ### Available Events | Event | Description | | ----- | ----------- | | `ActivationSuccess` / `licenseseat://activation-success` | License successfully activated | | `ActivationError` / `licenseseat://activation-error` | Activation failed | | `ValidationSuccess` / `licenseseat://validation-success` | License validated successfully | | `ValidationFailed` / `licenseseat://validation-failed` | Validation failed | | `DeactivationSuccess` / `licenseseat://deactivation-success` | License deactivated | | `HeartbeatSuccess` / `licenseseat://heartbeat-success` | Heartbeat acknowledged | | `HeartbeatError` / `licenseseat://heartbeat-error` | Heartbeat failed | ## Offline Validation Enable Ed25519 cryptographic offline validation for air-gapped or unreliable network environments. ### Tauri Configuration ```json { "plugins": { "licenseseat": { "offlineFallbackMode": "allow_offline", "maxOfflineDays": 7 } } } ``` ### Rust Configuration ```rust use licenseseat::{Config, OfflineFallbackMode}; let config = Config { offline_fallback_mode: OfflineFallbackMode::AllowOffline, max_offline_days: 7, ..Default::default() }; ``` ### How Offline Validation Works > **Offline model note:** the current Rust/Tauri SDK still uses signed offline tokens today. Machine files are the newer preferred offline artifact at the API level, but this SDK has not migrated yet. 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 bypassing expiration ### Security Features - **Ed25519 Signatures**: Offline licenses are cryptographically signed - **Clock Tamper Detection**: Detects system clock manipulation - **Grace Period**: Configurable offline validity period - **Secure Storage**: Tokens cached locally with platform-appropriate storage ## TypeScript API Reference ### Functions | Function | Description | Returns | | -------- | ----------- | ------- | | `activate(key, options?)` | Activate a license key | `Promise` | | `validate()` | Validate current license | `Promise` | | `deactivate()` | Deactivate and release seat | `Promise` | | `getStatus()` | Get current license status | `Promise` | | `hasEntitlement(key)` | Check if entitlement is active | `Promise` | | `checkEntitlement(key)` | Get detailed entitlement status | `Promise` | | `heartbeat()` | Send heartbeat ping | `Promise` | | `getLicense()` | Get cached license | `Promise` | | `reset()` | Clear SDK state | `Promise` | ### Types ```typescript interface License { licenseKey: string; deviceId: string; activationId: string; activatedAt: string; } interface LicenseStatus { status: 'active' | 'inactive' | 'invalid' | 'pending' | 'offlineValid' | 'offlineInvalid'; message?: string; license?: string; device?: string; activatedAt?: string; lastValidated?: string; } interface ValidationResult { valid: boolean; code?: string; message?: string; license: { key: string; status: string; planKey: string; activeEntitlements: Array<{ key: string; expiresAt?: string }>; }; } interface EntitlementStatus { active: boolean; reason?: 'nolicense' | 'notfound' | 'expired'; expiresAt?: string; } interface ActivationOptions { deviceId?: string; deviceName?: string; metadata?: Record; } ``` ## Error Handling ### TypeScript ```typescript try { const license = await activate(key); showSuccess('License activated!'); } catch (error) { const message = error as string; if (message.includes('invalid')) { showError('Invalid license key'); } else if (message.includes('limit')) { showError('Device limit reached. Deactivate another device first.'); } else if (message.includes('expired')) { showError('This license has expired'); } else if (message.includes('network')) { showError('Network error. Please check your connection.'); } else { showError(`Activation failed: ${message}`); } } ``` ### Rust ```rust use licenseseat::Error; match sdk.activate("KEY").await { Ok(license) => println!("Activated: {}", license.device_id), Err(Error::Api { code, message, .. }) => { println!("API error: {} - {}", code, message); } Err(Error::Network(e)) => { println!("Network error: {}", e); } Err(Error::Crypto(e)) => { println!("Offline validation failed: {}", e); } Err(e) => println!("Error: {}", e), } ``` ### 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 | ## Tauri Permissions The plugin uses Tauri's permission system for fine-grained access control. | Permission | Description | | ---------- | ----------- | | `licenseseat:default` | All commands (recommended) | | `licenseseat:allow-activate` | Only activation | | `licenseseat:allow-validate` | Only validation | | `licenseseat:allow-deactivate` | Only deactivation | | `licenseseat:allow-status` | Only status checks | | `licenseseat:allow-entitlements` | Only entitlement checks | ## Telemetry The SDK automatically collects and sends the following telemetry: | Field | Description | | ----- | ----------- | | `sdk_name` | `rust` or `tauri` | | `sdk_version` | SDK version | | `os_name` | Operating system (macOS, Windows, Linux) | | `os_version` | OS version | | `platform` | `native` or `tauri` | | `device_type` | `desktop` | | `app_version` | Your app version (if configured) | See [[api-reference/telemetry|Telemetry]] for the full field reference. ## Platform Support | Platform | Minimum Version | Notes | | -------- | --------------- | ----- | | Rust | 1.70+ | Async runtime required (tokio) | | Tauri | v2.0.0+ | Full plugin support | | macOS | 10.15+ | Full support | | Windows | 10+ | Full support | | Linux | glibc 2.31+ | Full support | ## Security ### API Key Protection Your API key is stored in `tauri.conf.json` and compiled into your app binary. It is not exposed to the JavaScript frontend. ### Device Fingerprinting The SDK generates a stable device ID based on hardware characteristics using `machine-uid`. This ID is: - Stable across app restarts - Not personally identifiable - Used for seat tracking and offline validation ### TLS The SDK uses `rustls` by default for TLS, with no OpenSSL dependency. ## Troubleshooting ### Plugin Not Loading 1. Ensure the plugin is registered in `main.rs` or `lib.rs`: ```rust .plugin(tauri_plugin_licenseseat::init()) ``` 2. Check that permissions are added to your capability file. 3. Rebuild the Rust backend: ```bash cd src-tauri && cargo build ``` ### "Command not found" Error Make sure you've installed the JS bindings: ```bash npm add @licenseseat/tauri-plugin ``` ### Debug Logging Enable debug mode to see detailed SDK logs: ```json { "plugins": { "licenseseat": { "debug": true } } } ``` Then check the Tauri console output for `[licenseseat]` prefixed messages. ## Next Steps - [[sdks/javascript|JavaScript SDK]] - For Electron and browser apps - [[sdks/swift|Swift SDK]] - For native macOS/iOS apps - [[api-reference/offline-token|Offline Licensing]] - Air-gapped validation - [[api-reference/index|API Reference]] - Direct API access --- ## Swift SDK The official Swift SDK for LicenseSeat provides a comprehensive, type-safe API for managing software licenses on Apple platforms. ## Installation You can install the Swift SDK using the SPM (Swift Package Manager) or through Xcode: ### Swift Package Manager Add to your `Package.swift`: ```swift dependencies: [ .package(url: "https://github.com/licenseseat/licenseseat-swift.git", from: "0.4.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 heartbeatInterval: 300, // Heartbeat every 5 minutes 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.heartbeatInterval = 300 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 | | `heartbeatInterval` | `TimeInterval` | `300` (5 min) | Standalone heartbeat interval (0 = disabled) | | `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) | Legacy 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` | Legacy offline token signature verified | | `offlineToken:verificationFailed` | Legacy offline token verification failed | | `heartbeat:success` | Heartbeat acknowledged by server | | `heartbeat:error` | Heartbeat request 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: > **Note:** The current Swift SDK offline flow still uses signed offline tokens. Machine files are the newer preferred offline artifact at the API level, but the Swift SDK has not migrated to them yet. Treat offline tokens as the current Swift implementation detail, not the long-term product direction. ```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 ### Manual Offline Methods ```swift // Sync offline assets (downloads the legacy token + signing key, caches them) try await LicenseSeat.shared.syncOfflineAssets() // Verify cached offline token (use when offline) let result = try await LicenseSeat.shared.verifyCachedOffline() // ValidationResult with valid, offline, license, activation ``` ## API Response Format The v1 API uses Stripe-style conventions with `object` fields identifying response types. ### Activation Response ```json { "object": "activation", "id": 12345, "fingerprint": "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 | ## Telemetry The SDK automatically collects and sends the following telemetry fields on every API call: | Field | Source | |-------|--------| | `sdk_name` | Always `swift` | | `sdk_version` | `LicenseSeatConfig.sdkVersion` | | `os_name` | `macOS`, `iOS`, `tvOS`, `watchOS`, `visionOS` | | `os_version` | `ProcessInfo.operatingSystemVersion` | | `platform` | `native` | | `device_model` | `sysctlbyname("hw.model")` | | `app_version` | `CFBundleShortVersionString` | | `app_build` | `CFBundleVersion` | | `device_type` | `desktop`, `phone`, `tablet`, `watch`, `tv`, `headset` | | `architecture` | `arm64` or `x64` (compile-time) | | `cpu_cores` | `ProcessInfo.processorCount` | | `memory_gb` | `ProcessInfo.physicalMemory` (rounded to nearest GB) | | `locale` | `Locale.current.identifier` | | `language` | 2-letter code extracted from locale | | `timezone` | `TimeZone.current.identifier` | | `screen_resolution` | Native pixel resolution (macOS/iOS only) | | `display_scale` | `NSScreen.backingScaleFactor` / `UIScreen.scale` | See [[api-reference/telemetry|Telemetry]] for the full field reference. ## 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 API key authentication and API health: ```swift do { let result = try await LicenseSeatStore.shared.testAuth() print("Authenticated: \(result.authenticated)") // true if API key is valid print("Healthy: \(result.healthy)") // API health status print("API Version: \(result.apiVersion)") // e.g., "1.0.0" } catch { print("Auth test failed: \(error)") } ``` ### AuthTestResult Properties | Property | Type | Description | | -------- | ---- | ----------- | | `authenticated` | `Bool` | Whether the API key is valid | | `healthy` | `Bool` | Whether the API is healthy | | `apiVersion` | `String` | The API version (e.g., `1.0.0`) | ## 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 - [[guides/offline-licensing|Offline Licensing Guide]] - In-depth offline documentation - [[guides/air-gapped-licensing|Air-Gapped Licensing]] - For devices without any internet - [[api-reference/index|API Reference]] - Direct API access ---