Skip to content

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.

python
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 ​

  1. In the dashboard: a product, a policy (for example 2 machines) and an API key, which can only validate.

  2. Install: pip install sunlicense. It uses only the standard library (urllib), so it adds nothing to a frozen build.

  3. activate validates 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.

  4. Your API key goes into the build you ship (a generated _build.py your release script writes from SUNLICENSE_API_KEY), never the repository.

  5. Machine id: hash something stable and send it as host:

    python
    import 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_ID override for those buyers. Choosing a machine id.

  6. 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.

  7. 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.

Send feedback

Found something broken, or have an idea? It goes straight to our board.

The first line becomes the title.
Images (optional, up to 5: paste, drop or capture)
Check each image before sending, and remove any that show something private.

We also receive this page's address and your browser version.

Ctrl Enter to send