← Back to Guides UnifiedLicensing
🐍

Python Integration Guide

Add license validation to any Python app — Flask, Django, FastAPI backends, or desktop tools built with Tkinter and PyQt. One SDK file, three lines of code, done.

Python 3.8+ Flask Django FastAPI Tkinter / PyQt

01Quick Start

Got a Python app and 30 seconds? Install two dependencies, drop in the SDK, validate a license:

terminal
pip install requests cryptography

Copy UnifiedLicensing.Python.v4.0-Complete.py into your project (details in the next section), then:

from UnifiedLicensing import UnifiedLicenseManager

manager = UnifiedLicenseManager(api_key='ul_live_YOUR_API_KEY',
                                product_key='your-product-key')

result = manager.validate('ABCD-EFGH-IJKL-MNOP')   # machine ID is auto-detected

if result['valid']:
    print(f"License OK — plan: {result['plan']}")
else:
    raise SystemExit(f"License check failed: {result['message']}")

That's genuinely it. If result['valid'] is True, the license is real, active, and hasn't hit its machine limit. If it's False, you lock the door.

Heads up

The rest of this guide goes deeper — framework wiring for Flask, Django, and FastAPI, desktop dialogs, trials, heartbeats — but nothing below is required. The snippet above is a complete integration on its own.

02Setup

