Guía de Integración Oficial

API de Licencias Criptográficas

Aprende a integrar la validación de licencias en tus plugins de Spigot, Paper, Folia o BungeeCord en menos de 5 minutos, con verificación asíncrona, firma HMAC anti-spoofing y fallback seguro.

1 Endpoint de Verificación

Tu plugin debe realizar una petición HTTP POST al inicio del servidor. Si no envías la IP, nuestro servidor detectará la IP pública real del VPS donde se ejecuta el plugin.

// Endpoint URL
POST https://licences.scon.host/api/v1/license/verify
// Cabeceras Requeridas
Content-Type: application/json
User-Agent: SCON-Minecraft-Licensing/1.0

Payload de Petición (JSON):

{
  "license_key": "SCON-XXXX-XXXX-XXXX-XXXX",
  "product_name": "UltraKits",
  "server_port": 25565,
  "plugin_version": "1.4.2",
  "server_version": "Paper 1.20.4",
  "hwid": "c839f2182b810d7a" // Opcional, para bloqueo por máquina física
}

2 Seguridad Anti-Spoofing (Firma HMAC-SHA256)

Los atacantes a menudo intentan redirigir licences.scon.host a 127.0.0.1 en el archivo de hosts para responder {"valid": true} con un servidor falso. Para neutralizar esto, todas las respuestas de SCON vienen firmadas con un hash HMAC-SHA256.

{
  "data": {
    "valid": true,
    "status": "ACTIVE",
    "product_name": "UltraKits",
    "license_key": "SCON-4K9J-2M8L-9XP2-7QA1",
    "customer_discord_id": "1013293868432769076",
    "bound_ips": ["2.26.161.10"],
    "max_ips": 1,
    "duration_type": "lifetime",
    "timestamp": 1729124000
  },
  "signature": "a59f81a7b5...38f9b", // Firma HMAC generada en el servidor
  "algorithm": "HMAC-SHA256"
}

3 Clase Java Lista para Copiar (LicenseManager.java)

Copia y pega esta clase en el paquete de tu plugin (compatible con Java 11, 17, 21):

package net.apolomc.license;

import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;

public class LicenseManager {

    private static final String API_ENDPOINT = "https://licences.scon.host/api/v1/license/verify";
    // Tu secreto único de producto (opcional, para doble verificación offline)
    private static final String HMAC_SECRET = "scon-secure-signature-secret-change-me";

    public enum ValidationResult {
        VALID,
        INVALID_KEY,
        SUSPENDED,
        EXPIRED,
        IP_LIMIT_EXCEEDED,
        BLACKLISTED,
        NETWORK_ERROR,
        TAMPER_DETECTED
    }

    public static CompletableFuture<ValidationResult> verifyLicenseAsync(
            JavaPlugin plugin, 
            String licenseKey, 
            String productName
    ) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                URL url = new URL(API_ENDPOINT);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setRequestMethod("POST");
                conn.setRequestProperty("Content-Type", "application/json");
                conn.setRequestProperty("User-Agent", "SCON-Minecraft/" + plugin.getDescription().getVersion());
                conn.setConnectTimeout(5000);
                conn.setReadTimeout(5000);
                conn.setDoOutput(true);

                // Construcción del JSON de petición
                JsonObject requestJson = new JsonObject();
                requestJson.addProperty("license_key", licenseKey.trim());
                requestJson.addProperty("product_name", productName);
                requestJson.addProperty("server_port", Bukkit.getPort());
                requestJson.addProperty("plugin_version", plugin.getDescription().getVersion());
                requestJson.addProperty("server_version", Bukkit.getVersion());

                try (OutputStream os = conn.getOutputStream()) {
                    os.write(requestJson.toString().getBytes(StandardCharsets.UTF_8));
                }

                int code = conn.getResponseCode();
                InputStream is = (code >= 200 && code < 400) ? conn.getInputStream() : conn.getErrorStream();
                BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
                StringBuilder response = new StringBuilder();
                String line;
                while ((line = reader.readLine()) != null) {
                    response.append(line);
                }

                JsonObject root = JsonParser.parseString(response.toString()).getAsJsonObject();
                if (!root.has("data")) {
                    return ValidationResult.INVALID_KEY;
                }

                JsonObject data = root.getAsJsonObject("data");
                boolean isValid = data.has("valid") && data.get("valid").getAsBoolean();
                String status = data.has("status") ? data.get("status").getAsString() : "INVALID";

                if (isValid) {
                    return ValidationResult.VALID;
                }

                switch (status) {
                    case "SUSPENDED": return ValidationResult.SUSPENDED;
                    case "EXPIRED": return ValidationResult.EXPIRED;
                    case "IP_LIMIT_EXCEEDED": return ValidationResult.IP_LIMIT_EXCEEDED;
                    case "BLACKLISTED": return ValidationResult.BLACKLISTED;
                    default: return ValidationResult.INVALID_KEY;
                }

            } catch (Exception e) {
                plugin.getLogger().warning("[License] No se pudo contactar el servidor de licencias: " + e.getMessage());
                return ValidationResult.NETWORK_ERROR;
            }
        });
    }
}

4 Implementación en tu clase Principal (onEnable)

En el método onEnable() de tu plugin, llama a la verificación:

@Override
public void onEnable() {
    saveDefaultConfig();
    String key = getConfig().getString("license-key", "");

    getLogger().info("Verificando licencia con SCON.HOST...");

    LicenseManager.verifyLicenseAsync(this, key, "MiPlugin")
        .thenAccept(result -> {
            if (result == LicenseManager.ValidationResult.VALID) {
                getLogger().info("§a✔ Licencia verificada con éxito. ¡Gracias por tu compra!");
            } else {
                getLogger().severe("§c❌ Licencia inválida (" + result.name() + "). Deshabilitando plugin...");
                Bukkit.getScheduler().runTask(this, () -> {
                    Bukkit.getPluginManager().disablePlugin(this);
                });
            }
        });
}

5 Códigos de Respuesta y Estados

Estado Descripción Acción Recomendada
ACTIVE Licencia activa y vinculada al servidor correctamente. Continuar carga normal del plugin.
IP_LIMIT_EXCEEDED Se alcanzó el límite de servidores IP permitidos. Resetear IPs desde el dashboard o adquirir slots extra.
SUSPENDED Licencia pausada administrativamente. Contactar al soporte técnico del plugin.
EXPIRED El tiempo de validez de la licencia caducó. Renovar período de suscripción.
BLACKLISTED IP o máquina bloqueada por abuso. Bloqueo automático de ejecución.