Skip to main content

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. Has code (machine-readable string like LICENSE_NOT_FOUND) and status_code (HTTP status).
  • NetworkError -- A network-level failure occurred (timeout, DNS, connection refused). Has an optional cause with 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 a ValidationResult with valid=False and an error dict. ApiError is 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:// and https:// 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

licensesos-1.0.0.tar.gz (19.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

licensesos-1.0.0-py3-none-any.whl (19.1 kB view details)

Uploaded Python 3

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

Hashes for licensesos-1.0.0.tar.gz
Algorithm Hash digest
SHA256 924c827afb2c38ff3fc92e87200d127db3b0549ea1b91f38db6130ae2c34bf5a
MD5 5c29b171f6d90cf0c6371130f468eeb8
BLAKE2b-256 aec31f3a44fc848641d4a75ed604fddfbeb71fc95432d614efd8750c48dce6f8

See more details on using hashes here.

Provenance

The following attestation bundles were made for licensesos-1.0.0.tar.gz:

Publisher: publish.yml on licensesos/python-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for licensesos-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2fa69e2509c351689112d2415fb61544d55321b5dd535409a77a4683c20d9dde
MD5 99be652af2c4433d1165b096e3199e0e
BLAKE2b-256 40191d11ff2c863d53d9f7bcf6dfd946fdb5ca4e834a11e6b0c988f37e967660

See more details on using hashes here.

Provenance

The following attestation bundles were made for licensesos-1.0.0-py3-none-any.whl:

Publisher: publish.yml on licensesos/python-sdk

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page