Hexalon Studios
Back to panel

Integrating license checks into a plugin

One reference page you can follow every time you wire up license checking in a new plugin.

1. Verify endpoint

Call this from your plugin whenever you need to check a license:

GET https://your-domain.com/api/license/{licenseKey}/verify?challenge={timestamp}
ParamRequiredDescription
challengeRecommendedCurrent time in milliseconds. Signed back by the server so you can verify the response is authentic.
hwidIf HWID limit is setA stable identifier for the machine, such as a hash of hardware info.
productIdRecommendedThe product ID your plugin belongs to. If the license isn't assigned to this product, verification fails with PRODUCT_MISMATCH. This binds the key to a specific product instead of just checking that the key string exists.
metadataOptionalAny string you want logged with this verification attempt.

2. Example request

curl "https://your-domain.com/api/license/XXXX-XXXX-XXXX-XXXX-XXXX/verify?challenge=1735689600000&productId=1"

3. Response

{ "valid": true, "result": "VALID", "signedChallenge": "MEUCIQD...", "userId": 5 }

userId is the panel account the license belongs to, or null if unassigned. Only present when valid is true.

ResultMeaning
VALIDLicense is valid, use the software normally.
NOT_FOUNDNo license with this key exists.
PRODUCT_MISMATCHThe license exists but isn't assigned to the productId you sent.
NOT_ACTIVELicense was suspended by the admin.
EXPIREDPast its expiration date.
IP_LIMIT_EXCEEDEDToo many different IPs in the last 12 hours.
HWID_LIMIT_EXCEEDEDToo many different devices activated.
RATE_LIMIT_EXCEEDEDVerification requests happening too fast.

4. Verifying the signature

We recommend verifying the signature. Fetch the public key once from GET /api/license/public-key and bundle it with your plugin at build time. Then verify signedChallenge against the exact challenge string you sent. This proves the response really came from your server and wasn't forged by the end user.

Java

import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;

PublicKey loadPublicKey(String pem) throws Exception {
    String base64 = pem
        .replace("-----BEGIN PUBLIC KEY-----", "")
        .replace("-----END PUBLIC KEY-----", "")
        .replaceAll("\\s", "");
    byte[] encoded = Base64.getDecoder().decode(base64);
    return KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(encoded));
}

boolean verifySignature(String challenge, String signedChallengeBase64, PublicKey publicKey) throws Exception {
    Signature sig = Signature.getInstance("SHA256withRSA");
    sig.initVerify(publicKey);
    sig.update(challenge.getBytes("UTF-8"));
    return sig.verify(Base64.getDecoder().decode(signedChallengeBase64));
}

Node.js

const crypto = require("crypto");

function verifySignature(challenge, signedChallengeBase64, publicKeyPem) {
  return crypto.verify(
    "sha256",
    Buffer.from(challenge, "utf8"),
    publicKeyPem,
    Buffer.from(signedChallengeBase64, "base64")
  );
}

5. Security note

If your plugin can call your own backend, do the license check there. It's the most secure option. If the check has to happen purely on the client side with no backend of your own, the challenge-response signature above is what protects you against a modified client simply claiming "valid: true".