Skip to main content

Go SDK

licensr-go wraps the licensing API: validation, seat/domain activation, offline EdDSA token verification, and hosted checkout. It also covers what a desktop tool needs on top: a hardware ID for seat activation, and a token store that saves to disk so your app can unlock with no network. Go 1.22+, no third-party dependencies: offline verification uses the standard library's crypto/ed25519.

Install​

go get github.com/tekunodev/licensr-go@latest

Quickstart​

import licensr "github.com/tekunodev/licensr-go"

client := licensr.NewClient(licensr.Config{
APIKey: "pk_live_...", // safe to embed in a distributed build — see the embedded-key doctrine below
PluginSlug: "my-plugin",
})

result, err := client.Validate(ctx, licensr.ValidateRequest{LicenseKey: userEnteredKey})
switch {
case err != nil:
// Licensr could not answer (offline, rate limited, server error): keep the previous state.
case result.Valid:
unlockFullFeatures()
case result.Entitlement == licensr.EntitlementLimited:
unlockDegradedMode() // perpetual-fallback: expired, but the plan grants a limited tier
}

The result keeps what you act on at the top level: Valid, Status (why, for your message), Entitlement and FeatureFlags. Plan and limit details are grouped under result.License (PlanID, ActivationMode, MaxSeats, MaxDomains, SeatsInUse, ExpiresAt). They are for display only; never use them to decide whether to unlock.

See embedded-key doctrine. Every method takes a context.Context, blocks until it has an answer, and is safe to call from many goroutines at once.

API​

Every method returns an error on failure — see Errors.

MethodWrapsNotes
client.Validate(ctx, licensr.ValidateRequest{...})POST /v1/license/validateAlways read Valid/Entitlement; never branch on HTTP status — an unknown key returns 200 {valid: false}, not 404.
client.Token(ctx, licensr.ValidateRequest{...})POST /v1/license/tokenSame check as Validate, plus a short-lived signed offline token. See Offline verification.
client.Activate(ctx, licensr.ActivateRequest{...})POST /v1/license/activateActivationType is licensr.ActivationSeat (per-machine) or licensr.ActivationDomain, fixed by the product's mode.
client.Deactivate(ctx, licensr.DeactivateRequest{...})POST /v1/license/deactivateFrees a seat/domain slot.
client.Activations(ctx, licensr.ActivationsRequest{...})GET /v1/license/activationsLists every current activation.
client.Checkout(ctx, licensr.CheckoutRequest{...})POST /v1/billing/checkoutReturns CheckoutURL — direct the user there. Requires a key with the checkout scope.
client.LastValidated()(no request)The last successful Validate result, or nil. Never blocks — safe on a hot path.

With a test key (pk_test_…), set TestClock: true on CheckoutRequest to put the buyer on a Stripe test clock you can fast-forward to try renewals; the response returns its id as TestClockID.

Client options​

licensr.NewClient(licensr.Config{
APIKey: "pk_live_...",
PluginSlug: "my-plugin",
BaseURL: "https://api.licensr.app", // default; override for self-hosted/staging
DeviceID: hwid, // buckets rate limits per installation instead of per key — see Hardware IDs below
Origin: "app://my-plugin", // rarely needed — set client type Native in the admin UI
Timeout: 10 * time.Second, // per attempt; the ctx you pass still applies
Retry: &licensr.RetryOptions{MaxRetries: 2}, // network errors / 429 / 5xx, exponential backoff + jitter, honors Retry-After; unset delays keep their defaults (300ms base, 5s cap)
HTTPClient: myHTTPClient, // bring your own proxy, TLS settings or transport
})

Hooks​

licensr.Config{
// ...
Hooks: licensr.Hooks{
OnValidated: func(r *licensr.ValidateResponse) { log.Println("validated:", r.Valid) },
OnRetry: func(method string, attempt int, delay time.Duration) { log.Println("retrying", method, attempt, delay) },
OnError: func(method string, err error) { reportError(method, err) },
},
}

Available hooks: OnValidated, OnActivated, OnDeactivated, OnTokenIssued, OnRetry, OnError. Each runs on the goroutine that made the call.

Offline verification​

