PyQt Licensing

Add license key activation and validation to your PyQt desktop application using the Keymint Python SDK.

Problem

You distribute a PyQt desktop app and need to prevent piracy. You need license keys that bind to the user's machine, a clean activation dialog, and a backend to manage keys. Building a licensing server, activation counting, and a management UI from scratch delays your launch by weeks.

Keymint provides the licensing API, a Python SDK with type hints, and a dashboard. You write the PyQt activation dialog and validation logic — everything else is handled.

Architecture Diagram

mermaid
Rendering diagram...

Create Product

From the Keymint Dashboard:

  1. Navigate to Products
  2. Enter your app name (e.g. "PhotoEditor Pro")
  3. Copy the product ID — you'll use it in every API call

Create License Policy

When generating a license key, save your settings as a Template (click "Save current configuration as a template"). This lets you re-use the same configuration.

SettingValue
licenseTypenode-locked
maxActivations1
formatXXXX-XXXX-XXXX-XXXX
expiryNone (perpetual)

Issue License

Install the Python SDK in your backend or admin scripts:

bash
pip install keymint

Generate keys from a Python script:

python
from keymint import KeyMint

admin = KeyMint(api_key="YOUR_ADMIN_API_KEY")

license = admin.create_key({
    "productId": "prod_Nx8K2mLpQ4rVtW9sBc",
    "maxActivations": 1,
    "licenseType": "node-locked",
    "newCustomer": {
        "name": "Jane Doe",
        "email": "jane@example.com"
    }
})

print(license["key"])  # "A8E2K-9F1BC-3D4GH-7J2KM"

The raw key value is returned at creation and encrypted at rest. You can retrieve it later via GET /key with an admin API key.

Validate License

Install the SDK in your PyQt project:

bash
pip install keymint

Create a license manager module:

python
import hashlib
import uuid
from PyQt6.QtCore import QSettings
from keymint import KeyMint
import os

PRODUCT_ID = os.environ.get("KEYMINT_PRODUCT_ID", "prod_Nx8K2mLpQ4rVtW9sBc")
CLIENT_API_KEY = os.environ.get("KEYMINT_CLIENT_API_KEY", "")

client = KeyMint(api_key=CLIENT_API_KEY)
settings = QSettings("YourCompany", "YourApp")


def get_host_id() -> str:
    """Generate a stable machine fingerprint."""
    raw = str(uuid.getnode()) + str(uuid.UUID(int=uuid.getnode()).hex)
    return hashlib.sha256(raw.encode()).hexdigest()[:16]


def get_stored_key() -> str | None:
    return settings.value("license/key")


def is_activated() -> bool:
    return settings.value("license/activated", False, type=bool)


def activate_license(license_key: str) -> dict:
    """Activate a license key. Returns {'success': bool, 'message': str}."""
    host_id = get_host_id()

    try:
        result = client.activate_key({
            "productId": PRODUCT_ID,
            "licenseKey": license_key,
            "hostId": host_id,
        })

        if result.get("code") == 0:
            settings.setValue("license/key", license_key)
            settings.setValue("license/activated", True)
            return {"success": True, "message": result.get("message", "License valid")}

        return {"success": False, "message": result.get("message", "Activation failed")}

    except Exception as e:
        return {"success": False, "message": str(e)}


def deactivate_license() -> bool:
    """Deactivate the current license, freeing a seat."""
    license_key = get_stored_key()
    if not license_key:
        return False

    host_id = get_host_id()

    try:
        result = client.deactivate_key({
            "productId": PRODUCT_ID,
            "licenseKey": license_key,
            "hostId": host_id,
        })

        if result.get("code") == 0:
            settings.remove("license/key")
            settings.remove("license/activated")
            return True

        return False

    except Exception:
        return False

Now gate your PyQt application entry point:

python
import sys
from PyQt6.QtWidgets import QApplication
from license_manager import get_stored_key, is_activated, activate_license
from activation_dialog import ActivationDialog
from main_window import MainWindow

def main():
    app = QApplication(sys.argv)

    if is_activated():
        # Token re-check on launch (validates license hasn't been revoked)
        stored_key = get_stored_key()
        if stored_key:
            result = activate_license(stored_key)
            if result["success"]:
                window = MainWindow()
                window.show()
                sys.exit(app.exec())
                return

    # Show activation dialog
    dialog = ActivationDialog()
    if dialog.exec() == ActivationDialog.DialogCode.Accepted:
        window = MainWindow()
        window.show()
        sys.exit(app.exec())