Grab your keys

  1. Log in to your vendor dashboard.
  2. Open Settings → API Keys and copy your API key. It starts with ul_live_ (or ul_test_ while you're experimenting).
  3. Open Products, pick your product, and copy its Product Key.

Copy the SDK into your project

Download UnifiedLicensing.Python.v4.0-Complete.py from the dashboard and drop it somewhere sensible. For most projects that's right next to your entry point, or inside a package folder:

your-project/
├── app.py
├── UnifiedLicensing.py          # renamed from UnifiedLicensing.Python.v4.0-Complete.py
├── requirements.txt
└── .env
Why rename the file?

Python imports map to filenames. Renaming to UnifiedLicensing.py lets you write from UnifiedLicensing import UnifiedLicenseManager. Keep the original download around as your reference copy — when a new SDK version ships, replace the file and you're upgraded.

Install dependencies

The SDK has exactly two third-party dependencies. Pin them in your requirements file:

requirements.txt
requests>=2.28
cryptography>=41.0
Don't skip cryptography

Without the cryptography package the SDK can't verify response signatures, which means a modified local payload could unlock features. It's a one-line install — keep it.

03Initialize

The constructor takes two keyword arguments: your API key and the product key. One manager instance handles everything — validation, activation, trials, heartbeats. Create it once at startup and reuse it:

from UnifiedLicensing import UnifiedLicenseManager

manager = UnifiedLicenseManager(
    api_key='ul_live_YOUR_API_KEY',     # your account API key
    product_key='your-product-key'      # which product this app belongs to
)

Load credentials from the environment

Treat your API key like a database password. Keep it out of source control by loading it from environment variables — either via os.environ directly or a .env file with python-dotenv:

import os
from UnifiedLicensing import UnifiedLicenseManager

manager = UnifiedLicenseManager(
    api_key=os.environ['UL_API_KEY'],
    product_key=os.environ['UL_PRODUCT_KEY']
)
.env
UL_API_KEY=ul_live_YOUR_API_KEY
UL_PRODUCT_KEY=your-product-key
Desktop apps are different

Environment variables work great for servers, but a desktop app ships to machines you don't control. For Tkinter/PyQt apps it's acceptable to compile the key into the binary (PyInstaller/Nuitka) — the key is scoped to one product and every sensitive operation happens server-side anyway. Section 8 shows this pattern.

04Validate Licenses

Validation is the workhorse. You send a license key, the API tells you whether it's real, active, and allowed on this machine. Here's a full example with proper error handling and a seat-quota check:

import hashlib
import platform
import sys

from UnifiedLicensing import UnifiedLicenseManager

manager = UnifiedLicenseManager(api_key='ul_live_YOUR_API_KEY',
                                product_key='your-product-key')

# A stable ID for "this machine". Hostname + architecture works well.
def get_machine_id() -> str:
    raw = f"{platform.node()}|{platform.machine()}|{sys.platform}"
    return hashlib.sha256(raw.encode()).hexdigest()

license_key = input("Enter your license key: ").strip().upper()

try:
    result = manager.validate(license_key, machine_id=get_machine_id())

    if result['valid']:
        # Optional: warn when the seat quota is nearly full
        if result['quota_used'] >= result['quota_limit']:
            print(f"Note: license {license_key} has used all "
                  f"{result['quota_limit']} seats.")

        print(f"Welcome! Plan: {result['plan']}, "
              f"seats: {result['quota_used']}/{result['quota_limit']}, "
              f"renews: {result['expires_at']}")
    else:
        # The license exists but can't be used — show the human-readable reason
        sys.exit(f"Access denied: {result['message']}")

except ConnectionError as e:
    # Network error, timeout, DNS failure — the API couldn't be reached.
    # Fail closed for paid features, or fail open briefly if you prefer.
    print(f"License server unreachable: {e}")
    sys.exit("Could not verify your license right now. Try again shortly.")

What comes back

FieldTypeMeaning
validboolThe big one. True means usable on this machine.
statusstractive, expired, suspended, or revoked.
planstrThe plan name — handy for gating pro features.
quota_usedintMachines currently activated against this license.
quota_limitintTotal seats the customer bought.
expires_atstr | NoneISO-8601 expiry date (None for lifetime licenses).
messagestrHuman-friendly reason when valid is False.

Under the hood

Every call is a signed POST to the validation endpoint. Knowing the wire format helps when debugging with logs or a proxy:

request
POST https://api.unifiedlicensing.com/api/v1/validate-license
Content-Type: application/json

{
    "api_key": "ul_live_YOUR_API_KEY",
    "product_key": "your-product-key",
    "license_key": "ABCD-EFGH-IJKL-MNOP",
    "machine_id": "9f86d081884c7d65...",
    "platform": "windows"
}
Validate vs. activate

validate() is a read-only check — use it freely. activate() consumes a seat and binds the machine to the license, so call it once when the customer first enters their key. After that, stick to validate().

05Flask Integration

Flask users get the cleanest pattern: initialize once in create_app(), guard routes with a decorator, and never think about licensing again.

1. Create the decorator

licensing.py
import functools
import os

from flask import jsonify, request, session

from UnifiedLicensing import UnifiedLicenseManager

manager = UnifiedLicenseManager(
    api_key=os.environ['UL_API_KEY'],
    product_key=os.environ['UL_PRODUCT_KEY']
)

# Cache successful checks for 10 minutes to avoid an API round-trip
# on every request. Invalid results are always re-checked live.
_validation_cache = {}
CACHE_TTL = 600


def licensed(view):
    """Decorator: route only runs with a valid license in the session."""
    @functools.wraps(view)
    def wrapper(*args, **kwargs):
        license_key = session.get('license_key')
        if not license_key:
            return jsonify(error='No license activated'), 403

        import time
        cached = _validation_cache.get(license_key)
        if cached and time.time() - cached[0] < CACHE_TTL:
            return view(*args, **kwargs)

        try:
            result = manager.validate(
                license_key,
                machine_id=session.get('machine_id')
            )
        except ConnectionError:
            return jsonify(error='License service unreachable'), 503

        if not result['valid']:
            session.pop('license_key', None)   # force re-activation
            return jsonify(error=result['message']), 403

        _validation_cache[license_key] = (time.time(), result)
        return view(*args, **kwargs)
    return wrapper

2. Activation routes

import uuid
from flask import Flask, redirect, request, session, url_for

from licensing import manager, licensed


@app.route('/activate', methods=['GET', 'POST'])
def activate():
    if request.method == 'POST':
        key = request.form.get('license_key', '').strip().upper()

        try:
            result = manager.activate(key, machine_id=session.setdefault(
                'machine_id', str(uuid.uuid4())
            ))
        except ConnectionError:
            return render_template('activate.html',
                                   error='License server unreachable.')

        if result['valid']:
            session['license_key'] = key
            return redirect(url_for('dashboard'))
        return render_template('activate.html', error=result['message'])

    return render_template('activate.html')

3. Protect routes

@app.route('/dashboard')
@licensed
def dashboard():
    return render_template('dashboard.html')


@app.route('/api/export', methods=['POST'])
@licensed
def export_data():
    # Only reachable with a valid license
    return do_export()


@app.route('/logout')
def logout():
    session.clear()
    return redirect(url_for('activate'))
Per-user vs per-install

The example uses a per-browser machine_id stored in the session, which fits SaaS-style dashboards where each customer activates their own key. If your Flask app runs on a customer's own server (self-hosted), use a stable host fingerprint instead — see get_machine_id() in section 4.

06Django Integration

Django projects benefit most from middleware: one component validates on every request, and your views stay completely licensing-free.

1. Settings

settings.py
UNIFIEDLICENSING = {
    'API_KEY': os.environ['UL_API_KEY'],
    'PRODUCT_KEY': os.environ['UL_PRODUCT_KEY'],
}

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    # ... other default middleware ...
    'licensing.middleware.LicenseMiddleware',   # add last
]

