rymga ← Volver a Licenses

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 }
CampoObligatorioNotas
licenseKeysíLa clave emitida para tu cliente.
productCodesíDebe coincidir con el producto al que pertenece la licencia.
serverIdsíHuella estable de la máquina; determina el límite de servidores.
labelnoEtiqueta libre que aparece junto a la activación en el panel.
noncesíÚnico para cada solicitud (un UUID). Las repeticiones se rechazan durante un intervalo breve.
timestampsí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[:])
Elige datos duraderos. Cualquier dato que cambie en cada inicio (un UUID aleatorio o un PID) consumirá una nueva plaza de servidor en cada ejecución. Guarda la huella o derívala de hardware estable.

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_FOUNDNo existe esa clave para este tenant.
PRODUCT_MISMATCHLa clave no corresponde a este productCode.
LICENSE_SUSPENDEDDesactivada temporalmente.
LICENSE_REVOKEDRevocada permanentemente.
EXPIREDHa superado expiresAt.
SERVER_CAP_EXCEEDEDDemasiadas máquinas distintas.
IP_LIMIT_EXCEEDEDDemasiadas IP distintas para esta máquina.
LICENSE_/IP_/SERVER_/CUSTOMER_BLACKLISTEDIncluido en la lista negra para esa dimensión.
NONCE_REPLAYEl nonce ya se ha utilizado.
RATE_LIMITEDDemasiadas solicitudes.
TENANT_SUSPENDED / TENANT_NOT_FOUNDProblema a nivel del tenant.
SIGNATURE_INVALIDLo 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"]
No confíes todavía en el JSON. Un estado 200 con valid: true no significa nada hasta que verifiques X-Signature. Eso se explica en la página siguiente.