SDK integration
Java SDK integration
Add Keyright licensing to a Java app: add the com.delta1labs:keyright dependency, verify licenses offline, activate online, gate features on entitlements, and handle trials and air-gapped machines — failing closed by design.
This page is the Java client’s end-to-end version: from adding the dependency to a shipping JVM app that unlocks paid features against a real license key. The com.delta1labs:keyright artifact verifies the same license and lease formats as the .NET, Node.js, and Python SDKs, so a mixed-language product line can share one Keyright tenant and one public key.
The vendor setup — creating a product, getting your public key, defining tiers and entitlements, issuing keys — is language-agnostic and is covered once in Getting started and, in developer depth, in the .NET SDK integration guide. This page assumes you’ve done that and focuses on the Java client code.
The model in one paragraph
You issue license keys server-side in Keyright; each tenant signs them with an RSA private key that never leaves the server. Your app embeds only the matching public key and verifies licenses offline against it, so a license check needs no network. For seat enforcement and revocation the app also activates online: it exchanges a license key for a short-lived signed lease bound to the machine, then keeps working offline until that lease expires. Anything that doesn’t check out — bad signature, wrong product, expiry, revocation, a rolled-back clock — resolves to the Free edition rather than throwing. It fails closed.
Step 1 — Add the dependency
The SDK is published at com.delta1labs:keyright. It targets Java 17+, lives in the com.delta1labs.keyright package, and has no runtime dependencies (it uses only the JDK’s own java.net.http client and crypto).
Maven:
<dependency>
<groupId>com.delta1labs</groupId>
<artifactId>keyright</artifactId>
<version>1.1.4</version>
</dependency>
Gradle (Kotlin DSL):
implementation("com.delta1labs:keyright:1.1.4")
Step 2 — Initialize the client
Construct one KeyrightClient at startup with the static initialize factory. KeyrightOptions exposes fluent setters for the common fields; pass the product slug you issue keys for, the base64 public key from your dashboard’s Integration tab, and — for online activation — your service URL:
import com.delta1labs.keyright.KeyrightClient;
import com.delta1labs.keyright.KeyrightOptions;
public final class Licensing {
public static final KeyrightClient CLIENT = KeyrightClient.initialize(
new KeyrightOptions()
.product("acme-app") // must match the product slug you issue keys for
.publicKeyBase64("MIIBIjANBgkq...") // base64 SubjectPublicKeyInfo from the Integration tab
.serviceUrl("https://keyright.delta1labs.com")); // omit if you ship offline license files only
}
Only publicKeyBase64 is strictly required — initialize throws IllegalArgumentException if it’s missing. The public key is not a secret: it can only verify signatures, never mint them, so shipping it inside your JAR is safe.
The fluent setters cover the fields you normally touch: product, publicKeyBase64, serviceUrl, licenseString, licenseFilePath, envVarName (defaults to "KEYRIGHT_LICENSE"), configLicensePath, defaultLicensePath, leaseCachePath, revocationListJson, localStatePath, now, machineComponents, clockTamperToleranceHours, and transport. A few options are plain public fields with no fluent setter — set them directly on the options object:
KeyrightOptions opts = new KeyrightOptions()
.product("acme-app")
.publicKeyBase64("MIIBIjANBgkq...")
.serviceUrl("https://keyright.delta1labs.com");
// keep an old key valid during a rotation window (public field, no setter)
opts.additionalPublicKeysBase64.add("<previous public key>");
// loosen node-lock tolerance if you fingerprint volatile hardware (default 1)
opts.nodeLockTolerance = 2;
// ship a signed revocation list read from a file (public field, no setter)
opts.revocationListPath = "/opt/acme/revocations.json";
KeyrightClient client = KeyrightClient.initialize(opts);
The SDK resolves a license across several sources, highest precedence first: an explicit licenseString, then an explicit licenseFilePath, then the file named by the envVarName environment variable, then configLicensePath, then the OS app-data default (<LocalAppData>/Keyright/<product>/license.json), and finally the cached activation lease (.../lease.json). You usually set none of these paths — activation (Step 4) writes the lease cache for you.
Step 3 — Gate features offline
Call validate(). It resolves the best license from the sources above, verifies the RSA signature, product match, node-lock, expiry, the optional shipped revocation list, and trial/clock-tamper state — all offline, and it never throws. It returns a KeyrightClient.Result with two public fields: info (a LicenseInfo) and source (which source won, a KeyrightClient.Source):
import com.delta1labs.keyright.KeyrightClient;
import com.delta1labs.keyright.Edition;
import com.delta1labs.keyright.LicenseInfo;
KeyrightClient.Result result = client.validate();
LicenseInfo info = result.info;
if (info.isPaid()) { // true for any edition above Free
// unlock paid features
}
if (info.edition == Edition.ENTERPRISE) {
// unlock enterprise-only features
}
// diagnostics: which source resolved the license?
if (result.source == KeyrightClient.Source.LEASE) {
// running on a cached activation lease
}
When you only need the license and not the source, validateInfo() returns the LicenseInfo directly.
Note that on LicenseInfo the license data is exposed as public final fields, while anything computed is a method. The fields are status (a LicenseStatus), edition (an Edition), licensee, expiryUtc (an Instant, nullable), isTrial (a boolean), trialDays (an Integer, nullable), product, licenseId, entitlements (an EntitlementSet), and message. The derived helpers are methods: isValid() (status is VALID), isPaid() (edition is above FREE), daysRemaining() (an Integer, or null for a perpetual license), isExpiringSoon(int withinDays), kind() (TRIAL / SUBSCRIPTION / PERPETUAL), and statusBadge() (e.g. "Enterprise" or "Enterprise Trial").
Gate on entitlements, not just the edition
Gate individual features on entitlements so changing a tier’s template doesn’t mean shipping new code. EntitlementSet has two accessors — isEnabled(name) for flags and getLimit(name, fallback) for numeric limits — and both fail closed:
import com.delta1labs.keyright.EntitlementSet;
import com.delta1labs.keyright.LicenseInfo;
LicenseInfo info = client.validateInfo();
EntitlementSet ent = info.entitlements;
// Boolean flag — a missing or unrecognized value reads as disabled
if (ent.isEnabled("export")) {
showExportCommand();
}
// Numeric limit — pass the fail-closed fallback yourself.
// "unlimited" resolves to EntitlementSet.MAXLONG (Long.MAX_VALUE).
long maxProjects = ent.getLimit("max-projects", 1);
if (currentProjectCount >= maxProjects) {
promptToUpgrade();
}
isEnabled treats "true", "1", "yes", and "enabled" (case-insensitive) as on; anything else, including a missing name, is off. getLimit parses the value as a long, maps the literal "unlimited" to EntitlementSet.MAXLONG, and returns your fallback when the name is missing or unparseable. Combine an entitlement check with the edition when a feature is both gated and tier-specific — and read a validated LicenseInfo once, then reuse it, rather than re-validating per check:
LicenseInfo info = client.validateInfo();
boolean advancedExport =
info.edition == Edition.ENTERPRISE
&& info.entitlements.isEnabled("advanced-export");
The client also exposes one-liner convenience methods — client.isEnabled("export"), client.getLimit("max-projects", 1), client.entitlements(), and client.edition() — but each of those validates on the spot, so prefer a single validateInfo() when checking several things together.
To find out why a check failed (for a “Register” dialog or diagnostics), read info.status and the human-facing info.message.
Step 4 — Activate online with a license key
When a customer enters a key, call activate(key). It’s synchronous and posts the key plus a stable machine id to your service, which consumes a seat and returns a short-lived signed lease bound to that machine. The SDK verifies the lease against your embedded public key and caches it locally, so every later validate() succeeds with no network until the lease’s grace window elapses.
activate does not throw for the ordinary failure paths (bad key, seat limit, offline, revoked) — like validate(), it returns a fail-closed LicenseInfo. Inspect the result:
LicenseInfo info = client.activate(customerEnteredKey);
if (info.isValid() && info.isPaid()) {
// Activated. The lease is cached; the app now works offline until it expires.
showLicensedUi(info.statusBadge()); // e.g. "Enterprise" or "Enterprise Trial"
} else {
// Surface info.message; the app stays in Free mode.
showActivationError(info.message); // "All seats for this license are in use.", etc.
}
- Seats are enforced server-side. Activating more machines than the license allows returns a seat-limit result (status
NO_LICENSE, message “All seats for this license are in use.”) and no lease. Re-activating a machine that’s already bound is idempotent — no extra seat is consumed. - Offline grace. If the service is unreachable,
activatefalls back to any still-valid cached lease, so a brief outage doesn’t lock the user out. Only when there’s no valid cached lease does it return a fail-closed “Could not reach issuing service” result. - Revocation takes effect on the next refresh — the client drops to
Freewith statusREVOKED.
activate throws only for programmer errors: IllegalStateException if no serviceUrl was configured, or IllegalArgumentException for an empty key.
The machine id it sends is the same one you can surface for support:
import com.delta1labs.keyright.MachineFingerprint;
String machineId = MachineFingerprint.current().toBoundString(); // e.g. "KRM1:...:...:..."
Step 5 — Deactivate (release a seat)
To move a license to another machine, release this machine’s seat first. deactivate(key) posts to your service and returns the service’s status string ("ok" or "not_found"). On "ok" it deletes the locally cached lease, so subsequent offline checks correctly fall back to Free.
Unlike activation, deactivation has no offline fallback — releasing a seat is a server-side change, so if the service can’t be reached (or answers non-2xx or unreadably) it throws IllegalStateException:
try {
String status = client.deactivate(customerEnteredKey);
if ("ok".equals(status)) {
showMessage("This machine's seat has been released.");
} else {
showMessage("No active seat was found for this key on this machine.");
}
} catch (IllegalStateException ex) {
// service unreachable or returned an error — the seat was NOT released
showError("Couldn't reach the licensing service. Try again while online.");
}
Step 6 — Air-gapped machines (offline activation)
For a machine that can never reach the service, an operator signs an offline lease for its machine id (dashboard Licenses → offline lease, or POST /admin/licenses/{id}/offline-lease) and delivers the JSON by file. Get the target machine’s id with MachineFingerprint.current().toBoundString(), hand it to the operator, then import the returned lease.
importOfflineLease(json) verifies the lease against your public key(s), product, machine, and expiry — the same path validate() uses — and caches it locally so later checks succeed with no network. It throws if the lease is invalid: IllegalArgumentException for missing or non-JSON input, IllegalStateException if the signature, product, machine, or expiry don’t check out.
import java.nio.file.Files;
import java.nio.file.Path;
try {
String leaseJson = Files.readString(Path.of("acme.lease.json"));
LicenseInfo info = client.importOfflineLease(leaseJson);
showLicensedUi(info.statusBadge());
} catch (IllegalArgumentException | IllegalStateException ex) {
// wrong signature / product / machine, or expired
showError("That offline lease isn't valid for this machine: " + ex.getMessage());
}
Step 7 — Node-locking, trials & revocation
- Node-locking. The SDK derives a stable machine fingerprint from the OS machine GUID, hostname, and OS descriptor, and byte-compatible with the other SDKs. A small
nodeLockTolerance(default1) means a swapped NIC or disk doesn’t lock the user out. The activation request sendsMachineFingerprint.current().toBoundString()so seats are counted per device;MachineFingerprint.current().displayId()gives a short human-readableKR-…id for support. To control what identifies a machine (for example in a container or a test), setmachineComponentson the options to a list of{key, value}string pairs. - Trials. A trial key activates through the exact same
activatepath as a paid one. Show the state straight offLicenseInfo—info.isTrial,info.statusBadge()(e.g. “Enterprise Trial”),info.expiryUtc, andinfo.daysRemaining()for a “27 days left” badge. A trial with atrialDayscount and no fixed expiry is anchored to first activation on this machine and counted down locally; it supersedes seamlessly when the customer later activates a paid key. Full flow: Self-service free trials. - Revocation. Revoke a key from the dashboard (or
POST /admin/licenses/{id}/revoke); the client drops toFree(statusREVOKED) on the next lease refresh. For a purely offline app, ship a signed revocation list with your build viarevocationListJson(fluent setter) orrevocationListPath(public field) so it still honours revocations. - Key rotation. When you rotate the tenant signing key, ship a build with the new public key in
publicKeyBase64and the outgoing one added toadditionalPublicKeysBase64— licenses and leases signed by either keep validating through the transition. Drop the old key once every lease signed by it has expired.
Troubleshooting
- Activation says the key wasn’t recognized. The most common cause is a product mismatch: the key was issued for a different product (or tenant) than the
productslug your client is configured with. Confirm the slug in Step 2 matches the product you issued the key under. - Everything reads as Free. That’s the design — the SDK fails closed on any verification problem instead of throwing. Read
info.status(NO_LICENSE,MALFORMED,SIGNATURE_INVALID,EXPIRED,MACHINE_MISMATCH,REVOKED,CLOCK_TAMPERED) andinfo.messageto see which one. ASIGNATURE_INVALIDalmost always means the embedded public key doesn’t match the tenant that signed the key. - A time-limited license suddenly won’t validate. If the system clock is moved backward beyond
clockTamperToleranceHours(default 24h) on a trial or subscription, the SDK returns statusCLOCK_TAMPERED. It doesn’t stick — set the correct time and validation recovers on the next call. Perpetual licenses are never subject to this check. IllegalStateExceptionfromactivateordeactivate. These throw only for configuration/connectivity problems: noserviceUrlset, or (fordeactivate) the service being unreachable. Ordinary licensing outcomes never throw — inspect the returnedLicenseInfo/status string instead.
See also
- Getting started — the condensed vendor path and go-live checklist.
- .NET SDK integration — the same journey with the vendor setup in full developer depth.
- Activation & lease lifecycle — how activation, leases, offline grace, and refresh fit together.
- Security & leases — the signing model, what’s a secret and what isn’t, and lease verification.
- HTTP API reference — the full endpoint map behind these calls.
- Samples repository — runnable client examples.