2. Middleware

licensing/middleware.py
import time

from django.conf import settings
from django.shortcuts import redirect

from UnifiedLicensing import UnifiedLicenseManager

manager = UnifiedLicenseManager(
    api_key=settings.UNIFIEDLICENSING['API_KEY'],
    product_key=settings.UNIFIEDLICENSING['PRODUCT_KEY'],
)

# Paths that must work without a license
EXEMPT_PATHS = {'/activate/', '/health/'}

_cache = {}          # license_key -> (timestamp, valid)
CACHE_TTL = 600      # 10 minutes


class LicenseMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        if request.path in EXEMPT_PATHS:
            return self.get_response(request)

        license_key = request.session.get('license_key')
        if not license_key:
            return redirect('/activate/')

        cached = _cache.get(license_key)
        if cached and time.time() - cached[0] < CACHE_TTL and cached[1]:
            return self.get_response(request)

        try:
            result = manager.validate(
                license_key,
                machine_id=request.session.get('machine_id'),
            )
        except ConnectionError:
            # Fail closed, but with a friendly page rather than a traceback
            from django.http import JsonResponse
            return JsonResponse({'error': 'License service unavailable'},
                                status=503)

        if not result['valid']:
            request.session.pop('license_key', None)
            return redirect('/activate/')

        _cache[license_key] = (time.time(), True)
        return self.get_response(request)

3. Activation view

licensing/views.py
import uuid

from django.conf import settings
from django.contrib import messages
from django.shortcuts import redirect, render

from UnifiedLicensing import UnifiedLicenseManager

manager = UnifiedLicenseManager(
    api_key=settings.UNIFIEDLICENSING['API_KEY'],
    product_key=settings.UNIFIEDLICENSING['PRODUCT_KEY'],
)


def activate(request):
    if request.method == 'POST':
        key = request.POST.get('license_key', '').strip().upper()

        if not request.session.get('machine_id'):
            request.session['machine_id'] = str(uuid.uuid4())

        try:
            result = manager.activate(key,
                                      machine_id=request.session['machine_id'])
        except ConnectionError:
            messages.error(request, 'License server unreachable.')
            return render(request, 'licensing/activate.html')

        if result['valid']:
            request.session['license_key'] = key
            messages.success(request,
                             f"License activated — plan: {result['plan']}")
            return redirect('/')
        messages.error(request, result['message'])

    return render(request, 'licensing/activate.html')
Multi-worker deployments

