How to add license keys to a Python CLI tool
Short answer: pip install sunlicense, add an activate <key> command that saves the key in the user's config directory, and before each command check it with SunLicense, but at most once a day: cache the last good check. Exit with a clear message when SunLicense says no; carry on when it can't be reached.
import json, sys, time, urllib.error
from pathlib import Path
from sunlicense import SunLicense
STATE = Path.home() / ".config" / "mytool" / "license.json" # {"key": "...", "checked": 0}
client = SunLicense(base_url="https://api.sunlicense.com", token=API_KEY) # API key from your build
def require_license():
state = json.loads(STATE.read_text()) if STATE.exists() else {}
if not state.get("key"):
sys.exit("No license yet. Run: mytool activate YOUR-KEY")
if time.time() - state.get("checked", 0) < 24 * 3600:
return # checked in the last day
try:
r = client.validate(state["key"], host=machine_id())
except (urllib.error.URLError, TimeoutError):
return # offline: carry on, check next time
if r.status == 400:
sys.exit(f"License not valid: {(r.data or {}).get('code')}. See https://example.com/account")
STATE.write_text(json.dumps({**state, "checked": time.time()}))validate returns (valid, status, data); network failures raise.
Or let your AI agent do it: paste the setup prompt into Claude Code, Cursor or Codex in your project. It adds the check and proves it with a test license.
Free plan: 3 products, 100 licenses, no credit card. Create a free account.
Step by step
In the dashboard: a product, a policy (for example 2 machines) and an API key, which can only validate.
Install:
pip install sunlicense. It uses only the standard library (urllib), so it adds nothing to a frozen build.activatevalidates the key once (a 400 means a typo or a wrong key: say so) and writes it to the state file. Use the platform's config directory (platformdirs.user_config_dir("mytool")if you have it) and create it first.Your API key goes into the build you ship (a generated
_build.pyyour release script writes fromSUNLICENSE_API_KEY), never the repository.Machine id: hash something stable and send it as
host:pythonimport hashlib, platform, socket def machine_id(): return hashlib.sha256(f"{platform.system()}|{platform.machine()}|{socket.gethostname()}".encode()).hexdigest()In CI or Docker the host name changes per run; offer a
MYTOOL_MACHINE_IDoverride for those buyers. Choosing a machine id.Results: 200 runs the command (and refreshes the cache), 400 exits with the reason and where to fix it, anything else runs. For tools used fully offline, hand out a signed offline license file.
Exit codes matter in scripts: exit non-zero on a rejection so a buyer's CI fails loudly instead of silently.
Packaging
PyInstaller and Nuitka make a single executable; Python source inside it can still be recovered. Run it through obfuscation first (python-minifier and more), and keep a piece of the tool behind the license with protection or protected files (Standard) if it matters.
Selling it
Gumroad, Lemon Squeezy, Payhip, Ko-fi and Stripe issue a key per purchase and revoke it on a refund. More on licensing Python tools. Selling a Discord bot in Python? See discord.py.
