rymga ← Back to Licenses

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 flag

The 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:

MemberMeaning
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"])
C#, Go and the rest: same three steps, and RSA verification is in their standard library (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: true without 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 if doesn't unlock everything.