Creator guide
Protect a script with a licence
Every buyer gets their own key. On start-up, your script has BDKSHOP check it and stops if it is refused: a shared copy or a refunded order no longer works. Nothing to host on your side.
How it works
- You turn on the licence key on your listing and paste a few lines of code into your script.
- On every sale, BDKSHOP creates a key such as
BDK-XXXX-XXXX-XXXX-XXXX, shown on the buyer’s order page. - The buyer copies it into your product’s configuration (for example their
server.cfgorconfig.yml). - On start-up, your script sends the key and a server identifier to BDKSHOP, which answers whether the licence is valid.
The key works on a limited number of servers, which you choose. A buyer who moves server clicks “Change server” on their order, at most once a week; you can also reset a licence yourself.
1. Turn on protection for a listing
- In My account › My listings, edit the listing, Anti-leak protection section.
- Choose Licence key and set the number of installations per licence (0 = unlimited, 50 at most).
- Save: the Integration code and issued licences link appears. It gives the listing’s product identifier (
prd_…), the ready-to-copy code and the list of keys sold.
On top of the key, every downloaded .zip or .jar archive carries the order’s invisible fingerprint. If your creation leaks, the Anti-leak page finds the original order from the file; you then report the leak in one click and the team revokes the matching licence.
2. Add the check to your script
Replace prd_your_identifier with your listing’s product identifier. On your licences page, the same code is already filled in with the right identifier. Code comments are in French.
-- server.lua : vérification de la licence BDKSHOP au démarrage de la ressource.
-- Dans le server.cfg de l'acheteur : set bdkshop_licence "BDK-XXXX-XXXX-XXXX-XXXX"
local PRODUIT = 'prd_your_identifier'
CreateThread(function()
Wait(5000)
local cle = GetConvar('bdkshop_licence', '')
local serveur = GetConvar('web_baseUrl', '')
if serveur == '' then serveur = GetConvar('sv_hostname', 'serveur') end
local ressource = GetCurrentResourceName()
PerformHttpRequest('https://bdkshop.fr/api/v1/licenses/verify', function(code, body)
local ok, data = pcall(json.decode, body or '')
if code ~= 200 or not ok or type(data) ~= 'table' then
-- BDKSHOP injoignable : on n'arrête pas le serveur de l'acheteur pour autant.
print(('^3[%s] Vérification de licence impossible pour le moment (%s).^0'):format(ressource, tostring(code)))
return
end
if not data.valid then
print(('^1[%s] Licence refusée : %s^0'):format(ressource, data.message))
StopResource(ressource)
return
end
print(('^2[%s] Licence valide.^0'):format(ressource))
end, 'POST', json.encode({
product = PRODUIT, key = cle, identifier = serveur, label = GetConvar('sv_hostname', 'Serveur FiveM'),
}), { ['Content-Type'] = 'application/json', ['Accept'] = 'application/json' })
end)
// Plugin Paper / Spigot (Java 17+). Dans config.yml de l'acheteur : licence: "BDK-XXXX-XXXX-XXXX-XXXX"
private static final String PRODUIT = "prd_your_identifier";
@Override
public void onEnable() {
saveDefaultConfig();
verifierLicence();
}
private void verifierLicence() {
String cle = getConfig().getString("licence", "").replace("\"", "");
String json = "{\"product\":\"" + PRODUIT + "\",\"key\":\"" + cle + "\",\"identifier\":\"" + instance() + "\",\"label\":\"Serveur Minecraft\"}";
HttpRequest requete = HttpRequest.newBuilder(URI.create("https://bdkshop.fr/api/v1/licenses/verify"))
.header("Content-Type", "application/json").header("Accept", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json)).build();
HttpClient.newHttpClient().sendAsync(requete, HttpResponse.BodyHandlers.ofString()).thenAccept(reponse -> {
if (reponse.statusCode() != 200) {
getLogger().warning("Vérification de licence impossible pour le moment (" + reponse.statusCode() + ").");
return;
}
if (!reponse.body().contains("\"valid\":true")) {
getLogger().severe("Licence refusée : le plugin est désactivé.");
getServer().getScheduler().runTask(this, () -> getServer().getPluginManager().disablePlugin(this));
}
}).exceptionally(erreur -> {
getLogger().warning("BDKSHOP injoignable : " + erreur.getMessage());
return null;
});
}
/** Identifiant stable de l'installation, créé au premier démarrage. */
private String instance() {
File fichier = new File(getDataFolder(), ".instance");
try {
if (!fichier.exists()) {
getDataFolder().mkdirs();
Files.writeString(fichier.toPath(), UUID.randomUUID().toString());
}
return Files.readString(fichier.toPath()).trim();
} catch (IOException e) {
return "port-" + getServer().getPort();
}
}
// Node.js 18+ (bot Discord, back-end de site). Clé de l'acheteur dans la variable BDKSHOP_LICENCE.
const os = require('node:os');
const PRODUIT = 'prd_your_identifier';
async function verifierLicence() {
try {
const reponse = await fetch('https://bdkshop.fr/api/v1/licenses/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({
product: PRODUIT,
key: process.env.BDKSHOP_LICENCE ?? '',
identifier: os.hostname(),
label: 'Mon application',
}),
});
if (!reponse.ok) return console.warn(`Vérification de licence impossible pour le moment (${reponse.status}).`);
const data = await reponse.json();
if (!data.valid) {
console.error(`Licence refusée : ${data.message}`);
process.exit(1);
}
} catch (erreur) {
console.warn('BDKSHOP injoignable :', erreur.message);
}
}
verifierLicence();
curl -X POST https://bdkshop.fr/api/v1/licenses/verify \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"product":"prd_your_identifier","key":"BDK-XXXX-XXXX-XXXX-XXXX","identifier":"mon-serveur-1","nonce":"a1b2c3"}'
# Réponse
{"valid":true,"status":"active","message":"Licence valide.","licensee":"pseudo","activations":1,"max_activations":1,"product":"prd_your_identifier","nonce":"a1b2c3","timestamp":1760000000,"signature":"…"}
One golden rule: if BDKSHOP does not answer (network outage, HTTP code other than 200), let the script run. Your buyers must never be penalised by an outage. Only a "valid": false answer should stop the product.
3. API reference
POST https://bdkshop.fr/api/v1/licenses/verify, JSON body (Content-Type: application/json). No authentication: the product identifier and the key are enough.
| Parameter | Required | Description |
|---|---|---|
product | yes | The listing’s product identifier (prd_…). |
key | yes | The buyer’s key. Case and surrounding spaces do not matter. |
identifier | yes | Stable identifier of the server or installation (3 to 200 characters). It is never stored in clear, only its fingerprint. |
label | no | Installation name shown to the buyer and to you (80 characters at most). |
nonce | no | Random value returned as is, to check the signature (64 characters at most). |
The answer is always HTTP 200 with this content (except for a malformed request: 422, or too many requests: 429):
curl -X POST https://bdkshop.fr/api/v1/licenses/verify \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{"product":"prd_your_identifier","key":"BDK-XXXX-XXXX-XXXX-XXXX","identifier":"mon-serveur-1","nonce":"a1b2c3"}'
# Réponse
{"valid":true,"status":"active","message":"Licence valide.","licensee":"pseudo","activations":1,"max_activations":1,"product":"prd_your_identifier","nonce":"a1b2c3","timestamp":1760000000,"signature":"…"}
status | valid | Meaning | What your script does |
|---|---|---|---|
active | true | Valid licence, installation recorded. | Runs normally. |
protection_disabled | true | You turned the key off on the listing: every key is accepted. | Runs normally. |
invalid_key | false | Unknown key for this product (typo, made-up key). | Stops and shows the message. |
revoked | false | Refunded order, or licence revoked after a leak. | Stops. |
expired | false | Product sold by subscription and the subscription has ended. | Stops and invites the buyer to subscribe again. |
activation_limit | false | Key already used on the maximum number of servers. | Stops and invites the buyer to “Change server” from the order. |
unknown_product | false | Unknown product identifier (mistake in your code). | To fix before publishing. |
Limits: 60 checks per minute per IP address, and 20 per minute for the same key. One check on start-up, then every few hours at most, is plenty.
4. Check the signature (advanced)
A clever buyer could redirect BDKSHOP’s address to a fake server that always answers “valid”. To prevent this, every answer is signed: signature is the HMAC-SHA256 of the text valid|status|product|nonce|timestamp (valid is 1 or 0), computed with your listing’s secret. Send a random nonce, recompute the signature and check that the timestamp is recent.
// Node.js 18+ : vérification de la signature (protège contre une fausse réponse).
const crypto = require('node:crypto');
const PRODUIT = 'prd_your_identifier';
const SECRET = 'votre_secret'; // Mon compte › Anti-fuite › Licences de l'annonce › « Vérifier la signature des réponses »
async function verifierLicence(cle, identifiant) {
const nonce = crypto.randomBytes(16).toString('hex');
const reponse = await fetch('https://bdkshop.fr/api/v1/licenses/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
body: JSON.stringify({ product: PRODUIT, key: cle, identifier: identifiant, nonce }),
});
const data = await reponse.json();
const texte = [data.valid ? '1' : '0', data.status, data.product, data.nonce ?? '', String(data.timestamp)].join('|');
const attendue = crypto.createHmac('sha256', SECRET).update(texte).digest('hex');
const recente = Math.abs(Date.now() / 1000 - data.timestamp) < 300;
if (data.nonce !== nonce || data.signature !== attendue || !recente) {
throw new Error('Réponse de licence non authentique.');
}
return data.valid;
}
The secret is on the listing’s licences page, under “Verify the signature of responses”. Anyone who opens your script can read it: obfuscate the part that holds it, and generate a new one if you think it has leaked.
Good practice
- Read the key from the buyer’s configuration, never hard-coded.
- Use an identifier that does not change on every restart (server name, a file created on first launch), otherwise each start counts as a new installation.
- Explain in the listing’s description where the buyer pastes their key.
- Print the answer’s
messagein the console: it is clear for the buyer. - Never block the server on a network error.
Frequently asked questions
What happens on a refund? The licence is revoked automatically: the next check answers revoked.
And for a subscription? The key stays the same on every renewal. When the subscription ends, it answers expired after a few days’ grace.
I remove protection from my listing? The API then answers “valid” to every key: your buyers’ servers keep running.
My script is in Python, PHP or something else? Any technology that can send a JSON HTTP POST request works: use the raw request as a model.
A question that is not here? Use “Report a bug or an idea” at the bottom of the page, or contact us.