Skip to main content

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 per Client.
  • 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.

MethodWrapsNotes
client.validate(licenseKey, callback)POST /v1/license/validateAlways read valid/entitlement; never branch on HTTP status — an unknown key returns 200 {valid: false}.
client.token(licenseKey, callback)POST /v1/license/tokenSame 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/activateactivation_type is kSeat (per-machine) or kDomain, fixed by the plugin's configured mode.
client.deactivate(licenseKey, activationId, callback)POST /v1/license/deactivateFrees a seat/domain slot.
client.activations(licenseKey, callback)GET /v1/license/activationsLists every current activation.
client.checkout(request, callback)POST /v1/billing/checkoutReturns 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. status is the HTTP status, code is one of the documented error codes (e.g. license_inactive, origin_not_allowed, rate_limit_exceeded) — branch on code, not message. retry_after_seconds is set on some 429 responses.
  • 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.