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.
Got a Python app and 30 seconds? Install two dependencies, drop in the SDK, validate a license:
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.
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.
ul_live_ (or ul_test_ while you're experimenting).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
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.
The SDK has exactly two third-party dependencies. Pin them in your requirements file:
requests>=2.28
cryptography>=41.0
api.unifiedlicensing.com, with connection pooling and sane timeouts.api.unifiedlicensing.com.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.
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
)
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']
)
UL_API_KEY=ul_live_YOUR_API_KEY
UL_PRODUCT_KEY=your-product-key
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.
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.")
| Field | Type | Meaning |
|---|---|---|
valid | bool | The big one. True means usable on this machine. |
status | str | active, expired, suspended, or revoked. |
plan | str | The plan name — handy for gating pro features. |
quota_used | int | Machines currently activated against this license. |
quota_limit | int | Total seats the customer bought. |
expires_at | str | None | ISO-8601 expiry date (None for lifetime licenses). |
message | str | Human-friendly reason when valid is False. |
Every call is a signed POST to the validation endpoint. Knowing the wire format helps when debugging with logs or a proxy:
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() 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().
Flask users get the cleanest pattern: initialize once in create_app(), guard routes with a decorator, and never think about licensing again.
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
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')
@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'))
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.
Django projects benefit most from middleware: one component validates on every request, and your views stay completely licensing-free.
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
]
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)
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')
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.
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.
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)]
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)
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.
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.
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()
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()
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.
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.")
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.")
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.
"""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)
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.
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.
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>
· <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:
activate())start_trial())@licensed)The stuff that actually bites people, and how to fix each one fast.
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.
Three usual causes, in order of likelihood:
UnifiedLicensing.py (the .Python.v4.0-Complete suffix isn't importable).UnifiedLicensing.py and shadows the real one — rename the test.sys.path or move the file next to your entry point.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')"
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.
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_.
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.
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.
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.