The module-level cache lives in one process. Under Gunicorn with multiple workers each process keeps its own copy — harmless, just slightly more API calls. For heavy traffic, swap _cache for Django's cache framework (django.core.cache.cache) backed by Redis and every worker shares one TTL.

07FastAPI Integration

FastAPI's dependency injection was practically designed for this. Define one dependency, attach it to any route (or the whole app), and protected endpoints simply refuse to run without a valid license.

1. Manager + dependency

deps.py
import os
import time
from typing import Annotated, Any, Dict

from fastapi import Depends, Header, HTTPException, status

from UnifiedLicensing import UnifiedLicenseManager

manager = UnifiedLicenseManager(
    api_key=os.environ['UL_API_KEY'],
    product_key=os.environ['UL_PRODUCT_KEY'],
)

_cache: Dict[str, tuple] = {}
CACHE_TTL = 600


async def require_license(
    x_license_key: Annotated[str | None, Header()] = None,
) -> Dict[str, Any]:
    """Dependency: caller must send X-License-Key with a valid license."""
    if not x_license_key:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail='Missing X-License-Key header',
        )

    key = x_license_key.strip().upper()

    cached = _cache.get(key)
    if cached and time.time() - cached[0] < CACHE_TTL:
        return cached[1]

    try:
        result = manager.validate(key, machine_id=None)   # auto-detected
    except ConnectionError:
        raise HTTPException(status_code=503,
                            detail='License service unavailable')

    if not result['valid']:
        raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
                            detail=result['message'])

    _cache[key] = (time.time(), result)
    return result


LicenseRequired = Annotated[Dict[str, Any], Depends(require_license)]

2. Use it on routes

main.py
from fastapi import FastAPI, HTTPException

from deps import LicenseRequired, require_license

app = FastAPI(title='My API')


@app.post('/activate')
async def activate(license_key: str):
    try:
        result = manager.activate(license_key.strip().upper(), machine_id=None)
    except ConnectionError:
        raise HTTPException(status_code=503, detail='License service unavailable')

    if not result['valid']:
        raise HTTPException(status_code=400, detail=result['message'])
    return {'activated': True, 'plan': result['plan']}


@app.get('/reports')
async def reports(license_info: LicenseRequired):
    # license_info contains plan, quota, expiry — use it freely
    return {'plan': license_info['plan'], 'data': build_reports()}


# Or guard everything under a router at once:
protected = APIRouter(dependencies=[Depends(require_license)])
# ...add routes to `protected`, then:  app.include_router(protected)
Async note

The SDK performs synchronous HTTP via requests. That's fine for low-to-moderate traffic; under high concurrency run calls in a thread pool: await run_in_threadpool(manager.validate, key) from starlette.concurrency. The 10-minute cache keeps the volume trivial for most apps either way.

08Desktop Apps

Desktop tools are where licensing matters most — your code ships onto machines you'll never see again. The flow: show a dialog asking for a key, activate it once, cache the result locally, then check quietly on every launch.

Tkinter example

app_tkinter.py
import json
import os
import tkinter as tk
from tkinter import messagebox

from UnifiedLicensing import UnifiedLicenseManager

KEYFILE = os.path.join(os.path.expanduser('~'), '.myapp_license.json')

manager = UnifiedLicenseManager(api_key='ul_live_YOUR_API_KEY',
                                product_key='your-product-key')


def save_key(license_key: str):
    with open(KEYFILE, 'w') as f:
        json.dump({'license_key': license_key}, f)


def load_key():
    if not os.path.exists(KEYFILE):
        return None
    try:
        with open(KEYFILE) as f:
            return json.load(f).get('license_key')
    except (json.JSONDecodeError, OSError):
        return None


