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}
| Param | Required | Description |
|---|---|---|
challenge | Recommended | Current time in milliseconds. Signed back by the server so you can verify the response is authentic. |
hwid | If HWID limit is set | A stable identifier for the machine, such as a hash of hardware info. |
productId | Recommended | The 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. |
metadata | Optional | Any 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.
| Result | Meaning |
|---|---|
VALID | License is valid, use the software normally. |
NOT_FOUND | No license with this key exists. |
PRODUCT_MISMATCH | The license exists but isn't assigned to the productId you sent. |
NOT_ACTIVE | License was suspended by the admin. |
EXPIRED | Past its expiration date. |
IP_LIMIT_EXCEEDED | Too many different IPs in the last 12 hours. |
HWID_LIMIT_EXCEEDED | Too many different devices activated. |
RATE_LIMIT_EXCEEDED | Verification 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".