client.Token() mints an EdDSA-signed JWT whose claims mirror Validate()'s response. Verify it without asking Licensr (the first check downloads the product's public keys and keeps them for the rest of the process):

token, err := client.Token(ctx, licensr.ValidateRequest{LicenseKey: licenseKey})
// ...
claims, err := licensr.VerifyOfflineToken(ctx, token.Token, token.Details.JWKSURL,
&licensr.VerifyOptions{PluginSlug: "my-plugin"})
// claims.Valid, claims.Entitlement, claims.Exp, ...

Always set PluginSlug. Every product in your store is signed with the same key, so without it a token bought for one of your products would also unlock another.

Offline tokens carry no revocation signal — they prove the token was genuinely issued and hasn't expired, not that the license is still active right now. Re-validate online before Exp.

To unlock on a launch with no network at all, save the token together with the public keys (FetchJWKS) in a TokenStore, and check it against those saved keys next time:

store := licensr.NewFileTokenStore("my-plugin", "") // "" = the platform's app-data folder

// Online: save the token with its keys.
jwks, err := licensr.FetchJWKS(ctx, token.Details.JWKSURL, nil)
// ...
err = store.Set(licenseKey, licensr.CachedOfflineToken{Token: token.Token, JWKS: jwks, CachedAt: time.Now()})

// Next launch, no network needed:
if cached, _ := store.Get(licenseKey); cached != nil {
claims, err := licensr.VerifyOfflineToken(ctx, cached.Token, "",
&licensr.VerifyOptions{PluginSlug: "my-plugin", JWKS: cached.JWKS})
// ...
}

The store keeps no claims: anything on disk can be edited, so read them by checking the token again. NewFileTokenStore writes one file per license, readable only by the current user, under ~/Library/Application Support/Licensr/<slug>/ (macOS), %APPDATA%\Licensr\<slug>\ (Windows) or $XDG_DATA_HOME/licensr/<slug>/, falling back to ~/.local/share/licensr/<slug>/ (Linux) — the same folders as the C++ SDK. NewMemoryTokenStore keeps tokens only until the process exits. Implement the three-method TokenStore interface to use your own storage, such as an OS keychain.

Errors​

Use errors.As to tell them apart:

ErrorWhen
*licensr.APIErrorNon-2xx response. Has Status, Code (the documented error codes, e.g. licensr.CodeActivationCapExceeded), Message, and RetryAfter. Branch on Code, not Message.
*licensr.NetworkErrorThe request never got a response (network failure, timeout, cancelled context, retries exhausted). errors.Unwrap gives the cause.
*licensr.TokenVerificationErrorVerifyOfflineToken rejected the token (bad signature, wrong algorithm, another product, expired or not yet valid), or the public keys could not be downloaded. errors.Unwrap gives the cause, so errors.Is(err, context.DeadlineExceeded) means "offline", not "forged".
var apiErr *licensr.APIError
if errors.As(err, &apiErr) && apiErr.Code == licensr.CodeActivationCapExceeded {
showMessage("All seats are in use. Deactivate another computer first.")
}

Hardware IDs​

licensr.ComputeHardwareID(salt) gives a stable ID for this computer. Use it as both Config.DeviceID and the seat ActivateRequest.Identifier:

hwid, err := licensr.ComputeHardwareID("my-plugin")
if err != nil {
hwid = loadOrCreateSavedUUID() // rare: a locked-down sandbox or an unsupported OS
}

It reads the operating system's machine ID (IOPlatformUUID on macOS, MachineGuid plus the system drive's serial on Windows, /etc/machine-id on Linux) and returns a SHA-256 hash of it mixed with your salt, so the raw ID never leaves the process. It returns the same value as the C++ SDK's compute_hardware_id on the same computer, so customers keep their seat if your product moves between the two. Keep the salt the same forever: changing it makes every customer's computer look new.

Example​

examples/cli is a complete command-line integration: unlock from a saved token when there is one, otherwise validate, activate this computer, and save a fresh token.

Source & issues​

Source lives in github.com/tekunodev/licensr-go, mirrored from this monorepo's sdks/go/. File issues and PRs there. API reference: pkg.go.dev/github.com/tekunodev/licensr-go.