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
Create Product
From the Keymint Dashboard:
- Navigate to Products
- Enter your app name (e.g. "PhotoEditor Pro")
- 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.
| Setting | Value |
|---|---|
licenseType | node-locked |
maxActivations | 1 |
format | XXXX-XXXX-XXXX-XXXX |
expiry | None (perpetual) |
Issue License
Install the Python SDK in your backend or admin scripts:
pip install keymintGenerate keys from a Python script:
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:
pip install keymintCreate a license manager module:
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 FalseNow gate your PyQt application entry point:
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:
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:
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:
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:
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:
pyinstaller --hidden-import=keymint --hidden-import=keymint._internal main.pyThe 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:
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
clientAPI key as an environment variable or bundled constant - Never bundle
adminAPI keys in your PyQt app - Use
clientscope — it can only activate, deactivate, and manage floating sessions
Offline Verification
For air-gapped deployments, use Keymint's offline signing:
# 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 JWTThen 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
"""
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