EYMINT
DocumentationAPI Reference
BlogDashboard
EYMINT
SectionsDocumentationAPI Reference

Getting Started

  • Keymint Documentation
  • Introduction
  • Getting Started
  • License Keys
  • Changelog

SDK References

  • Software Development Kits (SDKs)
  • Node.js SDK
  • Go SDK
  • Python SDK
  • C# SDK

Integration Guides

  • Integration Guides
  • Customer Portal for Vendors
  • Roles & Permissions
  • Radar Alert Destinations
  • Payment Provider Integrations
  • Stripe Subscription Licensing
  • Email Delivery
  • API Key Management
  • Webhook Deliveries
  • Analytics and Retention
  • Dashboard Workflows
  • Electron Licensing
  • PyQt Licensing
  • Tauri Licensing
  • Next.js SaaS Licensing

Licensing Guides

  • Licensing Concepts
  • Node-Locking & Machine Identifiers
  • Floating Licenses
  • Offline Verification
  • License Radar
  • Public Key Management and Rotation
  • Feature Flags
  • Webhooks & Events

Templates

  • Starter Templates
  • Electron License Starter
  • PyQt License Starter
  • Tauri License Starter
  • Next.js SaaS License Starter

Security & Trust

  • Security & Trust

Support

  • Frequently Asked Questions
  • Customer Portal
  • Using the Customer Portal
  • Workspace Settings and Roles

Legal

  • Privacy Policy
  • Subprocessors
  • Data Processing Agreement
  • Security Overview
  • Terms of Service
  • Service Level Agreement (SLA)
  • GDPR Compliance
Go to Dashboard

Getting Started

  • Keymint Documentation
  • Introduction
  • Getting Started
  • License Keys
  • Changelog

SDK References

  • Software Development Kits (SDKs)
  • Node.js SDK
  • Go SDK
  • Python SDK
  • C# SDK

Integration Guides

  • Integration Guides
  • Customer Portal for Vendors
  • Roles & Permissions
  • Radar Alert Destinations
  • Payment Provider Integrations
  • Stripe Subscription Licensing
  • Email Delivery
  • API Key Management
  • Webhook Deliveries
  • Analytics and Retention
  • Dashboard Workflows
  • Electron Licensing
  • PyQt Licensing
  • Tauri Licensing
  • Next.js SaaS Licensing

Licensing Guides

  • Licensing Concepts
  • Node-Locking & Machine Identifiers
  • Floating Licenses
  • Offline Verification
  • License Radar
  • Public Key Management and Rotation
  • Feature Flags
  • Webhooks & Events

Templates

  • Starter Templates
  • Electron License Starter
  • PyQt License Starter
  • Tauri License Starter
  • Next.js SaaS License Starter

Security & Trust

  • Security & Trust

Support

  • Frequently Asked Questions
  • Customer Portal
  • Using the Customer Portal
  • Workspace Settings and Roles

Legal

  • Privacy Policy
  • Subprocessors
  • Data Processing Agreement
  • Security Overview
  • Terms of Service
  • Service Level Agreement (SLA)
  • GDPR Compliance

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 (in More options, open Save 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": "YOUR_PRODUCT_ID",
    "maxActivations": "1",
    "licenseType": "node-locked",
    "newCustomer": {
        "name": "Jane Doe",
        "email": "jane@example.com"
    }
})

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

Save the raw key when it's returned — it's encrypted at rest, and the API can't look a key up without already knowing it. You can still view or export keys in the dashboard.

Validate License

Install the SDK in your PyQt project:

bash
pip install keymint

Create a license manager module. The SDK raises KeyMintApiError (with status, code and message) for every non-2xx response; treat 401, 402, 403 and 404 as a rejected license and anything else as a temporary failure. The Python SDK doesn't expose errorCode yet, so branch on status:

python
import os
from PyQt6.QtCore import QSettings
from keymint import KeyMint, KeyMintApiError
from keymint.identity import get_or_create_installation_id

PRODUCT_ID = os.environ.get("KEYMINT_PRODUCT_ID", "YOUR_PRODUCT_ID")
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:
    """Stable installation ID, persisted to ~/.keymint/installation-id."""
    return get_or_create_installation_id()


def is_rejected(error: KeyMintApiError) -> bool:
    """401 = invalid, expired, blocked or wrong device; 402 = vendor
    subscription lapsed; 403 = activation limit reached; 404 = product
    deactivated or removed. Anything else (no network, 429, 5xx) is
    transient."""
    return error.status in (401, 402, 403, 404)


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


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


