Official Python SDK for the LicensesOS license management API
Project description
licenseos
Official Python SDK for LicensesOS -- software license management, validation, activation, and usage metering.
Installation
pip install licenseos
Requires Python 3.9 or higher. One runtime dependency (httpx).
For offline validation support (Ed25519 token verification):
pip install licenseos[offline]
Quick Start
from licenseos import LicensesOSClient
client = LicensesOSClient("your-api-key")
result = client.validate("XXXX-XXXX-XXXX-XXXX")
if result.is_valid():
print("License is valid")
else:
print("Invalid:", result.error["message"] if result.error else "Unknown error")
Or use the client as a context manager:
with LicensesOSClient("your-api-key") as client:
result = client.validate("XXXX-XXXX-XXXX-XXXX")
Configuration
client = LicensesOSClient(
"your-api-key",
base_url="https://api.licenseos.com", # default
timeout=30.0, # seconds, default 30
)
| Option | Type | Default | Description |
|---|---|---|---|
base_url |
str |
https://api.licenseos.com |
API base URL |
timeout |
float |
30.0 |
Request timeout in seconds |
License Validation
Basic Validation
result = client.validate("XXXX-XXXX-XXXX-XXXX")
if result.is_valid():
print("Status:", result.license["status"])
Validation with Identifier
Pass a domain or device identifier to check activation status:
result = client.validate("XXXX-XXXX-XXXX-XXXX", "example.com")
if result.is_valid():
print("Activated:", result.activated)
print("Remaining activations:", result.get_remaining_activations())
Validation with Metadata
result = client.validate("XXXX-XXXX-XXXX-XXXX", "example.com", {
"app_version": "2.1.0",
"os": "linux",
})
Checking Entitlements
if result.is_valid():
if result.has_entitlement("premium_features"):
enable_premium()
max_seats = result.get_entitlement("max_seats", 1)
print("Max seats:", max_seats)
Checking Status
result.is_active() # status == "active"
result.is_expired() # license is expired
result.is_revoked() # status == "revoked"
ValidationResult Properties
| Property | Type | Description |
|---|---|---|
valid |
bool |
Whether the license is valid |
license |
dict | None |
License details |
activated |
bool | None |
Whether the identifier is activated |
activation |
dict | None |
Activation details if activated |
offline_token |
str | None |
Offline validation token (if policy allows) |
error |
dict | None |
Error details if validation failed |
| Method | Returns | Description |
|---|---|---|
is_valid() |
bool |
Whether the license is valid |
is_active() |
bool |
Whether the status is active |
is_expired() |
bool |
Whether the license is expired |
is_revoked() |
bool |
Whether the license is revoked |
has_entitlement(key) |
bool |
Check if an entitlement key exists |
get_entitlement(key, default) |
Any |
Get an entitlement value with default |
get_remaining_activations() |
int | None |
Remaining activations, None if unlimited |
to_dict() |
dict |
Serialize to a plain dictionary |
Activation Management
Activate a License
result = client.activate(
"XXXX-XXXX-XXXX-XXXX",
"example.com",
label="Production Server",
metadata={"server_id": "srv-01"},
)
print("Activation ID:", result.activation["id"])
print("Remaining:", result.get_remaining_activations())
Deactivate a License
result = client.deactivate("XXXX-XXXX-XXXX-XXXX", "example.com")
if result.is_deactivated():
print("Successfully deactivated")
List Activations
result = client.list_activations("XXXX-XXXX-XXXX-XXXX")
print("Total:", result.count)
print("Limit:", result.limit)
print("Active:", result.get_active_count())
for activation in result.activations:
print(f"{activation['identifier']} - {activation['status']}")
# Check if a specific identifier is activated
if result.has_activation("example.com"):
print("example.com is activated")
Heartbeat Check-ins
Send periodic heartbeat signals to confirm an activation is still alive:
result = client.heartbeat("XXXX-XXXX-XXXX-XXXX", "example.com")
print("Next check-in at:", result.next_check_in_at)
print("Seconds until next:", result.get_seconds_until_next_check_in())
Automatic Heartbeat Loop
import time
import threading
def start_heartbeat(client, license_key, identifier, interval=300):
def loop():
while True:
try:
result = client.heartbeat(license_key, identifier)
print("Heartbeat OK, next at:", result.next_check_in_at)
except Exception as e:
print("Heartbeat failed:", e)
time.sleep(interval)
thread = threading.Thread(target=loop, daemon=True)
thread.start()
HeartbeatResult Properties
| Property | Type | Description |
|---|---|---|
activation |
dict |
The activation details |
next_check_in_at |
str |
ISO 8601 timestamp of next deadline |
| Method | Returns | Description |
|---|---|---|
get_next_check_in() |
datetime |
Next check-in as a datetime object |
get_seconds_until_next_check_in() |
float |
Seconds remaining until next check-in |
Usage Metering
Track and enforce API call counts, export quotas, or any usage-based limit.
Increment Usage
result = client.increment_usage("XXXX-XXXX-XXXX-XXXX")
print("Current uses:", result.usage["uses_count"])
print("Max uses:", result.usage["max_uses"])
print("Remaining:", result.usage["uses_remaining"])
Increment by a Specific Amount
result = client.increment_usage("XXXX-XXXX-XXXX-XXXX", 5)
Check Usage Limits
if result.is_limit_reached():
print("Usage limit reached")
pct = result.get_usage_percentage()
if pct is not None and pct > 80:
print(f"Warning: {pct:.0f}% of usage limit consumed")
reset_date = result.get_reset_date()
if reset_date:
print("Usage resets at:", reset_date.isoformat())
UsageResult Properties
| Property | Type | Description |
|---|---|---|
usage |
dict |
Usage counts, limits, and resets |
The usage dictionary contains:
| Field | Type | Description |
|---|---|---|
uses_count |
int |
Current usage count |
max_uses |
int | None |
Maximum allowed uses, None if unlimited |
uses_remaining |
int | None |
Remaining uses, None if unlimited |
usage_reset_interval |
str | None |
Reset interval (monthly, yearly) |
uses_reset_at |
str | None |
Next reset timestamp (ISO 8601) |
| Method | Returns | Description |
|---|---|---|
is_limit_reached() |
bool |
Whether the usage limit has been hit |
get_usage_percentage() |
float | None |
Usage as a percentage of the limit |
get_reset_date() |
datetime | None |
Next usage reset as a datetime object |
Offline Validation
When a policy has offline validation enabled, the validate() response includes a signed token. You can verify this token locally without contacting the API server.
Requires the cryptography package:
pip install licenseos[offline]
Step 1: Obtain an Offline Token
result = client.validate("XXXX-XXXX-XXXX-XXXX", "example.com")
if result.is_valid() and result.offline_token:
# Persist the token (e.g., to a file or database)
with open("license.token", "w") as f:
f.write(result.offline_token)
Step 2: Validate Offline
from licenseos import OfflineValidator
# Your app's Ed25519 public key (base64-encoded, from the signing-key endpoint)
validator = OfflineValidator("base64-encoded-public-key")
with open("license.token") as f:
token = f.read()
result = validator.validate(token)
if result.is_valid():
print("License valid offline")
print("Status:", result.status)
print("Entitlements:", result.entitlements)
print("Expires in:", result.remaining_seconds(), "seconds")
elif result.is_expired():
print("Token expired -- re-validate online for a fresh token")
else:
print("Invalid token:", result.error_code)
OfflineValidationResult Properties
| Property | Type | Description |
|---|---|---|
valid |
bool |
Whether the token is valid |
status |
str |
License status (active, etc.) |
error_code |
str | None |
Error code if invalid |
license_id |
str | None |
License ID |
entitlements |
dict | None |
License entitlements |
expires_at |
str | None |
License expiration (ISO 8601) |
activation_limit |
int | None |
Maximum activations |
activation_count |
int |
Current activation count |
identifier |
str | None |
Device/domain identifier |
activated |
bool |
Whether identifier is activated |
issued_at |
int |
Token issue timestamp (Unix) |
expires_at_timestamp |
int |
Token expiry timestamp (Unix) |
ttl |
int |
Token TTL in seconds |
uses_count |
int |
Current usage count |
max_uses |
int | None |
Maximum allowed uses |
| Method | Returns | Description |
|---|---|---|
is_valid() |
bool |
Whether the token is valid |
is_expired() |
bool |
Whether the token has expired |
remaining_seconds() |
int |
Seconds remaining before token expiry |
Machine Fingerprinting
Generate a unique machine identifier for device-locked licenses. Uses standard library only (no extra dependencies).
from licenseos import get_machine_id
machine_id = get_machine_id()
result = client.validate("XXXX-XXXX-XXXX-XXXX", machine_id)
The machine ID is a SHA-256 hash of the MAC address and hostname.
Error Handling
The SDK raises two error types, both extending LicensesOSError:
ApiError-- The API returned a non-2xx response. Hascode(machine-readable string likeLICENSE_NOT_FOUND) andstatus_code(HTTP status).NetworkError-- A network-level failure occurred (timeout, DNS, connection refused). Has an optionalcausewith the underlying error.
from licenseos import ApiError, NetworkError
try:
result = client.activate("XXXX-XXXX-XXXX-XXXX", "example.com")
except ApiError as e:
print(f"API error [{e.code}]: {e} (HTTP {e.status_code})")
if e.code == "LICENSE_NOT_FOUND":
print("The license key does not exist")
elif e.code == "ACTIVATION_LIMIT_REACHED":
print("No more activations available")
elif e.code == "LICENSE_SUSPENDED":
print("This license has been suspended")
except NetworkError as e:
print("Network error:", e)
# Fall back to cached state or offline validation
Note: The
validate()method does not raise for invalid licenses. An invalid license returns aValidationResultwithvalid=Falseand anerrordict.ApiErroris only raised for actual API errors (authentication failure, server error, etc.).
Domain Normalization
The SDK automatically normalizes domain/URL identifiers before sending them to the API. The static method is also available for direct use:
LicensesOSClient.normalize_domain("https://WWW.Example.Com:8080/path")
# Returns: "example.com"
LicensesOSClient.normalize_domain("HTTP://blog.example.com/")
# Returns: "blog.example.com"
Normalization rules:
- Lowercases and strips whitespace
- Strips
http://andhttps://protocol prefixes - Strips paths, ports, and
www.prefix - IDNA-encodes internationalized domain names
Grace Period / Cached State
For resilience when the API is unreachable, cache the validation status and timestamp, then use should_allow_premium() to decide if the cached result is still trustworthy:
import time
# After a successful validation
cached_state = {
"status": result.license["status"] if result.license else None,
"cached_at": int(time.time()),
}
save_to_storage(cached_state)
# Later, when the API is unreachable
state = load_from_storage()
if LicensesOSClient.should_allow_premium(state):
enable_premium() # Cache is fresh enough
else:
disable_premium() # Cache is stale
The default grace period is 48 hours beyond the 12-hour cache TTL (60 hours total). Customize it:
# Custom grace period: 24 hours (in seconds)
LicensesOSClient.should_allow_premium(state, 86_400)
# No grace period -- strict 12-hour cache TTL only
LicensesOSClient.should_allow_premium(state, 0)
Type Safety
The SDK ships with full type annotations compatible with mypy (strict mode) and editor auto-completion. All TypedDict definitions are available for import:
from licenseos.types import (
ValidationResponseData,
ValidationLicenseData,
ActivationDetailData,
HeartbeatResponseData,
UsageData,
OfflineTokenPayload,
)
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file licensesos-1.0.0.tar.gz.
File metadata
- Download URL: licensesos-1.0.0.tar.gz
- Upload date:
- Size: 19.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
924c827afb2c38ff3fc92e87200d127db3b0549ea1b91f38db6130ae2c34bf5a
|
|
| MD5 |
5c29b171f6d90cf0c6371130f468eeb8
|
|
| BLAKE2b-256 |
aec31f3a44fc848641d4a75ed604fddfbeb71fc95432d614efd8750c48dce6f8
|
Provenance
The following attestation bundles were made for licensesos-1.0.0.tar.gz:
Publisher:
publish.yml on licensesos/python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
licensesos-1.0.0.tar.gz -
Subject digest:
924c827afb2c38ff3fc92e87200d127db3b0549ea1b91f38db6130ae2c34bf5a - Sigstore transparency entry: 937119016
- Sigstore integration time:
-
Permalink:
licensesos/python-sdk@1bd0731b5a01428bdbee7aa5ec9ac462befb634d -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/licensesos
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1bd0731b5a01428bdbee7aa5ec9ac462befb634d -
Trigger Event:
release
-
Statement type:
File details
Details for the file licensesos-1.0.0-py3-none-any.whl.
File metadata
- Download URL: licensesos-1.0.0-py3-none-any.whl
- Upload date:
- Size: 19.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2fa69e2509c351689112d2415fb61544d55321b5dd535409a77a4683c20d9dde
|
|
| MD5 |
99be652af2c4433d1165b096e3199e0e
|
|
| BLAKE2b-256 |
40191d11ff2c863d53d9f7bcf6dfd946fdb5ca4e834a11e6b0c988f37e967660
|
Provenance
The following attestation bundles were made for licensesos-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on licensesos/python-sdk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
licensesos-1.0.0-py3-none-any.whl -
Subject digest:
2fa69e2509c351689112d2415fb61544d55321b5dd535409a77a4683c20d9dde - Sigstore transparency entry: 937119019
- Sigstore integration time:
-
Permalink:
licensesos/python-sdk@1bd0731b5a01428bdbee7aa5ec9ac462befb634d -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/licensesos
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@1bd0731b5a01428bdbee7aa5ec9ac462befb634d -
Trigger Event:
release
-
Statement type: