Client SDKs

Beta

Add LicenseX to your loader

Both SDKs wrap the LicenseX /api/v1/* endpoints. Download the source files below, drop them into your project, initialise a client with your app credentials, and call Authenticate(). Every response is HMAC-SHA256-verified before your code sees it — the same way KeyAuth's SDKs work, except the server is yours.

Installation

Both SDKs are plain source files — download them, drop them into your project, and they work. No package manager, no build step to run first, nothing to install.

licensex.hpp
// LicenseX C++ SDK
// Copy this file and licensex.cpp into your project. Requires C++17 and
// links against winhttp.lib and bcrypt.lib (Windows only).
//
// Every response from the server (except InvalidCredentials, which the
// server can't sign — see below) carries an HMAC-SHA256 signature computed
// with your app's api_secret. This SDK verifies that signature before
// trusting anything in the response body, so a patched/cracked client
// would need to recover api_secret itself, not just flip a boolean.
#pragma once

#include <string>
#include <optional>

namespace licensex {

enum class ErrorCode {
    None,
    InvalidCredentials,  // app_id/api_secret rejected. Sent UNSIGNED — the
                          // server has no proof you're who you claim to be,
                          // so there's nothing to sign the response with.
    ServerRejected,       // Verified success:false — key is genuinely
                          // invalid/expired/banned/revoked, or (for
                          // ResetHwid) the reset limit was hit.
    SignatureInvalid,     // Response body's HMAC didn't match. Tampered in
                          // transit, or api_secret doesn't match the
                          // dashboard. NEVER trust auth.ok when this fires.
    ResetLimitReached,    // ResetHwid() specific: resetsUsed/resetsAllowed
                          // on the result say exactly where you stand.
    NotFound,             // GetVariable() specific: no variable with that
                          // name exists for this app.
    RateLimited,          // Too many requests — back off and retry.
    NetworkError,         // Request never got a response.
    Timeout,              // Server didn't respond in time.
    ParseError,           // Response body wasn't valid JSON.
};

struct LicenseInfo {
    std::string status;                       // "active" | "expired" | "banned" | "unused"
    std::optional<std::string> expiresAt;      // nullopt = lifetime license
};

struct AuthResult {
    bool ok = false;
    ErrorCode error = ErrorCode::None;
    std::string errorMessage;
    LicenseInfo license;
};

// ResetResult, VariableResult, LogResult, GetHardwareId(), and the full
// Client class declaration continue below — see licensex.hpp for the
// complete file (110 lines).

class Client {
public:
    Client(std::string baseUrl, std::string appId, std::string apiSecret);

    AuthResult Authenticate(const std::string& licenseKey, const std::string& hwid);
    ResetResult ResetHwid(const std::string& licenseKey);
    VariableResult GetVariable(const std::string& name);
    LogResult Log(const std::string& message, const std::string& licenseKey = "");

private:
    std::string baseUrl_, appId_, apiSecret_;
};

}  // namespace licensex

Then build:

$terminal
g++ -std=c++17 your_loader.cpp licensex.cpp \
    -I . -lwinhttp -lbcrypt -o loader.exe

# (Or add winhttp.lib and bcrypt.lib to your existing
# CMake / Visual Studio project's linker settings.)

Initialise the client

Create one client instance at startup and keep it alive. It holds your credentials and an HTTP connection pool internally.

loader.cpp
#include "licensex.hpp"

// Initialise once at startup — keep the client alive for the
// lifetime of your process. NOT thread-safe to construct concurrently.
licensex::Client client(
    "https://your-licensex-deployment.example.com",
    "app_your_app_id_here",
    "your_api_secret_here"   // load from encrypted storage in prod
);

Authenticate a key

Pass the license key and hardware ID. Every response is HMAC-SHA256-verified before your code sees it — except a credential rejection, which is sent unsigned (there'd be no secret to sign it with). ServerRejected after a clean verify means the key is genuinely invalid; SignatureInvalid means don't trust the body at all.

loader.cpp
std::string hwid = licensex::GetHardwareId();
std::string key  = GetKeyFromUser(); // however your loader collects it

licensex::AuthResult auth = client.Authenticate(key, hwid);

if (!auth.ok) {
    switch (auth.error) {
        case licensex::ErrorCode::InvalidCredentials:
            // app_id/api_secret rejected outright — server sends this
            // UNSIGNED (see "Signature verification" below), so it never
            // goes through the HMAC check at all.
            ShowError("This build is misconfigured. Contact support.");
            break;
        case licensex::ErrorCode::ServerRejected:
            // Verified "no" from the server — key is invalid/expired/banned
            ShowError("Invalid license key.");
            break;
        case licensex::ErrorCode::SignatureInvalid:
            // Response claimed success/failure but the HMAC didn't match —
            // tampered in transit, or api_secret mismatch. Never trust
            // auth.ok in this case, whatever the body said.
            ShowError("Could not verify server response.");
            break;
        case licensex::ErrorCode::RateLimited:
            ShowError("Too many attempts. Try again shortly.");
            break;
        default:
            // NetworkError, Timeout, etc.
            ShowError("Could not reach license server.");
            break;
    }
    return false;
}

// auth.license.status    → "active" | "expired" | "banned" | "unused"
// auth.license.expiresAt → std::optional<std::string> (nullopt = lifetime)
// That's the whole shape the server returns here — there's no per-license
// "note" field in the response, even though the dashboard lets you attach
// one to a license for your own reference.
UnlockApp();

Hardware ID

GetHardwareId() derives a stable fingerprint from the CPU, machine GUID, and system drive serial. You pass it into Authenticate() — you're in control of when it's collected.

loader.cpp
// What GetHardwareId() actually reads (stable across reboots):
//   • CPU vendor + model string via CPUID
//   • MachineGuid from HKLM\SOFTWARE\Microsoft\Cryptography
//   • Volume serial number of the system drive
//
// The three values are concatenated and SHA-256'd into a fixed string.
// You never need to call this yourself for auth — Authenticate() takes
// it as a parameter so you control when/how you collect it.
std::string hwid = licensex::GetHardwareId();

Resetting a device

Let a user clear their own device slots after a reformat or hardware upgrade, without you touching the dashboard. Capped per-license by a limit the app owner sets — this isn't a way around max devices.

loader.cpp
// Self-service HWID reset — for when a user reformats or upgrades their
// PC and Authenticate() now fails with DeviceLimitReached. Posts
// { app_id, api_secret, license_key } to /api/v1/reset-hwid, which clears
// every device slot on that license so the next Authenticate() call
// re-registers this machine as if it were new.
//
// Bounded server-side by a per-app "max resets" setting the app owner
// controls from the dashboard — this is NOT unlimited, so don't let a
// user spam it hoping to dodge the device limit.
licensex::ResetResult reset = client.ResetHwid(key);

if (!reset.ok) {
    switch (reset.error) {
        case licensex::ErrorCode::ResetLimitReached:
            // reset.resetsUsed / reset.resetsAllowed tell you exactly
            // where they stand — show that, not just a flat refusal.
            ShowError("Reset limit reached (" +
                std::to_string(reset.resetsUsed) + "/" +
                std::to_string(reset.resetsAllowed) +
                "). Contact support.");
            break;
        case licensex::ErrorCode::ServerRejected:
            ShowError("License is banned, revoked, or invalid.");
            break;
        default:
            ShowError("Could not reach license server.");
            break;
    }
    return false;
}

// Devices are cleared — call Authenticate() again to re-register this
// machine on the license.
RetryAuthenticate();

Remote variables

Pull server-controlled values at runtime without recompiling. Minimum client version, feature flags, download URLs — change them from the dashboard instantly.

loader.cpp
// Pull a server-controlled value at runtime — no recompile needed.
// Use this for things like minimum client version, feature flags,
// download URLs, or anything you want to change from the dashboard.
//
// Under the hood this posts { app_id, api_secret, name } to /api/v1/variable
// — same credentials as Authenticate(), just a different endpoint — and
// the response is signed the same way, so GetVariable() verifies it
// before ok is ever true.
licensex::VariableResult ver = client.GetVariable("min_client_version");

if (!ver.ok) {
    // NotFound: no variable with that name exists for this app.
    // SignatureInvalid / RateLimited / NetworkError: same meaning as
    // in Authenticate() above.
    return true; // fail open — don't block the user over a missing flag
}

if (IsOutdated(ver.value)) {
    ShowError("Update required: " + ver.value);
    return false;
}

Client logging

Fire lightweight events to the dashboard logs. Tie each entry to a license key so you can see exactly what's happening per-user.

loader.cpp
// Send a lightweight event to the dashboard logs.
// licenseKey ties the log entry to the right license in your app.
client.Log("loader started", licenseKey);
client.Log("feature_x accessed", licenseKey);

Error codes

Only ServerRejected after a clean signature check means the key is genuinely bad. InvalidCredentials is the one exception that arrives unsigned by design — treat it as a config problem in your build, not a bad key. Everything else (SignatureInvalid, timeouts, network errors) means the check couldn't complete at all — don't treat those as a real auth failure.

CodeWhen it fires
InvalidCredentialsapp_id/api_secret rejected outright. Sent unsigned — see "Signature verification" below — so it never reaches the HMAC check.
ServerRejectedServer returned success:false after a verified signature — the key is genuinely invalid, expired, revoked, or banned.
SignatureInvalidHMAC check failed on a response that was supposed to be signed — response was tampered with in transit, or api_secret doesn't match the dashboard. Never trust the body's success/ok value when this fires.
NotFoundReturned by GetVariable() when no variable with that name exists for the app. Not an error worth blocking on — decide a sensible fallback.
ResetLimitReachedReturned by ResetHwid() once the license has used its allotted self-service resets. resetsUsed/resetsAllowed on the result tell you exactly where it stands.
RateLimitedToo many requests in a short window — limits are per-IP, per-license-key, and per-app, and apply to every /api/v1/* endpoint. Back off and retry.
NetworkErrorHTTP request failed before a response arrived.
TimeoutServer didn't respond within the deadline.
ParseErrorResponse body wasn't valid JSON — usually a proxy or server error page.

Both SDKs target Windows only at runtime. The C++ SDK requires C++17 and links against winhttp.lib and bcrypt.lib; it has been verified to compile cleanly with MinGW-w64. The C# SDK requires .NET 8 and has been verified to build cleanly with the .NET 8 SDK — retargeting to .NET Framework or netstandard2.0 mainly means swapping the HTTP and P/Invoke calls for their older equivalents. Neither SDK has been exercised against a live LicenseX deployment end-to-end; test against your own deployment before shipping.