API de validación
Un endpoint, una solicitud JSON y una respuesta JSON firmada. Ese es todo el contrato; los SDK son simples envoltorios para facilitar su uso. Solo define el transporte: nada de lo descrito depende de un lenguaje ni de un entorno de ejecución.
Endpoint
POST https://licenses.rymga.com/t/{slug}/api/v1/validate
Content-Type: application/json{slug} es el slug de tu tenant, disponible en el panel. Todo el tráfico usa HTTPS.
Solicitud
{
"licenseKey": "RYMGA-XXXX-XXXX-XXXX",
"productCode": "YOUR_PRODUCT",
"serverId": "stable-machine-fingerprint",
"label": "optional human-readable label",
"nonce": "unique-per-request-uuid",
"timestamp": 1733600000000
}| Campo | Obligatorio | Notas |
|---|---|---|
| licenseKey | sí | La clave emitida para tu cliente. |
| productCode | sí | Debe coincidir con el producto al que pertenece la licencia. |
| serverId | sí | Huella estable de la máquina; determina el límite de servidores. |
| label | no | Etiqueta libre que aparece junto a la activación en el panel. |
| nonce | sí | Único para cada solicitud (un UUID). Las repeticiones se rechazan durante un intervalo breve. |
| timestamp | sí | Milisegundos desde la época Unix. |
El serverId
El serverId puede ser cualquier cadena que sea estable para una máquina y razonablemente difícil de falsificar. Es lo que cuenta el límite de servidores: un identificador que cambia entre reinicios hace que una sola máquina consuma varias plazas y acabe alcanzando SERVER_CAP_EXCEEDED. En la JVM, usa
PersistentServerId.getOrCreate(dir): guarda un identificador aleatorio en tu carpeta de datos y lo reutiliza, por lo que no le afectan los cambios del nombre de host, la dirección MAC ni el contenedor. En otros lenguajes, guarda tu propio identificador durante la instalación o derívalo de un identificador duradero de la instalación o del hardware y aplica un hash. Evita datos volátiles como el nombre de host: en un contenedor corresponde a su identificador y cambia con cada reinicio.
String raw = System.getProperty("os.name")
+ "|" + InetAddress.getLocalHost().getHostName();
MessageDigest md = MessageDigest.getInstance("SHA-256");
String serverId = HexFormat.of()
.formatHex(md.digest(raw.getBytes(UTF_8)));
import { createHash } from "node:crypto";
import os from "node:os";
const raw = `${os.platform()}|${os.hostname()}`;
const serverId = createHash("sha256").update(raw).digest("hex");
import hashlib, platform, socket
raw = f"{platform.system()}|{socket.gethostname()}"
server_id = hashlib.sha256(raw.encode()).hexdigest()
using System.Security.Cryptography;
using System.Text;
var raw = $"{Environment.OSVersion.Platform}|{Environment.MachineName}";
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(raw));
var serverId = Convert.ToHexString(bytes).ToLowerInvariant();
raw := runtime.GOOS + "|" + must(os.Hostname())
sum := sha256.Sum256([]byte(raw))
serverId := hex.EncodeToString(sum[:])
Respuesta
El código de estado HTTP indica el resultado: 200 concedida, 4xx denegada; el cuerpo está siempre firmado (see Verificar la firma).
// Granted (200)
{
"valid": true,
"productCode": "YOUR_PRODUCT",
"tier": "PREMIUM",
"expiresAt": "2027-01-01T00:00:00Z",
"featureFlags": { "export": true, "api": true, "seats": 5 },
"nonce": "...",
"serverTime": "2026-08-09T00:00:00Z"
}
// Denied (403 / 404 / 400 / 429)
{ "valid": false, "reason": "SERVER_CAP_EXCEEDED", "nonce": "...", "serverTime": "..." }expiresAt— fecha UTC en formato ISO-8601; se omite en licencias perpetuas.featureFlags— un mapa arbitrario que defines por nivel o por licencia.nonce— repite el nonce de la solicitud para poder asociar la respuesta.
Motivos de denegación
Cuando valid es false, reason tiene uno de estos valores:
| LICENSE_NOT_FOUND | No existe esa clave para este tenant. |
| PRODUCT_MISMATCH | La clave no corresponde a este productCode. |
| LICENSE_SUSPENDED | Desactivada temporalmente. |
| LICENSE_REVOKED | Revocada permanentemente. |
| EXPIRED | Ha superado expiresAt. |
| SERVER_CAP_EXCEEDED | Demasiadas máquinas distintas. |
| IP_LIMIT_EXCEEDED | Demasiadas IP distintas para esta máquina. |
| LICENSE_/IP_/SERVER_/CUSTOMER_BLACKLISTED | Incluido en la lista negra para esa dimensión. |
| NONCE_REPLAY | El nonce ya se ha utilizado. |
| RATE_LIMITED | Demasiadas solicitudes. |
| TENANT_SUSPENDED / TENANT_NOT_FOUND | Problema a nivel del tenant. |
| SIGNATURE_INVALID | Lo establece tu cliente cuando no puede verificar la firma. |
Límites aplicados
- serverId → límite de servidores. Número de máquinas distintas en las que puede ejecutarse una licencia.
- IP por servidor → límite de IP. Número de IP distintas permitidas por máquina activada.
Ambos se configuran por licencia en el panel o se heredan de su nivel. Superar cualquiera de ellos deniega la solicitud con el motivo correspondiente de la lista anterior.
Una llamada directa
No necesitas un SDK para probarlo; sirve cualquier cliente HTTP:
curl -X POST https://licenses.rymga.com/t/acme/api/v1/validate \
-H "Content-Type: application/json" \
-d '{
"licenseKey": "RYMGA-7QX4-9F2K-M8A2",
"productCode": "MY_APP",
"serverId": "3f9a...c1",
"nonce": "0d1e2f3a-4b5c-6d7e-8f90-a1b2c3d4e5f6",
"timestamp": 1733600000000
}' -i # -i to also see X-Signature / X-Kid headers
const res = await fetch(
"https://licenses.rymga.com/t/acme/api/v1/validate",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
licenseKey: key, productCode: "MY_APP",
serverId, nonce: crypto.randomUUID(),
timestamp: Date.now(),
}),
},
);
const signature = res.headers.get("X-Signature");
const bodyBytes = new Uint8Array(await res.arrayBuffer());
// verify signature over bodyBytes, then JSON.parse — see next page
import json, time, uuid, urllib.request
body = json.dumps({
"licenseKey": key, "productCode": "MY_APP",
"serverId": server_id, "nonce": str(uuid.uuid4()),
"timestamp": int(time.time() * 1000),
}).encode()
req = urllib.request.Request(
"https://licenses.rymga.com/t/acme/api/v1/validate",
data=body, headers={"Content-Type": "application/json"})
resp = urllib.request.urlopen(req)
raw = resp.read() # verify over these exact bytes
signature = resp.headers["X-Signature"]
valid: true no significa nada hasta que verifiques X-Signature. Eso se explica en la página siguiente.