def show_activation_dialog(root) -> bool:
    """Modal activation window. Returns True when a valid key is saved."""
    win = tk.Toplevel(root)
    win.title('Activate My App')
    win.geometry('420x180')
    win.grab_set()

    tk.Label(win, text='Enter your license key:',
             font=('Segoe UI', 11)).pack(pady=(18, 8))

    entry = tk.Entry(win, width=34, justify='center',
                     font=('Consolas', 11))
    entry.pack(pady=4)

    status = tk.Label(win, text='', fg='#888')
    status.pack(pady=6)

    def on_activate(event=None):
        key = entry.get().strip().upper()
        status.config(text='Checking…', fg='#888')
        root.update_idletasks()
        try:
            result = manager.activate(key, machine_id=None)
        except ConnectionError:
            status.config(text='Could not reach license server.', fg='#c00')
            return
        if result['valid']:
            save_key(key)
            win.destroy()
        else:
            status.config(text=result['message'], fg='#c00')

    btn = tk.Button(win, text='Activate', command=on_activate,
                    bg='#C7A34C', fg='white', relief='flat',
                    padx=18)
    btn.pack(pady=8)
    entry.bind('<Return>', on_activate)

    root.wait_window(win)
    return os.path.exists(KEYFILE)


def main():
    root = tk.Tk()
    root.withdraw()   # hidden until licensed

    saved = load_key()
    licensed = False
    if saved:
        try:
            licensed = manager.validate(saved, machine_id=None)['valid']
        except ConnectionError:
            # Offline grace period: trust the cached key today
            licensed = True

    if not licensed and not show_activation_dialog(root):
        messagebox.showerror('My App', 'A valid license is required.')
        return

    root.deiconify()
    root.title('My App — Licensed')
    tk.Label(root, text='Welcome! Everything works.', padx=40,
             pady=30).pack()
    root.mainloop()


if __name__ == '__main__':
    main()

PyQt5 example

app_pyqt.py
import sys

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QApplication, QDialog, QLabel, QLineEdit,
                             QMessageBox, QPushButton, QVBoxLayout)

from UnifiedLicensing import UnifiedLicenseManager

manager = UnifiedLicenseManager(api_key='ul_live_YOUR_API_KEY',
                                product_key='your-product-key')


class ActivationDialog(QDialog):
    def __init__(self):
        super().__init__()
        self.setWindowTitle('Activate My App')
        self.setFixedWidth(420)

        layout = QVBoxLayout(self)
        layout.addWidget(QLabel('Enter your license key:'))

        self.key_input = QLineEdit()
        self.key_input.setPlaceholderText('ABCD-EFGH-IJKL-MNOP')
        layout.addWidget(self.key_input)

        self.status_label = QLabel('')
        self.status_label.setStyleSheet('color: #888;')
        layout.addWidget(self.status_label)

        button = QPushButton('Activate')
        button.clicked.connect(self.try_activate)
        layout.addWidget(button)

    def try_activate(self):
        key = self.key_input.text().strip().upper()
        self.status_label.setText('Checking…')
        QApplication.processEvents()

        try:
            result = manager.activate(key, machine_id=None)
        except ConnectionError:
            self.status_label.setText('Could not reach license server.')
            self.status_label.setStyleSheet('color: #c00;')
            return

        if result['valid']:
            self.accept()   # closes dialog with success
        else:
            self.status_label.setText(result['message'])
            self.status_label.setStyleSheet('color: #c00;')


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

    dialog = ActivationDialog()
    if dialog.exec_() != QDialog.Accepted:
        sys.exit(0)

    window = QMainWindowStub()
    window.show()
    sys.exit(app.exec_())


class QMainWindowStub:
    """Replace with your real main window."""
    def show(self):
        pass


if __name__ == '__main__':
    main()
Keep validation off the GUI thread where it counts