if __name__ == "__main__":
    main()

Handle Activations

Build a PyQt activation dialog:

python
from PyQt6.QtWidgets import (
    QDialog, QVBoxLayout, QLabel, QLineEdit,
    QPushButton, QMessageBox
)
from PyQt6.QtCore import Qt
from license_manager import activate_license, get_stored_key


class ActivationDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setWindowTitle("Activate License")
        self.setFixedSize(420, 220)
        self.setWindowFlags(
            self.windowFlags() & ~Qt.WindowType.WindowContextHelpButtonHint
        )

        layout = QVBoxLayout()
        layout.setSpacing(12)

        title = QLabel("Enter your license key to activate")
        title.setAlignment(Qt.AlignmentFlag.AlignCenter)
        layout.addWidget(title)

        self.key_input = QLineEdit()
        self.key_input.setPlaceholderText("A8E2K-9F1BC-3D4GH-7J2KM-XXXX")
        self.key_input.setMaxLength(64)
        layout.addWidget(self.key_input)

        # Pre-fill if a key was stored but deactivated
        stored = get_stored_key()
        if stored:
            self.key_input.setText(stored)

        self.activate_btn = QPushButton("Activate")
        self.activate_btn.clicked.connect(self._on_activate)
        layout.addWidget(self.activate_btn)

        self.status_label = QLabel("")
        self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        layout.addWidget(self.status_label)

        self.setLayout(layout)

    def _on_activate(self):
        key = self.key_input.text().strip()
        if not key:
            self.status_label.setText("Please enter a license key")
            return

        self.activate_btn.setEnabled(False)
        self.status_label.setText("Activating...")
        self.activate_btn.repaint()

        result = activate_license(key)

        if result["success"]:
            QMessageBox.information(self, "Success", "License activated successfully!")
            self.accept()
        else:
            self.status_label.setText(f"Failed: {result['message']}")
            self.activate_btn.setEnabled(True)

Handle Revocations

Block a license from the Dashboard or programmatically:

python
from keymint import KeyMint

admin = KeyMint(api_key="YOUR_ADMIN_API_KEY")

admin.block_key({
    "productId": "prod_Nx8K2mLpQ4rVtW9sBc",
    "licenseKey": "A8E2K-9F1BC-3D4GH-7J2KM",
})

When a license is blocked, subsequent activate_key() calls return code != 0. Your app should handle this gracefully:

python
def check_license_health() -> bool:
    """Periodically verify the license is still valid."""
    license_key = get_stored_key()
    if not license_key:
        return False

    host_id = get_host_id()

    try:
        result = client.activate_key({
            "productId": PRODUCT_ID,
            "licenseKey": license_key,
            "hostId": host_id,
        })

        valid = result.get("code") == 0
        if not valid:
            settings.setValue("license/activated", False)
        return valid

    except Exception:
        # Network error — err on the side of allowing continued use
        return is_activated()

Add a deactivation option in your app's preferences:

python
from PyQt6.QtWidgets import QPushButton, QMessageBox
from license_manager import deactivate_license

# Inside your preferences dialog:
deactivate_btn = QPushButton("Deactivate License")
deactivate_btn.clicked.connect(self._on_deactivate)

def _on_deactivate(self):
    reply = QMessageBox.question(
        self,
        "Deactivate License",
        "This will free your license seat for use on another machine. Continue?",
        QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
    )

    if reply == QMessageBox.StandardButton.Yes:
        success = deactivate_license()
        if success:
            QMessageBox.information(self, "Success", "License deactivated.")
            self.close()
        else:
            QMessageBox.warning(self, "Error", "Failed to deactivate license.")

Production Considerations

PyInstaller / Nuitka Bundling

When bundling with PyInstaller:

bash
pyinstaller --hidden-import=keymint --hidden-import=keymint._internal main.py

The keymint package must be discoverable. If you use --onefile, the SDK works out of the box.

Host ID Strategy

PyQt apps commonly run on Windows, macOS, and Linux. Choose a host ID strategy that works across platforms:

