Selling a plugin, a bot, a script or a desktop app, and not sure how license keys actually work? You're not alone. Most guides jump straight to code, and skip the part where you decide what a key should do.
This guide explains the whole system in plain words: what a key is, how it gets tied to a computer, how your app checks it (online and offline), how trials and expiry work, and what happens when you revoke one. It applies to any language and any store. At the end there's a short code example and the mistakes that hurt paying customers most.
What a license key actually is
A license key like K7QX-M2PD-9HWT-R4CN is just a random, hard-to-guess ID. On its own it means nothing. It points to a record on a license server, and that record is the truth:
- who bought it (the customer),
- what they bought (the product and its rules),
- its status: active, suspended or revoked,
- when it expires, if ever,
- which machines it's activated on.
That's the key difference from the old "serial number" approach, where a key was valid if it passed a math check inside the app. Those get cracked by keygens: once someone reverse-engineers the formula, they can print infinite valid keys. A random key backed by a server can't be generated, only shared, and sharing is something the server can see and stop.
The four pieces every licensing system has
| Piece | What it is | Example |
|---|---|---|
| Product | The thing you sell | "MyPlugin", "Acme Editor" |
| Policy | The rules one purchase gets | 2 machines, 365 days, subscription |
| License | One buyer's key, issued from a policy |
K7QX-…, active, expires 6 Nov 2027 |
| Machine | One computer or server the license runs on | a server id, a desktop's machine id |
The policy is where most of your decisions live. Change a policy's machine limit and every license on it follows, so you set the rules once instead of per buyer.
Activation and machine limits
A key that works anywhere gets shared. So your app sends two things with every check: the key, and a machine id, a stable ID for the computer or server it runs on (a random ID saved in the app's folder works well; hardware-based IDs change when someone swaps a disk).
The first time a new machine checks in, it activates: it takes one of the license's slots. With "2 machines", a buyer's laptop and desktop both work. A third computer is refused with a clear "too many machines" answer, and the buyer can free a slot (from a customer portal, a Discord bot, or by asking you) when they move to a new PC.
Game-server plugins often use an IP limit instead or as well: the key works from a limited number of network addresses, the classic "one server per purchase" rule.
Be generous. People reinstall, upgrade and move. A support ticket costs you more than one extra slot.
Checking a key: online first
Most apps check online: on startup, the app asks the license server "is this key valid on this machine?" and gets one of a handful of answers.
| Answer | Means | What your app should do |
|---|---|---|
| Valid | Active, not expired, machine allowed | Start normally |
| Expired | Past its end date | Offer a renewal |
| Revoked / Suspended | You switched it off | Stop, show a support contact |
| Too many machines | The slots are full | Ask the buyer to free one |
| Not found | No such key | Ask them to check for typos |
| No answer | Network down, server error, timeout | Keep running, try again later |
That last row is the one beginners get wrong. If your app stops whenever the license server can't be reached, every outage (yours, the buyer's ISP, a firewall) turns paying customers into angry ones. Some marketplaces, BuiltByBit for example, warn that a plugin which disables itself when a server is unreachable may be treated as malicious. Only stop on a clear "no".
Re-check every few hours while the app runs, too. A startup-only check never notices a key you revoked yesterday on a server that hasn't restarted in a month.
Here's the logic in JavaScript (every language looks the same):
import { SunLicense } from "sunlicense";
const client = new SunLicense({ baseUrl: "https://api.sunlicense.com", token: API_KEY });
async function checkLicense() {
try {
const result = await client.validate(licenseKey, { host: machineId });
if (result.valid) return "run";
if (result.status === 400) return "stop"; // a clear "no": revoked, expired, too many machines...
return "run"; // a server error is not a "no"
} catch {
return "run"; // unreachable: keep running, check again later
}
}
Offline license files
Some customers run with no internet at all. For them, a license server can export a signed license file: the license's details (key, expiry, machine limit), plus a signature made with your private key, usually Ed25519. Your app ships the matching public key and verifies the file without a network. Anyone can read the file, but changing a single character breaks the signature.
The trade-off is revocation: a file someone already has stays valid until it expires. So give files a short life (a month, say) and treat them as a fallback: check online first, and use the file only when the server can't be reached.
Trials, subscriptions and expiry
The policy's type decides how time works:
- Perpetual: pay once, works forever (usually with updates for a while).
- Subscription: valid until a date; renewing pushes the date forward. An expired key gets an "expired" answer, so your app can show "renew here" instead of a generic error.
- Trial: a short duration (7 or 14 days) and often one machine. When it ends, the same "expired" answer becomes your upgrade prompt.
Revoking, suspending and refunds
- Suspend is a pause: the key stops working until you reinstate it. Use it while you look into something.
- Revoke is permanent: for a leaked key, or a buyer who charged back.
- Refunds should revoke automatically. If your store sends refund webhooks (Tebex, Stripe, Lemon Squeezy and others do), connect them, or refunded buyers keep a free copy.
Because every buyer has their own key, revoking one never touches anyone else.
Where keys come from
Don't email keys by hand. Connect your store so a key is created at checkout and delivered to the buyer: by email, on a "redeem" page, or written straight into the download (BuiltByBit and Voxel Shop can do that for Minecraft plugins).
What license keys don't do
A key check is one if in your code, and anyone with a decompiler can delete it. Keys stop casual sharing and make leaks traceable; they don't stop a determined cracker. If that worries you, the next layers are obfuscation and sealing part of your code so it only runs with a valid license. I wrote those up in How to protect your plugin or app from piracy, step by step.
Build it or use a service
You can build all of this: a small API, a database of licenses, a signing key for offline files, and a dashboard to revoke from. Plenty of people do. If you'd rather not, I build SunLicense, which does the pieces above: policies with machine and IP limits, SDKs for Java, JavaScript, Python, .NET, PHP and Lua, signed offline files, store integrations that issue and revoke keys, and a Discord bot for buyers. The free plan covers 3 products and 100 licenses, with no credit card and no cut of your sales.
Writing a Minecraft plugin? The step-by-step version is How to add license keys to a Minecraft plugin in 10 minutes. For anything else, see how SunLicense works and set up your first product.
This article was written with the help of an AI assistant and reviewed by me before publishing.




Top comments (0)