What you will make
A small window with a license key, customer email, Check now, Export CSV and Release this machine. The app saves a real CSV only after a verified license includes csv_export. No developer management key is embedded.
Download the project and extract it to a normal private folder. Keep its files together. Follow the shared setup first: a tutorial product, a normal plan with csv_export, a customer and a Sandbox license.
1. Install Python and the SDK
Use Python 3.11–3.14. In the extracted project folder:
python -m venv .venv
Activate the environment with the command for your terminal:
| Terminal | Command |
|---|---|
| Windows PowerShell | .venv\Scripts\Activate.ps1 |
| macOS / Linux shell | source .venv/bin/activate |
Then install the SDK. Flask is not needed for this desktop app.
python -m pip install "kdpika-licensing==2.0.1"
python -m tkinter
The second command should open a small Tk test window; close it. If Tkinter is missing, use a Python installation with Tk support. Debian/Ubuntu distributions commonly provide python3-tk; package names vary on other Linux distributions. This is a local app prerequisite, not a server/hosting requirement.
2. Set your public App ID
Replace app_YOUR_PUBLIC_ID with the real ID copied from Products. It is public configuration, not a management secret.
macOS/Linux:
export KDP_APP_ID="app_YOUR_PUBLIC_ID"
export KDP_ENVIRONMENT="SANDBOX"
python desktop.py
Windows PowerShell:
$env:KDP_APP_ID="app_YOUR_PUBLIC_ID"
$env:KDP_ENVIRONMENT="SANDBOX"
python desktop.py
Grant product access
SandboxThis license will be isolated in SANDBOX.
The dashboard creates or reuses the customer for the entered email. You do not need a separate Add customer step.
3. Activate and export
- Paste the Sandbox license key into the window and enter the email attached to it.
- Choose Activate. The SDK collects the installation/device information, calls KDPika and verifies its signed answer.
- Type a message and choose Export CSV. Open the path shown by the app. The CSV contains your message.
- Close and reopen the app. Its private saved state remembers this installation; you do not need to enter the key on every start.
The files live under your user profile in .kdpika-python-tutorial, separated by product and environment. Do not place this folder in the application installer, a shared network folder or a public web directory. On Windows configure private user ACLs; POSIX mode bits alone do not set them. Optional OS keyring support is described in Python storage guidance.
4. Understand the important code
common.py is the small shared part used by both starter apps. The important order is: obtain a result, require valid + verified access, require the feature, then do the work.
"""Shared application code. Real licensing calls; never a developer management key."""
import csv
import os
from pathlib import Path
from kdpika_licensing import LicenseClient
class AccessDenied(Exception):
pass
def application_folder() -> Path:
# Use one private, persistent folder for each app + machine + environment.
product = os.environ.get("KDP_APP_ID", "").strip()
if not product.startswith("app_") or not product.replace("_", "").isalnum():
raise ValueError("Set KDP_APP_ID to your product's public App ID first.")
environment = os.environ.get("KDP_ENVIRONMENT", "SANDBOX").upper()
if environment not in {"SANDBOX", "LIVE"}:
raise ValueError("KDP_ENVIRONMENT must be SANDBOX or LIVE.")
folder = Path.home() / ".kdpika-python-tutorial" / product / environment.lower()
folder.mkdir(parents=True, exist_ok=True, mode=0o700)
return folder
def make_client() -> LicenseClient:
folder = application_folder()
return LicenseClient(
product=os.environ["KDP_APP_ID"],
environment=os.environ.get("KDP_ENVIRONMENT", "SANDBOX"),
base_url="https://licensing.kdpika.com",
application_name="Python Licensing Tutorial",
application_version="1.0.0",
storage_dir=folder / "license-state",
)
def require_csv_access(client: LicenseClient):
result = client.check()
if not result.valid or not result.signature_valid:
raise AccessDenied("Check your license or reconnect, then try again.")
if result.payload.get("metered"):
# A normal feature check does NOT charge a credit. Never offer free work by mistake.
raise AccessDenied("This license uses credits. Run credit_export.py for paid exports.")
if not result.feature("csv_export"):
raise AccessDenied("CSV export is not included in this license.")
return result
def export_csv(client: LicenseClient, text: str) -> Path:
require_csv_access(client) # Check again at the protected action, not only at startup.
path = application_folder() / "export.csv"
with path.open("w", encoding="utf-8", newline="") as output:
writer = csv.writer(output)
writer.writerow(["Message"])
# Keep spreadsheet applications from interpreting user text as a formula.
writer.writerow(["'" + text if text.lstrip().startswith(("=", "+", "-", "@")) else text])
return path
The example intentionally refuses metered exports. A feature check does not spend a credit; accepting it alone would accidentally provide unpaid work. After understanding this first app, run the credit-based export.
5. Add device rules and offline access
Set the device allowance in the software plan or one license's Limits & overrides. It is enforced by the licensing server; there is no max_devices=... constant to trust inside the client.
Set normal validation/offline windows in Products → Validation & offline policy. check() uses a valid saved result when permitted; validate() explicitly checks online. An expired license cannot be made usable by increasing the offline window.
If your KDPika subscription includes advanced matching, choose strict/flexible matching in that product policy. The SDK handles the fingerprint; never use a sample's made-up component hash or a random ID as a hardware fallback. Hardware matching is software-based, not hardware attestation.
6. Try shared seats
- Create a separate floating software plan. Set a positive shared-seat allowance and timing values; keep the device allowance large enough for your intended installations. Your KDPika plan must include floating licensing.
- Issue a Sandbox license and activate it in this app. The SDK automatically checks out a seat and starts background heartbeats after successful activation/check.
- Keep every protected operation behind a fresh
client.check()/ feature guard, as the CSV action already does. A background worker alone is not permission to continue forever. - Closing this window calls
client.close(), releasing a shared seat. It does not deactivate the installed machine. Release this machine usesdeactivate()and requires activation again.
When using explicit SDK seat operations in your own UI:
from kdpika_licensing import AllSeatsInUseError
try:
result = client.checkout()
if not result.valid or not result.signature_valid:
show_access_unavailable()
elif result.feature("csv_export"):
enable_export_button()
except AllSeatsInUseError:
show_message("All seats are in use. Close the app on another machine and try again.")
# These UI callback names belong to your own application.
# Still check access again immediately before protected work.
After a crash there may be no checkin; the signed lease times out. Offline access stops at the shared-seat deadline even if normal offline grace is longer. Long work should check its authorization at sensible boundaries, not only before the entire job starts.
7. Give a customer extra access
Open their license → Limits & overrides. For example, grant 3 devices while its plan normally allows 2. The customer's next eligible check uses the signed effective settings. Reset to plan default removes the exception.
To change everyone, edit the plan and choose the appropriate new-only or new-and-existing scope. The preview shows the affected licenses; individual overrides are preserved. Lowering a limit does not silently kick out already-active devices/seats: it blocks additional ones until usage is below the limit.
8. Add customer self-service
Open Customer portal, configure your name/logo/support contact and copy its sign-in link. When testing, use its Sandbox link. You can put that link in your app's Help menu using Python's webbrowser.open(portal_url). Copy the real sign-in URL from the dashboard; do not construct a token URL or put a license key into a query string.
The hosted portal handles verified email links, license-key sign-in, allowed device/session removal, balances and history. Hide KDPika branding only if your platform plan includes it. No custom-domain host or payment flow is created by this example.
Full window code
SDK calls run in one worker so a slow network does not freeze the window. Tk widgets are updated on the main thread. No GUI screenshots/target-OS packaging certification are claimed by the downloadable example.
"""Small real desktop app. SDK/network work stays off the Tk event thread."""
from concurrent.futures import ThreadPoolExecutor
from common import AccessDenied, export_csv, make_client
def main():
import tkinter as tk
from tkinter import ttk
client = make_client()
root = tk.Tk()
root.title("My licensed Python app")
root.geometry("560x600")
root.minsize(360, 560)
frame = ttk.Frame(root, padding=24)
frame.pack(fill="both", expand=True)
frame.columnconfigure(0, weight=1)
ttk.Label(frame, text="Make your first licensed export", font=("", 16, "bold")).grid(sticky="w")
status = tk.StringVar(value="Paste your test license and its matching email.")
status_label = ttk.Label(frame, textvariable=status, wraplength=460)
status_label.grid(sticky="ew", pady=(10, 18))
frame.bind("<Configure>", lambda event: status_label.configure(wraplength=max(250, event.width - 48)))
ttk.Label(frame, text="License key").grid(sticky="w")
key = ttk.Entry(frame, show="*", font=("", 12))
key.grid(sticky="ew", pady=(4, 10))
ttk.Label(frame, text="Email on this license").grid(sticky="w")
email = ttk.Entry(frame, font=("", 12))
email.grid(sticky="ew", pady=(4, 10))
message = ttk.Entry(frame, font=("", 12))
message.insert(0, "My first licensed export")
message.grid(sticky="ew", pady=(10, 12))
actions = ttk.Frame(frame)
actions.grid(sticky="ew")
executor = ThreadPoolExecutor(max_workers=1)
buttons, pending = [], []
closing = False
def show_result(result):
status.set("License accepted. Try Export CSV." if result.valid and result.signature_valid
else "Access is unavailable. Check your license or connection.")
key.delete(0, "end")
def run(work, done):
if pending or closing:
return
status.set("Checking…")
for button in buttons:
button.state(["disabled"])
pending.append((executor.submit(work), done))
def show_released(result):
status.set("Machine released. Activate again before exporting." if result.valid
else "The machine could not be released. Reconnect and try again.")
def activate():
license_key, customer_email = key.get().strip(), email.get().strip()
run(lambda: client.activate(license_key, customer_email or None), show_result)
def export():
text = message.get()
run(lambda: export_csv(client, text), lambda path: status.set(f"Saved: {path}"))
for label, command in [("Activate", activate), ("Check now", lambda: run(client.validate, show_result)),
("Export CSV", export), ("Release this machine", lambda: run(client.deactivate, show_released))]:
button = ttk.Button(actions, text=label, command=command)
button.pack(side="top", fill="x", pady=3)
buttons.append(button)
def finish_close(_):
executor.shutdown(wait=False)
root.destroy()
def close():
nonlocal closing
if closing:
return
closing = True
status.set("Closing and releasing any shared seat…")
for button in buttons:
button.state(["disabled"])
# Queued after any in-progress action. close() releases a floating seat, not the machine.
pending.append((executor.submit(client.close), finish_close))
def poll():
if pending and pending[0][0].done():
future, done = pending.pop(0)
try:
done(future.result())
except AccessDenied as error:
status.set(str(error))
except Exception:
status.set("The action could not be completed. Check your connection and try again.")
if closing:
finish_close(None)
return
if not closing:
for button in buttons:
button.state(["!disabled"])
elif not pending:
return
root.after(75, poll)
root.protocol("WM_DELETE_WINDOW", close)
root.after(75, poll)
run(client.check, show_result)
root.mainloop()
if __name__ == "__main__":
main()
If something does not work
| What happens | What to check |
|---|---|
| Key is refused | Same product, Sandbox environment and matching customer email |
| CSV export is unavailable | A verified current license includes csv_export; use existing-license scope if you changed a plan |
| Device cannot match | Check the real machine and publisher policy; do not fabricate a fingerprint |
| No shared seat | Close another running app or end a genuinely stuck session in the portal/dashboard |
| Access stops offline | The signed offline/lease deadline may have passed; reconnect |
| This license uses credits | Use the credit example instead of a free feature-only export |