C++ SDK
C++ SDK
Official C++ SDK for LicenseSeat. Add license validation to native applications, Unreal Engine games, and VST/AU audio plugins.
Building a VST plugin or Unreal game? We provide a single-header JUCE integration and an Unreal Engine plugin with zero external dependencies.
Features
- License activation & deactivation - Automatic device fingerprinting
- Online & offline validation - Ed25519 cryptographic verification
- Entitlement checking -
has_entitlement()andcheck_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<T, Error>pattern
Installation
CMake (FetchContent)
include(FetchContent)
FetchContent_Declare(
licenseseat
GIT_REPOSITORY https://github.com/licenseseat/licenseseat-cpp.git
GIT_TAG main
)
FetchContent_MakeAvailable(licenseseat)
target_link_libraries(your_target PRIVATE licenseseat::licenseseat)
Manual Build
git clone https://github.com/licenseseat/licenseseat-cpp.git
cd licenseseat-cpp
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
sudo cmake --install build
Dependencies
- nlohmann/json - JSON parsing
- cpp-httplib - HTTP client (with OpenSSL for HTTPS)
Cryptographic operations (Ed25519, SHA-256) use vendored libraries with no external dependencies.
Quick Start
#include <licenseseat/licenseseat.hpp>
int main()
{
licenseseat::Config config;
config.api_key = "pk_live_xxxxxxxx";
config.product_slug = "your-product";
licenseseat::Client client(config);
// Validate a license
auto result = client.validate("XXXX-XXXX-XXXX-XXXX");
if (result.is_ok()) {
const auto& validation = result.value();
if (validation.valid) {
std::cout << "License is valid!\n";
std::cout << "Plan: " << validation.license.plan_key() << "\n";
} else {
std::cout << "Invalid: " << validation.code << " - " << validation.message << "\n";
}
} else {
std::cerr << "Error: " << result.error_message() << "\n";
}
// Check entitlements
if (client.has_entitlement("pro")) {
// Enable pro features
}
return 0;
}
Configuration
licenseseat::Config config;
// Required
config.api_key = "pk_live_xxxxxxxx";
config.product_slug = "your-product";
// Optional - API settings
config.api_url = "https://licenseseat.com/api/v1"; // Default
config.timeout_seconds = 30;
config.max_retries = 3;
// Optional - Device identification
config.device_id = ""; // Auto-generated if empty
// Optional - Offline support
config.signing_public_key = "base64-ed25519-public-key"; // Pre-configure for offline
config.max_offline_days = 30;
// Optional - Caching
config.storage_path = ""; // Path for license cache (empty = no persistence)
// Optional - Auto-validation
config.auto_validate_interval = 3600.0; // Seconds between background validations
Configuration Options
| Option | Type | Default | Description |
|---|---|---|---|
api_key |
string |
required | Your publishable API key |
product_slug |
string |
required | Product identifier |
api_url |
string |
https://licenseseat.com/api/v1 |
API endpoint |
timeout_seconds |
int |
30 |
HTTP request timeout |
max_retries |
int |
3 |
Retry attempts for failed requests |
device_id |
string |
"" |
Device identifier (auto-generated if empty) |
signing_public_key |
string |
"" |
Ed25519 public key for offline verification |
max_offline_days |
int |
0 |
Maximum days offline (0 = disabled) |
storage_path |
string |
"" |
Path for license cache (empty = no persistence) |
auto_validate_interval |
double |
3600.0 |
Seconds between auto-validation cycles |
Validation
Validation checks if a license is valid. The API always returns HTTP 200 for validation - check the valid field to determine validity.
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_idto validate:auto result = client.validate("LICENSE-KEY", device_id);Without it, validation may return
valid: falsewith codedevice_not_activated.
Async Validation
client.validate_async("LICENSE-KEY", [](licenseseat::Result<licenseseat::Validation> result) {
if (result.is_ok() && result.value().valid) {
// License is valid
}
});
Activation
Activation binds a license to a device, consuming a seat.
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.
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
// Simple boolean check (uses cached license data)
if (client.has_entitlement("pro")) {
enable_pro_features();
}
// Detailed check with reason
auto entitlement = client.check_entitlement("feature-key");
if (entitlement.active) {
// Feature unlocked
} else {
std::cout << "Not available: " << entitlement.reason << "\n";
}
Status
Get the current cached license status without making a network request.
auto status = client.get_status();
std::cout << "Valid: " << (status.valid ? "yes" : "no") << "\n";
std::cout << "Code: " << status.code << "\n";
Offline Support
The SDK supports offline license validation using Ed25519 cryptographic signatures.
Offline Token Workflow
Step 1: Generate and cache offline token while online
// Generate offline token (requires network)
auto token_result = client.generate_offline_token("LICENSE-KEY");
if (token_result.is_error()) {
std::cerr << "Failed to generate token: " << token_result.error_message() << "\n";
return;
}
auto offline_token = token_result.value();
// Fetch signing key (requires network)
auto key_result = client.fetch_signing_key(offline_token.token.kid);
if (key_result.is_error()) {
std::cerr << "Failed to fetch key: " << key_result.error_message() << "\n";
return;
}
std::string public_key = key_result.value();
// Store both for offline use
save_to_disk(offline_token, public_key);
Step 2: Verify offline (no network required)
// Load cached data
auto [offline_token, public_key] = load_from_disk();
// Verify signature locally
auto verify_result = client.verify_offline_token(offline_token, public_key);
if (verify_result.is_ok() && verify_result.value()) {
// Token is valid - license data available in offline_token.token
std::cout << "License: " << offline_token.token.license_key << "\n";
std::cout << "Plan: " << offline_token.token.plan_key << "\n";
std::cout << "Expires: " << offline_token.token.exp << "\n";
// Check entitlements from token
for (const auto& ent : offline_token.token.entitlements) {
std::cout << "Entitlement: " << ent.key << "\n";
}
}
Pre-configured Public Key
For simpler deployments, pre-configure the signing public key:
licenseseat::Config config;
config.api_key = "pk_live_xxxxxxxx";
config.product_slug = "your-product";
config.signing_public_key = "MCowBQYDK2VwAyEA..."; // Your public key
config.max_offline_days = 30;
licenseseat::Client client(config);
// Now verify_offline_token can use the pre-configured key
auto result = client.verify_offline_token(offline_token); // No key param needed
Offline Token Fields
| Field | Type | Description |
|---|---|---|
license_key |
string |
The license key |
product_slug |
string |
Product identifier |
plan_key |
string |
Plan identifier |
mode |
string |
License mode (hardware_locked or floating) |
seat_limit |
int |
Maximum allowed activations |
device_id |
string |
Device this token is bound to (if hardware_locked) |
iat |
int64 |
Issued at (Unix timestamp) |
exp |
int64 |
Expires at (Unix timestamp) |
nbf |
int64 |
Not valid before (Unix timestamp) |
kid |
string |
Key ID for fetching the signing public key |
entitlements |
array |
List of entitlements with keys and expiration |
metadata |
object |
Custom metadata attached to the license |
Auto-Validation
Background validation at configurable intervals.
// 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.
#include <licenseseat/events.hpp>
// Subscribe to validation success
auto sub1 = client.on(licenseseat::events::VALIDATION_SUCCESS, [](const std::any& data) {
std::cout << "License validated successfully!\n";
});
// Subscribe to validation failure
auto sub2 = client.on(licenseseat::events::VALIDATION_FAILED, [](const std::any& data) {
std::cout << "License validation failed\n";
});
// Subscribe to offline token ready
auto sub3 = client.on(licenseseat::events::OFFLINE_TOKEN_READY, [](const std::any& data) {
std::cout << "Offline token generated\n";
});
// Later: cancel subscriptions
sub1.cancel();
sub2.cancel();
sub3.cancel();
Available Events
| Event | Description |
|---|---|
LICENSE_LOADED |
License data loaded from cache |
ACTIVATION_START |
Activation request starting |
ACTIVATION_SUCCESS |
Device activated successfully |
ACTIVATION_ERROR |
Activation failed |
VALIDATION_START |
Validation request starting |
VALIDATION_SUCCESS |
License validated successfully |
VALIDATION_FAILED |
License validation returned invalid |
VALIDATION_ERROR |
Validation request failed (network, etc.) |
VALIDATION_OFFLINE_SUCCESS |
Offline token verified successfully |
VALIDATION_OFFLINE_FAILED |
Offline token verification failed |
DEACTIVATION_START |
Deactivation request starting |
DEACTIVATION_SUCCESS |
Device deactivated successfully |
DEACTIVATION_ERROR |
Deactivation failed |
NETWORK_ONLINE |
Network connectivity restored |
NETWORK_OFFLINE |
Network connectivity lost |
AUTOVALIDATION_CYCLE |
Auto-validation cycle completed |
AUTOVALIDATION_STOPPED |
Auto-validation stopped |
OFFLINE_TOKEN_READY |
Offline token generated |
OFFLINE_TOKEN_VERIFIED |
Offline token verified |
SDK_RESET |
SDK state reset |
Error Handling
The SDK uses a Result<T, Error> pattern instead of exceptions.
auto result = client.validate("LICENSE-KEY");
if (result.is_ok()) {
auto& validation = result.value();
// Success - check validation.valid for license validity
} else {
// Error - network, auth, or API error
std::cerr << "Error: " << result.error_message() << "\n";
switch (result.error_code()) {
case licenseseat::ErrorCode::NetworkError:
// No network connectivity
break;
case licenseseat::ErrorCode::LicenseNotFound:
// Invalid license key
break;
case licenseseat::ErrorCode::AuthenticationFailed:
// Invalid API key
break;
case licenseseat::ErrorCode::SeatLimitExceeded:
// Too many activations
break;
// ... handle other cases
}
}
Error Codes
| Code | Description |
|---|---|
Success |
Operation completed successfully |
NetworkError |
HTTP request failed (no connectivity) |
ConnectionTimeout |
Request timed out |
SSLError |
SSL/TLS error |
InvalidLicenseKey |
License key format is invalid |
LicenseNotFound |
License key not found in system |
LicenseExpired |
License has expired |
LicenseRevoked |
License has been revoked |
LicenseSuspended |
License is suspended |
LicenseNotActive |
License is not active |
LicenseNotStarted |
License hasn't started yet |
SeatLimitExceeded |
Maximum activations reached |
ActivationNotFound |
Device activation not found |
DeviceAlreadyActivated |
Device is already activated |
ProductNotFound |
Product slug not found |
AuthenticationFailed |
Invalid or missing API key |
PermissionDenied |
API key lacks required permissions |
ServerError |
Server-side error (5xx) |
SigningNotConfigured |
Offline signing not configured |
InvalidSignature |
Cryptographic signature invalid |
FileError |
File read/write error |
Unreal Engine Plugin
A complete UE plugin using native FHttpModule and FJsonObject. No external dependencies.
auto* LicenseSeat = GetGameInstance()->GetSubsystem<ULicenseSeatSubsystem>();
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 GameInstanceSubsystemfor 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.
#include "LicenseSeatJuceStandalone.h"
LicenseSeatJuceStandalone license("pk_live_xxxxxxxx", "your-plugin");
// Audio thread safe (reads std::atomic)
void processBlock(juce::AudioBuffer<float>& 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<bool>for lock-free status checks in audio threadMessageManager::callAsyncfor 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.
// 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<bool> for lock-free reads in real-time contexts.
Platform Support
| Platform | Compiler | Status |
|---|---|---|
| Linux | GCC 9+, Clang 10+ | Supported |
| macOS | Apple Clang 12+ (ARM & Intel) | Supported |
| Windows | MSVC 2019+ | Supported |
Device Identification
The SDK automatically generates a unique device identifier:
- macOS: IOKit Platform UUID
- Windows: Machine GUID from registry
- Linux:
/etc/machine-idor hostname-based fallback
Reset
Clear all cached data (license, offline tokens, etc.).
client.reset();
Next Steps
- C# SDK - For .NET applications
- Swift SDK - For Apple platforms
- Offline Licensing - Air-gapped validation
- API Reference - Direct API access