Skip to content
bdkshop

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

  1. You turn on the licence key on your listing and paste a few lines of code into your script.
  2. On every sale, BDKSHOP creates a key such as BDK-XXXX-XXXX-XXXX-XXXX, shown on the buyer’s order page.
  3. The buyer copies it into your product’s configuration (for example their server.cfg or config.yml).
  4. 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

  1. In My account › My listings, edit the listing, Anti-leak protection section.
  2. Choose Licence key and set the number of installations per licence (0 = unlimited, 50 at most).
  3. 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.

ParameterRequiredDescription
productyesThe listing’s product identifier (prd_…).
keyyesThe buyer’s key. Case and surrounding spaces do not matter.
identifieryesStable identifier of the server or installation (3 to 200 characters). It is never stored in clear, only its fingerprint.
labelnoInstallation name shown to the buyer and to you (80 characters at most).
noncenoRandom 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":"…"}
statusvalidMeaningWhat your script does
activetrueValid licence, installation recorded.Runs normally.
protection_disabledtrueYou turned the key off on the listing: every key is accepted.Runs normally.
invalid_keyfalseUnknown key for this product (typo, made-up key).Stops and shows the message.
revokedfalseRefunded order, or licence revoked after a leak.Stops.
expiredfalseProduct sold by subscription and the subscription has ended.Stops and invites the buyer to subscribe again.
activation_limitfalseKey already used on the maximum number of servers.Stops and invites the buyer to “Change server” from the order.
unknown_productfalseUnknown 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 message in 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.

Report a bug or an idea

Your message goes straight to the BDKSHOP team. For an order or a payment, open a dispute instead or contact us from the help page.

Type

We record the page concerned and your browser to reproduce the problem. Do not include passwords or bank details.