python
import platform
import hashlib
import uuid
import subprocess

def get_host_id() -> str:
    """Cross-platform stable machine fingerprint."""
    system = platform.system()

    if system == "Windows":
        output = subprocess.check_output("wmic csproduct get uuid", shell=True)
        raw = output.decode().split("\n")[1].strip()
    elif system == "Darwin":
        output = subprocess.check_output(
            "ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID",
            shell=True,
        )
        raw = output.decode().split('"')[-2]
    else:
        # Linux: use machine-id
        try:
            with open("/etc/machine-id") as f:
                raw = f.read().strip()
        except FileNotFoundError:
            raw = str(uuid.getnode())

    return hashlib.sha256(raw.encode()).hexdigest()[:16]

API Key Security

  • Store the client API key as an environment variable or bundled constant
  • Never bundle admin API keys in your PyQt app
  • Use client scope — it can only activate, deactivate, and manage floating sessions

Offline Verification

For air-gapped deployments, use Keymint's offline signing:

python
# Run from your backend/server (NOT bundled in the app)
admin = KeyMint(api_key="YOUR_ADMIN_API_KEY")

offline_license = admin.sign_offline_key({
    "productId": "prod_Nx8K2mLpQ4rVtW9sBc",
    "licenseKey": "A8E2K-9F1BC-3D4GH-7J2KM",
    "hostId": "machine-fingerprint",
    "ttl": 30 * 24 * 60 * 60,  # 30 days
})
# offline_license["file"] is a JSON string with the signed JWT

Then verify offline using the jose library. See the Offline Verification guide for details.

Offline licensing is available on all plans.

Complete Source Code

Full license_manager.py
python
"""
license_manager.py — Keymint license management for PyQt applications.
Drop this file into your PyQt project and call activate_license()
from your activation dialog.
"""
import hashlib
import platform
import subprocess
import uuid
import os
from PyQt6.QtCore import QSettings
from keymint import KeyMint

PRODUCT_ID = os.environ.get("KEYMINT_PRODUCT_ID", "")
CLIENT_API_KEY = os.environ.get("KEYMINT_CLIENT_API_KEY", "")

if not PRODUCT_ID or not CLIENT_API_KEY:
    raise RuntimeError(
        "KEYMINT_PRODUCT_ID and KEYMINT_CLIENT_API_KEY must be set"
    )

client = KeyMint(api_key=CLIENT_API_KEY)
settings = QSettings("YourCompany", "YourApp")


def get_host_id() -> str:
    system = platform.system()
    try:
        if system == "Windows":
            output = subprocess.check_output(
                "wmic csproduct get uuid", shell=True
            )
            raw = output.decode().split("\n")[1].strip()
        elif system == "Darwin":
            output = subprocess.check_output(
                "ioreg -rd1 -c IOPlatformExpertDevice | grep IOPlatformUUID",
                shell=True,
            )
            raw = output.decode().split('"')[-2]
        else:
            with open("/etc/machine-id") as f:
                raw = f.read().strip()
    except Exception:
        raw = str(uuid.getnode())

    return hashlib.sha256(raw.encode()).hexdigest()[:16]


def get_stored_key() -> str | None:
    val = settings.value("license/key")
    return val if val else None


def is_activated() -> bool:
    return settings.value("license/activated", False, type=bool)


def activate_license(license_key: str) -> dict:
    host_id = get_host_id()
    try:
        result = client.activate_key({
            "productId": PRODUCT_ID,
            "licenseKey": license_key,
            "hostId": host_id,
        })
        if result.get("code") == 0:
            settings.setValue("license/key", license_key)
            settings.setValue("license/activated", True)
            return {"success": True, "message": result.get("message", "")}
        return {"success": False, "message": result.get("message", "Activation failed")}
    except Exception as e:
        return {"success": False, "message": str(e)}


def deactivate_license() -> bool:
    license_key = get_stored_key()
    if not license_key:
        return False
    host_id = get_host_id()
    try:
        result = client.deactivate_key({
            "productId": PRODUCT_ID,
            "licenseKey": license_key,
            "hostId": host_id,
        })
        if result.get("code") == 0:
            settings.remove("license/key")
            settings.remove("license/activated")
            return True
        return False
    except Exception:
        return False