def activate_license(license_key: str) -> dict:
    """Returns {'success': bool, 'rejected': bool, 'message': str}.

    The SDK raises KeyMintApiError for every non-2xx response, so a returned
    result always means success.
    """
    try:
        result = client.activate_key({
            "productId": PRODUCT_ID,
            "licenseKey": license_key,
            "hostId": get_host_id(),
        })
    except KeyMintApiError as e:
        if is_rejected(e):
            return {"success": False, "rejected": True, "message": e.message}
        return {"success": False, "rejected": False,
                "message": "Could not reach the licensing server. Try again later."}

    settings.setValue("license/key", license_key)
    settings.setValue("license/activated", True)
    return {"success": True, "rejected": False, "message": result.get("message", "License valid")}


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

    try:
        client.deactivate_key({
            "productId": PRODUCT_ID,
            "licenseKey": license_key,
            "hostId": get_host_id(),
        })
    except KeyMintApiError:
        return False

    settings.remove("license/key")
    settings.remove("license/activated")
    return True

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, settings
from activation_dialog import ActivationDialog
from main_window import MainWindow

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

    stored_key = get_stored_key()
    if stored_key and is_activated():
        # Re-check on launch: catches revoked or expired licenses.
        # Re-activating on the same device doesn't use another seat.
        result = activate_license(stored_key)
        if result["success"] or not result["rejected"]:
            # Valid, or the server couldn't be reached: keep working offline.
            window = MainWindow()
            window.show()
            sys.exit(app.exec())
        settings.setValue("license/activated", False)

    # 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": "YOUR_PRODUCT_ID",
    "licenseKey": "A8E2K-9F1BC-3D4GH-7J2KM",
})

When a license is blocked, the next activate_key() call raises KeyMintApiError with status 401 and code 1 (expired and unknown keys return the same response; the error body's errorCode is license_invalid_or_forbidden). Branch on status, not on the message text, which may be reworded. Re-check periodically, and only revoke local access when the server rejects the license:

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

    try:
        client.activate_key({
            "productId": PRODUCT_ID,
            "licenseKey": license_key,
            "hostId": get_host_id(),
        })
        return True
    except KeyMintApiError as e:
        if is_rejected(e):
            settings.setValue("license/activated", False)
            return False
        # Network error, 429 or 5xx: keep the cached state
        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 main.py

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

Host ID Strategy

Use the SDK's get_or_create_installation_id() (shown above). On first run it combines a random ID with a hardware anchor (the BIOS/hardware UUID, then the OS machine ID, then the primary MAC address, skipping placeholder values from cloned VMs), persists it to ~/.keymint/installation-id, and returns its SHA-256 hash. It stays stable across restarts and updates; an OS reinstall or a wiped home directory produces a new ID, which uses a new seat.

python
from keymint.identity import get_or_create_installation_id

host_id = get_or_create_installation_id()  # 64-character hex string

If you roll your own, avoid wmic — it isn't installed by default on current Windows 11 builds. Read HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid with winreg instead.

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). Signing also
# activates hostId on the license, so it uses a seat.
admin = KeyMint(api_key="YOUR_ADMIN_API_KEY")

offline_license = admin.sign_key({
    "productId": "YOUR_PRODUCT_ID",
    "licenseKey": "A8E2K-9F1BC-3D4GH-7J2KM",
    "hostId": "HOST_ID_FROM_THE_MACHINE",  # get_or_create_installation_id() on that machine
    "ttl": 30 * 24 * 60 * 60,  # 30 days
})
# offline_license["file"] is the .lic file contents (JSON with the signed JWT)

Then verify it in the app with the SDK's verify_offline_license() (from keymint), or with an EdDSA-capable JWT library such as pip install "pyjwt[crypto]". See the Offline Verification guide for the file format and checks.

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 os
from PyQt6.QtCore import QSettings
from keymint import KeyMint, KeyMintApiError
from keymint.identity import get_or_create_installation_id

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:
    return get_or_create_installation_id()


def is_rejected(error: KeyMintApiError) -> bool:
    # 401 invalid/expired/blocked/wrong device, 402 vendor subscription
    # lapsed, 403 activation limit reached, 404 product deactivated or
    # removed. Anything else is transient.
    return error.status in (401, 402, 403, 404)


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


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


def activate_license(license_key: str) -> dict:
    try:
        result = client.activate_key({
            "productId": PRODUCT_ID,
            "licenseKey": license_key,
            "hostId": get_host_id(),
        })
    except KeyMintApiError as e:
        if is_rejected(e):
            return {"success": False, "rejected": True, "message": e.message}
        return {"success": False, "rejected": False,
                "message": "Could not reach the licensing server. Try again later."}

    settings.setValue("license/key", license_key)
    settings.setValue("license/activated", True)
    return {"success": True, "rejected": False, "message": result.get("message", "")}


def check_license_health() -> bool:
    license_key = get_stored_key()
    if not license_key:
        return False
    result = activate_license(license_key)
    if result["rejected"]:
        settings.setValue("license/activated", False)
        return False
    return result["success"] or is_activated()


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

On this page

  • Problem
  • Architecture Diagram
  • Create Product
  • Create License Policy
  • Issue License
  • Validate License
  • Handle Activations
  • Handle Revocations
  • Production Considerations
  • PyInstaller / Nuitka Bundling
  • Host ID Strategy
  • API Key Security
  • Offline Verification
  • Complete Source Code