The examples above call the API directly from UI callbacks for clarity — a slow network stalls the window briefly. For production polish, run validation in a worker thread (threading.Thread + a queue callback, or PySide's QThread) and disable the Activate button until the result arrives.

09Trials & Heartbeats

Start a trial

Trial tokens are generated by the API and tied to a machine ID, so wiping a local config file doesn't reset the clock — the trial state lives server-side.

from UnifiedLicensing import UnifiedLicenseManager

manager = UnifiedLicenseManager(api_key='ul_live_YOUR_API_KEY',
                                product_key='your-product-key')

try:
    trial = manager.start_trial(machine_id=None)   # auto-detected

    if trial['success']:
        # Persist the token — you need it for every future status check.
        config['trial_token'] = trial['trial_token']
        print(f"Trial started! {trial['days_remaining']} days remaining.")
    else:
        print(f"Trial unavailable: {trial['message']}")
        # Typical reasons: this machine already used its trial,
        # or the product doesn't have trials enabled.
except ConnectionError:
    print("Couldn't reach the license server.")

Check trial status

token = config.get('trial_token')

if token:
    status = manager.check_trial(token, machine_id=None)

    if status['valid']:
        print(f"Trial active — {status['days_remaining']} days left.")
    elif status['expired']:
        print("Trial ended. Time to upgrade!")
        # Show your pricing page here.
    else:
        print("No active trial for this machine.")

Heartbeats

A heartbeat is a periodic "still alive" ping. It keeps the machine marked as active, lets you detect revoked or refunded licenses within minutes instead of never, and gives you honest usage counts. Once an hour is plenty.

heartbeat_worker.py
"""Run hourly alongside your app, or from cron / systemd timer:

    0 * * * * python /opt/myapp/heartbeat_worker.py
"""
import logging

from UnifiedLicensing import UnifiedLicenseManager

logging.basicConfig(level=logging.INFO)
log = logging.getLogger('heartbeat')

manager = UnifiedLicenseManager(api_key='ul_live_YOUR_API_KEY',
                                product_key='your-product-key')


def send_heartbeat(license_key: str, machine_id: str) -> None:
    try:
        result = manager.heartbeat(license_key, machine_id=machine_id)

        if not result.get('valid', False):
            # License was revoked, refunded, or expired since our last check.
            log.warning('License invalid during heartbeat — locking features.')
            # Disable premium features, notify admin, exit, etc.
        else:
            log.info('Heartbeat OK.')

    except ConnectionError as e:
        # Network hiccup — not fatal. Next hour's beat will retry.
        log.error('Heartbeat failed: %s', e)
In-process alternative for long-running apps

For daemons and desktop apps, skip cron and start a daemon thread that sleeps 3600 seconds between beats. Wrap the whole loop body in try/except ConnectionError so a dead network never kills the thread — it should just sleep and retry.

10Complete Flask App

Here's a single-file Flask app with everything wired end to end: activation, a protected dashboard, trials, status checks, and graceful failure modes. Save it as app.py, fill in your keys, and run python app.py.

app.py
import functools
import os
import time
import uuid

from flask import (Flask, redirect, render_template_string, request,
                   session, url_for)

from UnifiedLicensing import UnifiedLicenseManager

UL_API_KEY = os.environ.get('UL_API_KEY', 'ul_live_YOUR_API_KEY')
UL_PRODUCT_KEY = os.environ.get('UL_PRODUCT_KEY', 'your-product-key')

manager = UnifiedLicenseManager(api_key=UL_API_KEY, product_key=UL_PRODUCT_KEY)

app = Flask(__name__)
app.secret_key = os.environ.get('FLASK_SECRET', 'dev-only-change-me')

_cache = {}
CACHE_TTL = 600

PAGE = """
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8">
<title>{{ title }}</title><style>
body { font-family: system-ui, sans-serif; background: #081220; color: #EFEAE0;
       display: flex; justify-content: center; padding: 48px 20px; }
.card { background: #14263B; border: 1px solid rgba(199,163,76,.25);
        border-radius: 12px; padding: 32px; max-width: 480px; width: 100%; }
h1 { color: #E4C468; font-size: 1.4rem; margin-bottom: 6px; }
.sub { color: #A9B2C3; font-size: .9rem; margin-bottom: 22px; }
input[type=text] { width: 100%; padding: 10px 12px; border-radius: 8px;
       border: 1px solid rgba(199,163,76,.3); background: #0E1B2C;
       color: #EFEAE0; font-family: monospace; margin-bottom: 14px; }
.row { display: flex; gap: 8px; flex-wrap: wrap; }
button { flex: 1; padding: 10px; border-radius: 8px; cursor: pointer;
         border: 1px solid rgba(199,163,76,.4); background: transparent;
         color: #E4C468; font-weight: 600; }
button.primary { background: #C7A34C; color: #081220; }
.msg { margin-top: 16px; padding: 10px 14px; border-radius: 8px; font-size: .9rem; }
.msg.ok  { background: rgba(126,201,143,.12); color: #7ec98f; }
.msg.err { background: rgba(181,80,46,.15);  color: #e08a63; }
.status { margin-top: 22px; padding: 14px 16px; border-radius: 8px;
          background: #0E1B2C; border: 1px solid rgba(199,163,76,.2);
          font-size: .88rem; line-height: 1.7; }
.status b { color: #E4C468; }
a { color: #E4C468; }
</style></head><body><div class="card">
<h1>🔒 {{ title }}</h1>

{% block body %}{% endblock %}
</div></body></html>"""

ACTIVATE_PAGE = """
{% extends PAGE %}{% block body %}
<p class="sub">Enter the license key from your purchase email.</p>
{% if msg %}<div class="msg {{ cls }}">{{ msg }}</div>{% endif %}
<form method="post">
    <input type="text" name="license_key" placeholder="ABCD-EFGH-IJKL-MNOP"
           autocomplete="off" required>
    <div class="row">
        <button type="submit" name="action" value="activate" class="primary">
            Activate</button>
        <button type="submit" name="action" value="trial">Start Trial</button>
    </div>
</form>
{% endblock %}"""

DASHBOARD_PAGE = """
{% extends PAGE %}{% block body %}
<p class="sub">Your app content lives here.</p>
{% if msg %}<div class="msg {{ cls }}">{{ msg }}</div>{% endif %}
<div class="status">
    {% if info.valid %}
        <b>✓ Licensed</b><br>
        Plan: <b>{{ info.plan }}</b><br>
        Seats: {{ info.quota_used }} / {{ info.quota_limit }}<br>
        Expires: {{ info.expires_at or 'never' }}
    {% else %}
        <b style="color:#e08a63">✗ Not licensed</b><br>
        {{ info.message }}
    {% endif %}
</div>
<p style="margin-top:18px"><a href="{{ url_for('check') }}">Re-check now</a>
   &middot; <a href="{{ url_for('logout') }}">Deactivate</a></p>
{% endblock %}"""


def licensed(view):
    @functools.wraps(view)
    def wrapper(*args, **kwargs):
        key = session.get('license_key')
        if not key:
            return redirect(url_for('activate'))

        cached = _cache.get(key)
        if cached and time.time() - cached[0] < CACHE_TTL:
            return view(*args, **kwargs)

        try:
            result = manager.validate(key,
                                      machine_id=session.get('machine_id'))
        except ConnectionError:
            return render_template_string(PAGE, title='Offline',
                **{'body': ''}), 503

        if not result['valid']:
            session.pop('license_key', None)
            return redirect(url_for('activate'))

        _cache[key] = (time.time(), result)
        return view(*args, **kwargs)
    return wrapper


@app.route('/activate', methods=['GET', 'POST'])
def activate():
    msg, cls = '', ''
    if request.method == 'POST':
        action = request.form.get('action')
        session.setdefault('machine_id', str(uuid.uuid4()))
        key = request.form.get('license_key', '').strip().upper()

        try:
            if action == 'trial':
                t = manager.start_trial(machine_id=session['machine_id'])
                if t['success']:
                    msg = f"Trial started — {t['days_remaining']} days remaining."
                    cls = 'ok'
                else:
                    msg = t['message']
                    cls = 'err'
            else:
                r = manager.activate(key, machine_id=session['machine_id'])
                if r['valid']:
                    session['license_key'] = key
                    _cache.pop(key, None)
                    return redirect(url_for('dashboard'))
                msg, cls = r['message'], 'err'
        except ConnectionError:
            msg, cls = 'Could not reach the license server.', 'err'

    return render_template_string(ACTIVATE_PAGE, PAGE=PAGE,
                                  title='Activate', msg=msg, cls=cls)


@app.route('/dashboard')
@licensed
def dashboard():
    key = session['license_key']
    info = _cache.get(key, (0, {'valid': False, 'message': 'Unknown'}))[1]

    msg, cls = '', ''
    if 'msg' in request.args:
        msg, cls = request.args['msg'], 'ok'

    return render_template_string(DASHBOARD_PAGE, PAGE=PAGE,
                                  title='Dashboard', info=info, msg=msg,
                                  cls=cls)


@app.route('/check')
@licensed
def check():
    key = session['license_key']
    _cache.pop(key, None)   # force a live re-check
    return redirect(url_for('dashboard', msg='Status refreshed.'))


@app.route('/logout')
def logout():
    session.clear()
    return redirect(url_for('activate'))


if __name__ == '__main__':
    app.run(debug=True)

What this little app demonstrates:

11Troubleshooting

The stuff that actually bites people, and how to fix each one fast.

SSLError / certificate verify failed

Usually a missing or outdated CA bundle — common on corporate Windows machines and some minimal Docker images. Fix the bundle itself rather than disabling verification:

# Option A: point requests at certifi's bundle explicitly
import certifi, requests
print(certifi.where())   # confirm the path exists

# Option B (Docker/Debian slim images): install the OS bundle
apt-get update && apt-get install -y ca-certificates
update-ca-certificates

If nothing else works during local testing, verify=False silences the error — but treat that strictly as a temporary diagnostic, never production code.

ImportError: cannot import name 'UnifiedLicenseManager'

Three usual causes, in order of likelihood:

  • The file wasn't renamed to UnifiedLicensing.py (the .Python.v4.0-Complete suffix isn't importable).
  • Your own test file is named UnifiedLicensing.py and shadows the real one — rename the test.
  • You're running from a directory the module isn't in — check sys.path or move the file next to your entry point.

