---
url: https://docs.sunlicense.com/how-to/python-cli.md
description: >-
  How to add license keys to a Python command-line tool - an activate command, a
  check with the SunLicense Python SDK cached for a day so commands stay fast, a
  clear exit on a rejection, offline use, and packaging with PyInstaller or
  Nuitka.
---

# 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](https://sunlicense.com/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](https://sunlicense.com/start?ref=docs-howto-python-cli).

## 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](/guide/sdks#choosing-a-machine-id).
6. **Results:** 200 runs the command (and refreshes the cache), 400 exits with the
   [reason](/guide/concepts#validation-result-codes) and where to fix it, anything else runs. For tools used fully
   offline, hand out a signed [offline license file](/guide/offline-licenses).
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](/guide/obfuscation) first (python-minifier and more), and keep a piece of the tool behind the license with
[protection](/guide/protection) or [protected files](/guide/protected-files) (Standard) if it matters.

## Selling it

[Gumroad](/integrations/gumroad), [Lemon Squeezy](/integrations/lemon-squeezy), [Payhip](/integrations/payhip),
[Ko-fi](/integrations/ko-fi) and [Stripe](/integrations/stripe) issue a key per purchase and revoke it on a refund. More on
[licensing Python tools](https://sunlicense.com/for/python-tools/). Selling a Discord bot in Python? See
[discord.py](/how-to/discord-py-bot).
