Good licensing UX is invisible when things work and helpful when they don’t. These patterns ensure your customers have a smooth experience.
First-run experience
The first time a user launches your app, they need to enter a license key. Make this frictionless:
Desktop apps
Show a clean, focused dialog. Don’t overwhelm with options; just a single input field and an activate button.
Welcome to YourApp
Enter your license key to get started
XXXX-XXXX-XXXX-XXXX
PurchaseActivate
- Auto-format the input as the user types (add dashes automatically).
- Accept keys with or without dashes.
- Show a “Purchase” link for users who don’t have a key yet.
- Trim whitespace; users often copy keys with trailing spaces.
Support multiple input methods to minimize friction:
# Via flag
yourapp --license-key XXXX-XXXX-XXXX-XXXX
# Via environment variable
export AUTHFORGE_LICENSE_KEY=XXXX-XXXX-XXXX-XXXX
yourapp
# Interactive prompt (if no key found)
yourapp
> No license key found. Enter your key: _
Storing the key locally
After successful validation, save the key locally so users don’t re-enter it on every launch.
import json
import os
CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".yourapp")
CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
def save_license_key(key):
os.makedirs(CONFIG_DIR, exist_ok=True)
config = load_config()
config["licenseKey"] = key
with open(CONFIG_FILE, "w") as f:
json.dump(config, f)
def load_license_key():
config = load_config()
return config.get("licenseKey")
On subsequent launches, load the stored key and authenticate silently:
stored_key = load_license_key()
if stored_key and client.login(stored_key):
# Authenticated silently; go straight to the app
run_app()
else:
# Show the license key dialog
key = show_license_dialog()
if client.login(key):
save_license_key(key)
run_app()
Settings page
Provide a settings or “About” page where users can see their license status:
| Field | Value |
|---|
| License key | A3K9-****-****-QHDT (partially masked) |
| Status | Active |
| Plan | Pro |
| Expires | December 31, 2026 |
| Devices | 1 of 2 slots used |
| [Deactivate] |
- Mask the license key (show first and last groups only).
- Show the expiry date prominently.
- Include a “Deactivate” button that clears the stored key and returns to the license dialog.
- If using license variables, show the plan tier.
Expiration and renewal
When a license is approaching expiration, show gentle reminders:
from datetime import datetime, timedelta
if client.login(license_key):
expires_at = client.license_variables.get("expiresAt")
if expires_at:
expiry = datetime.fromisoformat(expires_at)
days_left = (expiry - datetime.now()).days
if days_left <= 7:
show_banner(
f"Your license expires in {days_left} days. "
"Renew now to avoid interruption.",
action_url="https://yoursite.com/renew"
)
When the license has expired, show a clear message with a path to renewal:
Your license has expired
Your license expired on December 31, 2026.
Renew to continue using YourApp.
Enter New KeyRenew
Trial mode
Use the Developer API to issue time-limited trial licenses:
// Create a 14-day trial license
const trial = await fetch("https://api.authforge.cc/v1/licenses", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
appId: process.env.AUTHFORGE_APP_ID,
quantity: 1,
expiresAt: new Date(Date.now() + 14 * 86400000).toISOString(),
label: "trial",
}),
});
In the app, show the remaining trial period:
if is_trial:
days_left = calculate_days_remaining()
show_persistent_banner(f"Trial: {days_left} days remaining. Upgrade to keep using YourApp.")
Upgrade prompts
When a user on a lower tier tries to use a gated feature, show a helpful prompt instead of silently failing:
def export_project():
features = client.license_variables.get("features", "")
if "export" not in features:
show_upgrade_dialog(
title="Export requires Pro",
message="Upgrade to the Pro plan to unlock project export.",
action_url="https://yoursite.com/pricing"
)
return
# ... actual export logic
Graceful degradation
If your app has a free tier or limited mode, fall back to it instead of killing the app entirely:
if client.login(license_key):
run_full_app()
elif has_free_tier:
show_notice("Running in free mode. Some features are limited.")
run_limited_app()
else:
show_error("Invalid license key.")
exit(1)
Error messages
Always show user-friendly messages. Map internal error codes to helpful text:
| Error | User sees |
|---|
invalid_key | ”Invalid license key. Please check and try again.” |
expired | ”Your license has expired. [Renew now]“ |
revoked | ”This license has been deactivated. Contact support.” |
hwid_mismatch | ”This license is in use on another device. Contact support to transfer.” |
no_credits | ”Service temporarily unavailable. Please try again in a few minutes.” |
| Network error | ”Couldn’t connect to the license server. Check your internet connection.” |
Never show internal error codes like no_credits or invalid_app to end users.
Offline first launch
The initial authentication always requires a network connection. Make this clear:
Connection Required
YourApp needs an internet connection for first-time activation. After that, it works offline.
Retry
After initial activation with LOCAL heartbeat mode, the app works offline until the session token expires (24 hours by default; up to 7 days if the SDK was configured with a longer ttlSeconds).