# 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] │
└────────────────────────────────────────────┘
```
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!

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:

## 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: 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
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:

## 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:

## 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 (