SDKs & examples
There is one official SDK — Java, zero-dependency — and it is a reference implementation of the validation contract. Every other language talks to the same endpoint directly: build the request, verify one RSA signature, read the JSON. The complete code for that is below, and it runs as-is.
The Java SDK
Download the SDK jar from your panel (JDK 17+, no transitive dependencies) and put it on your classpath. To use it as a Maven/Gradle dependency, install it to your local repository first:
mvn install:install-file \
-Dfile=rymga-licenses-sdk-1.0.0.jar \
-DgroupId=com.rymga -DartifactId=rymga-licenses-sdk \
-Dversion=1.0.0 -Dpackaging=jar<!-- then declare it in your pom.xml -->
<dependency>
<groupId>com.rymga</groupId>
<artifactId>rymga-licenses-sdk</artifactId>
<version>1.0.0</version>
</dependency>Configure it once with your tenant, product code and embedded public key, then validate:
// PUBLIC_KEY = your tenant public key (X.509 SubjectPublicKeyInfo, Base64), pinned as a constant.
LicenseClient client = LicenseClient.builder()
.baseUrl("https://licenses.rymga.com")
.tenantSlug("acme")
.productCode("MY_APP")
.publicKeyBase64(PUBLIC_KEY)
.build();
// Stable per-machine id, persisted in your data folder (survives restarts/containers).
String serverId = PersistentServerId.getOrCreate(dataFolder.toPath());
LicenseResult r = client.validate(licenseKey, serverId);
if (!r.valid()) { // granted AND signature verified
if (r.networkError()) { /* your policy: block, or a grace window */ }
return lockOut(r.reason());
}
String tier = r.tier(); // e.g. "PREMIUM"
Object seats = r.feature("seats"); // read a feature flagThe serverId drives the server cap, so it must be stable per machine —
an id that changes on each restart makes one machine eat several slots and eventually hit
SERVER_CAP_EXCEEDED. PersistentServerId.getOrCreate(dir) writes a
random id once to <dir>/.rymga-server-id and reuses it — immune to hostname/MAC
changes; pass a folder that survives restarts. ServerFingerprint.derive() is the
best-effort fallback (OS machine-id + MACs, excluding the volatile hostname).
The result object
The SDK returns a LicenseResult. The important members:
| Member | Meaning |
|---|---|
| valid() | True only if the backend granted the license and the signature verified. |
| signatureValid() | Whether the response signature checked out, on its own. |
| networkError() | True when the call couldn't reach the backend — for grace handling. |
| grace() | True when this verdict was re-served from the offline-grace cache (see below). |
| reason() | The deny reason when not valid (see the Validation API page). |
| tier() | The plan the license is on, e.g. PREMIUM. |
| expiresAt() | Expiry Instant, or null for a perpetual license. |
| featureFlags() / feature(k) | The flag map, or a single flag by key. |
| rawBody() | The exact response bytes, if you want them. |
Offline grace (optional). So a blip reaching the backend doesn't take down software your
customer already paid for, enable .offlineGrace(Duration.ofHours(72)) on the builder.
When the backend is unreachable, validate() re-serves the last granted,
signature-verified response within that window and sets grace() == true. It's opt-in
and bounded on purpose: during the window a revoked license keeps working until the client next
reaches the backend (which then evicts the cached grant), and grace never revives a license past its own
expiresAt. Keep the TTL as short as your uptime needs.
Any other language
No SDK needed — the contract is small. These examples are complete: they build the request,
verify the signature, and read the verdict. Copy, set your public key and serverId,
and they run.
import crypto from "node:crypto"; // Node 18+, no dependencies
const url = "https://licenses.rymga.com/t/acme/api/v1/validate";
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
licenseKey, productCode: "MY_APP", serverId,
nonce: crypto.randomUUID(), timestamp: Date.now(),
}),
});
const body = Buffer.from(await res.arrayBuffer()); // exact bytes
const ok = crypto.createVerify("RSA-SHA256")
.update(body)
.verify(PUBLIC_KEY_PEM, res.headers.get("X-Signature"), "base64");
if (!ok) throw new Error("SIGNATURE_INVALID");
const verdict = JSON.parse(body);
if (!verdict.valid) lockOut(verdict.reason);
else if (verdict.tier === "PREMIUM") enablePremium();
# pip install cryptography (stdlib has no RSA verifier)
import json, time, uuid, base64, urllib.request
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
url = "https://licenses.rymga.com/t/acme/api/v1/validate"
body = json.dumps({
"licenseKey": license_key, "productCode": "MY_APP", "serverId": server_id,
"nonce": str(uuid.uuid4()), "timestamp": int(time.time() * 1000),
}).encode()
res = urllib.request.urlopen(urllib.request.Request(
url, data=body, headers={"Content-Type": "application/json"}))
raw = res.read() # exact bytes
sig = base64.b64decode(res.headers["X-Signature"])
pub = serialization.load_pem_public_key(PUBLIC_KEY_PEM)
pub.verify(sig, raw, padding.PKCS1v15(), hashes.SHA256()) # raises on failure
verdict = json.loads(raw)
if not verdict["valid"]:
lock_out(verdict["reason"])
RSA.VerifyData / rsa.VerifyPKCS1v15). The
Signature page has the verify snippet for each.Offline & grace
What to do when the backend is unreachable is your call — the Java SDK exposes
networkError(), and a raw client sees the request throw:
- Fail closed — block until a check succeeds. Strictest; not friendly to offline users.
- Grace window — remember the last successful, signed verdict and its time, and keep running for, say, 72 hours before requiring a fresh check.
Whatever you choose, only ever cache a verdict whose signature you verified — never a raw
valid: true.
Hardening
- Validate periodically, not just at startup — every few hours while running.
- Pin the public key as a constant in the build; never fetch it at runtime over plain HTTP.
- Never trust
valid: truewithout a verified signature. - Obfuscate the check so it's hard to locate and strip. For JVM builds, Lock Master encrypts strings and control flow around your validation code, making the check far harder to patch out.
- Don't branch on one boolean. Spread the consequences of an invalid license so a single
patched
ifdoesn't unlock everything.