C++ SDK
licensr is the licensing API for native hosts (audio plugins, games, desktop apps). No third-party dependencies: JSON, HTTP, and Ed25519 are vendored or use the OS stack (WinHTTP, NSURLSession, libcurl).
It is built so two plugins can link Licensr into the same DAW without colliding:
- Hidden symbols under
licensr::. - One background worker per install (
api_key+plugin_slug+base_url), not perClient. - Network calls are async. Never call them from the audio thread. Use
Client::snapshot()there (lock-free, no alloc).
Install
CMake FetchContent against the public mirror. Pin GIT_TAG to a tag from tekunodev/licensr-cpp releases (not main). If no tag is published yet, vendor the tree and add_subdirectory.
include(FetchContent)
FetchContent_Declare(licensr
GIT_REPOSITORY https://github.com/tekunodev/licensr-cpp.git
GIT_TAG v0.2.0
)
FetchContent_MakeAvailable(licensr)
target_link_libraries(your_plugin PRIVATE licensr::licensr)
Or vendor sdks/cpp/ directly and add_subdirectory(...).
An optional licensr_juce target ships a JUCE module (LicenseActivationComponent) for JUCE-based audio plugins — see the demo synth for a full VST3/AU/Standalone example.
Quickstart
#include <licensr/licensr.h>
licensr::ClientConfig config;
config.api_key = "pk_live_..."; // Client key — safe to embed; see shipping the API key
config.plugin_slug = "my-plugin";
licensr::Client client(config);
client.validate(user_entered_key, [](licensr::Result<licensr::ValidateResult> result) {
if (!result.ok()) {
// result.error().kind is kApi (branch on .code) or kNetwork.
return;
}
if (result.value().valid) {
// unlock full features
} else if (result.value().entitlement == licensr::Entitlement::kLimited) {
// perpetual-fallback: expired, but the plan grants a limited tier
}
});
// Elsewhere, e.g. once per processBlock — never blocks, never allocates:
if (auto snapshot = client.snapshot()) {
bool valid = snapshot->valid;
}
The result keeps what you act on at the top level: valid, status (why, for your message), entitlement and feature_flags. Plan and limit details are grouped under result.license (plan_id, activation_mode, max_seats, max_domains, seats_in_use, expires_at). They are for display only; never use them to decide whether to unlock.
See embedded-key doctrine. Local JWT verify is not in this SDK yet — token() mints and you can persist with FileTokenStore; verify with the JS or C# helper, or wait for the C++ follow-up.
API
Every method is asynchronous and calls callback from Licensr's worker thread with a Result<T> — never both a value and an error, and never thrown.
| Method | Wraps | Notes |
|---|---|---|
client.validate(licenseKey, callback) | POST /v1/license/validate | Always read valid/entitlement; never branch on HTTP status — an unknown key returns 200 {valid: false}. |
client.token(licenseKey, callback) | POST /v1/license/token | Same check as validate, plus a short-lived signed offline token. Verify server-side or via the JS/TS SDK's verifyOfflineToken for now. |
client.activate(request, callback) | POST /v1/license/activate | activation_type is kSeat (per-machine) or kDomain, fixed by the plugin's configured mode. |
client.deactivate(licenseKey, activationId, callback) | POST /v1/license/deactivate | Frees a seat/domain slot. |
client.activations(licenseKey, callback) | GET /v1/license/activations | Lists every current activation. |
client.checkout(request, callback) | POST /v1/billing/checkout | Returns checkout_url — direct the user there. Requires a key with the checkout scope. |
Client options
licensr::ClientConfig config;
config.api_key = "pk_live_...";
config.plugin_slug = "my-plugin";
config.base_url = "https://api.licensr.app"; // default; override for self-hosted/staging
config.device_id = stable_hwid; // buckets rate limits per installation instead of per key — reuse your activate() identifier
config.origin = "app://my-plugin"; // rarely needed; set client type Native in the admin UI
config.timeout_ms = 10000;
config.retry = {/* max_retries */ 2, /* base_delay_ms */ 300, /* max_delay_ms */ 5000}; // network errors / 429 / 5xx, exponential backoff + jitter, honors Retry-After
config.transport = my_custom_transport; // bring your own HTTP stack instead of the platform default
Events
client.events().on_validated = [](const licensr::ValidateResult& result) { /* ... */ };
client.events().on_retry = [](const std::string& method, int attempt, int delay_ms) { /* ... */ };
client.events().on_error = [](const std::string& method, const licensr::Error& error) { /* report to crash reporting */ };
Available hooks: on_validated, on_activated, on_deactivated, on_token_issued, on_retry, on_error. Because multiple Client instances for the same installation share one client core (see above), event hooks are a broadcast registry — every live Client's hooks fire for every request on that installation, not just the one that issued it.
Errors
licensr::Error has an ErrorKind:
kApi— non-2xx response with a parsed error body.statusis the HTTP status,codeis one of the documented error codes (e.g.license_inactive,origin_not_allowed,rate_limit_exceeded) — branch oncode, notmessage.retry_after_secondsis set on some429responses.kNetwork— the request never got a response: DNS/connect/TLS failure, timeout, or retries were exhausted.
Hardware IDs
licensr::compute_hardware_id(salt) reads a platform-specific machine identifier — IOPlatformUUID (macOS/iOS), MachineGuid + system volume serial (Windows), /etc/machine-id (Linux) — and returns sha256(salt + raw_id) as a lowercase hex string, so the real platform identifier never leaves the process:
#include <licensr/hwid.h>
std::string device_id = licensr::compute_hardware_id(config.plugin_slug).value_or(generate_and_persist_a_fallback_uuid());
config.device_id = device_id; // buckets rate limits per installation
licensr::ActivateRequest request;
request.activation_type = licensr::ActivationType::kSeat;
request.identifier = device_id; // ties the seat activation to this machine
Use a stable, plugin-specific salt (your plugin_slug is a reasonable default) — the same machine produces a different, uncorrelatable identifier for a different salt. compute_hardware_id returns std::nullopt under restricted permissions or an unusual sandbox; fall back to a self-generated UUID you persist yourself in that case.
Token storage
FileTokenStore caches a string blob — typically the token from Client::token's TokenResult::token — per license key, under a per-plugin directory in the platform's app-data location (~/Library/Application Support/Licensr/<plugin_slug>/ on macOS, %APPDATA%\Licensr\<plugin_slug>\ on Windows, $XDG_DATA_HOME/licensr/<plugin_slug>/ on Linux):
#include <licensr/token_store.h>
licensr::FileTokenStore store(config.plugin_slug);
store.save(license_key, token_result.token);
AUv3 app extensions run in their own sandboxed container, separate from the host app's — pass an explicit base_dir (your app group's shared container) in that case: licensr::FileTokenStore store(config.plugin_slug, auv3_container_path);. Bring your own TokenStore implementation instead if your host already has its own persistence (a DAW's plugin-state chunk, a keychain wrapper).
HTTP transport
Licensr picks a platform default at CMake configure time: WinHTTP on Windows, NSURLSession on macOS/iOS, libcurl on Linux — all three ride the OS's own TLS/certificate store rather than vendoring one. Set LICENSR_HTTP_BACKEND=NONE and supply ClientConfig::transport to plug in your own (a DAW host's networking callback, a game engine's HTTP client) — implement licensr::HttpTransport, whose single perform() method is called synchronously from Licensr's worker thread and must never be invoked from the audio thread.
Source & issues
Source lives in github.com/tekunodev/licensr-cpp, mirrored from this monorepo's sdks/cpp/. File issues and PRs there.