ModuleNotFoundError: No module named 'requests'

The dependencies weren't installed into the interpreter actually running your code. With multiple Python versions this bites constantly:

# Always install against the interpreter you run:
python -m pip install requests cryptography

# Verify from inside your venv:
python -c "import requests, cryptography; print('ok')"

ConnectionError even though the site is up

Check the basics in order: outbound HTTPS allowed from this network? Corporate proxy set via the standard HTTPS_PROXY env var? DNS resolving (nslookup api.unifiedlicensing.com)? Behind strict firewalls (common on client servers), ask IT to allow-list api.unifiedlicensing.com on port 443.

"License already active on maximum machines"

This isn't a bug — the seat quota is full. Legitimate fixes: the customer deactivates an old machine from their purchase email/dashboard, or you bump the quota on their license from the vendor panel. In dev, remember that every activate() call burns a seat too — use validate() in tests, and test keys prefixed ul_test_.

Validation passes locally but fails in production

Nearly always a machine-ID mismatch: your local run auto-detects one hostname while the deployed container/server presents another, and the license is bound to the original. Self-hosted deployments should generate a stable machine ID once (see section 4) and persist it across restarts/redeploys instead of re-deriving it.

Flask/Django session lost after restart

Sessions are cookie-backed and signed with secret_key. If that key changes between restarts (or workers), all sessions invalidate and users land back on the activation page. Set a fixed SECRET_KEY/FLASK_SECRET in the environment.

Still stuck?

Enable verbose logging to see exactly what's sent and received:

import logging
logging.basicConfig(level=logging.DEBUG)
logging.getLogger('urllib3').setLevel(logging.DEBUG)   # requests uses urllib3

If the log shows a well-formed request and a non-valid response, compare the message field against the table in section 4 — it names the exact reason. Otherwise, grab support from your vendor dashboard with the request timestamp.