ExisOne Python SDK

Embed our cross-platform Python library to generate hardware IDs, activate and validate licenses, and send support tickets.

Package: exisone-client · Version: 0.10.0 · Python: 3.8+
View on PyPI →

Install

pip install exisone-client

Initialize

from exisone import ExisOneClient, ExisOneClientOptions

options = ExisOneClientOptions(
    base_url="https://your-api-host",  # must be https
    access_token="exo_at_<public>_<secret>",  # create in Access Tokens UI
    offline_public_key=None,  # optional: set for offline license validation
    hardware_id_override=None  # optional: per-user/per-tenant ID (e.g. Docker)
)

client = ExisOneClient(options)

# Optional: change base URL later
client.with_base_url("https://another-host")

# Library version
sdk_version = client.get_version()

base_url can also be set via environment variable EXISONE_BASEURL.

For offline license validation, set offline_public_key to your tenant's RSA public key (PEM format). Obtain this from the Crypto Keys page.

Capabilities

Quick Start

from exisone import ExisOneClient, ExisOneClientOptions

# Initialize
options = ExisOneClientOptions(
    base_url="https://www.exisone.com",
    access_token="exo_at_xxx_yyy"
)
client = ExisOneClient(options)

# 1) Hardware ID (store locally)
hwid = client.generate_hardware_id()

# 2) Activation with version (user enters key/email)
result = client.activate(
    activation_key=activation_key,
    email=user_email,
    hardware_id=hwid,
    product_name="MyProduct",
    version="1.0.0"
)
if not result.success:
    if result.error_code == "version_outdated":
        print(f"Please upgrade to version {result.minimum_required_version}")
    else:
        print(result.error_message)

# 3) Validate on app start (requires 'verify' permission)
result = client.validate(
    activation_key=activation_key,
    hardware_id=hwid,
    product_name="MyProduct",
    version="1.0.0"
)

if result.status == "version_outdated":
    print(f"Update required: minimum version is {result.minimum_required_version}")
elif result.is_valid:
    print(f"Licensed until: {result.expiration_date}")
    print(f"Features: {', '.join(result.features)}")

# 4) Optional: Deactivate from the same hardware
success = client.deactivate(activation_key, hwid, "MyProduct")

# 5) (Publisher tooling) Generate a new key for a product
key = client.generate_activation_key("MyProduct", "user@example.com", plan_id=1)

# 6) Submit support ticket from client
client.send_support_ticket("MyProduct", user_email, "Subject", "Message body")

Offline License Validation

For customers without internet access, generate offline activation codes from the License Keys page by providing their Hardware ID. These codes are RSA-signed and validated locally.

Setup

# Configure with your tenant's RSA public key for offline validation
options = ExisOneClientOptions(
    base_url="https://your-api-host",
    access_token="your-token",
    offline_public_key="""-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
-----END PUBLIC KEY-----"""
)
client = ExisOneClient(options)

Smart Validation (Recommended)

Auto-detects online vs offline keys based on format. Falls back to offline validation if server is unreachable.

hwid = client.generate_hardware_id()

# Works with both online keys (XXXX-XXXX-XXXX-XXXX) and offline codes
result = client.validate_smart(license_key_or_offline_code, hwid, "MyProduct")

if result.is_valid:
    print(f"Licensed until: {result.expiration_date}")
    print(f"Offline mode: {result.was_offline}")
    print(f"Features: {', '.join(result.features)}")
else:
    print(f"Invalid: {result.error_message}")

Direct Offline Validation (No Network)

# Validate completely offline - no server calls
result = client.validate_offline(offline_code, hwid)

if result.is_valid:
    print(f"Product: {result.product_name}")
    print(f"Expires: {result.expiration_date}")
elif result.is_expired:
    print("License expired")
elif result.hardware_mismatch:
    print("Wrong machine - license bound to different hardware")
else:
    print(f"Invalid: {result.error_message}")

Deactivation with Opportunistic Sync

# Tries to notify server, but succeeds locally even if offline
result = client.deactivate_smart(key_or_code, hwid, "MyProduct")
print(f"Success: {result.success}, Server notified: {result.server_notified}")

Offline Workflow

  1. Customer runs your app and sees their Hardware ID (from generate_hardware_id())
  2. Customer contacts you with their Hardware ID (email/phone/support ticket)
  3. You generate an Offline Activation Code in the License Keys page
  4. Customer enters the code into your app
  5. Your app validates locally using validate_offline() or validate_smart()
Security: Offline codes are RSA-SHA256 signed and embed: product ID, hardware ID, expiration date, email, and feature flags. They cannot be forged or transferred to other machines.

Error Handling

from exisone import ExisOneClient, ExisOneClientOptions
import requests

client = ExisOneClient(options)

try:
    result = client.validate(activation_key, hardware_id)
except requests.HTTPError as e:
    print(f"HTTP error: {e.response.status_code}")
except requests.RequestException as e:
    print(f"Network error: {e}")

Permissions

MethodRequired PermissionNotes
validate()verifyServer enforces in /api/license/validate
generate_activation_key()generatePublisher operations
deactivate()NoneAllowed only when server-side hardware matches bound license
send_support_ticket()emailSends email via tenant/global SMTP
activate()NoneCurrent API does not require a specific scope

API Reference

# Version
get_version() -> str

# Hardware
generate_hardware_id() -> str

# Activation
activate(activation_key: str, email: str, hardware_id: str,
         product_name: str, version: str = None) -> ActivationResult

# Validation (Online)
validate(activation_key: str, hardware_id: str,
         product_name: str = None, version: str = None) -> ValidationResult

# Validation (Offline)
validate_offline(offline_code: str, hardware_id: str) -> OfflineValidationResult
validate_smart(key_or_code: str, hardware_id: str,
               product_name: str = None) -> SmartValidationResult

# Deactivation
deactivate(activation_key: str, hardware_id: str, product_name: str) -> bool
deactivate_smart(key_or_code: str, hardware_id: str,
                 product_name: str) -> DeactivationResult

# Generation (publisher)
generate_activation_key(product_name: str, email: str,
                        plan_id: int = None, validity_days: int = None) -> str

# Support
send_support_ticket(product_name: str, email: str, subject: str, message: str) -> None

# Configuration
with_base_url(base_url: str) -> ExisOneClient

# Result Types
@dataclass
class ActivationResult:
    success: bool
    error_code: str | None
    error_message: str | None
    server_version: str | None
    minimum_required_version: str | None
    license_data: str | None

@dataclass
class ValidationResult:
    is_valid: bool
    status: str
    expiration_date: datetime | None
    features: list[str]
    server_version: str | None
    minimum_required_version: str | None

@dataclass
class OfflineValidationResult:
    is_valid: bool
    error_message: str | None
    product_name: str | None
    expiration_date: datetime | None
    features: list[str]
    is_expired: bool
    hardware_mismatch: bool

@dataclass
class SmartValidationResult:
    is_valid: bool
    status: str
    expiration_date: datetime | None
    features: list[str]
    was_offline: bool
    error_message: str | None
    product_name: str | None
    server_version: str | None
    minimum_required_version: str | None

@dataclass
class DeactivationResult:
    success: bool
    server_notified: bool
    error_message: str | None

Environment Variables

VariableDescription
EXISONE_BASEURLDefault base URL if not specified in options

Dependencies

Compatibility

Corporate Seat Usage New in 0.10.0

For corporate (multi-seat) licenses, get_seat_usage() returns the current consumption and remaining capacity so your app can show customers how much of their license is in use and warn them before they hit the cap. Only meaningful for corporate licenses — standard single-device licenses return 404 and the method raises.

usage = client.get_seat_usage(corporate_key)

if usage.is_unlimited:
    print(f"{usage.current_seats} seats in use (unlimited)")
else:
    print(f"{usage.current_seats} / {usage.max_seats} seats in use")
    if usage.remaining is not None and usage.remaining <= 5:
        print(f"WARNING: only {usage.remaining} seat(s) remaining")
    if not usage.has_capacity:
        print("License is FULL. Release a seat or upgrade before adding new users.")

Fields on SeatUsage:

Hardware ID Override (Docker / Multi-Tenant) New in 0.8.0

By default, generate_hardware_id() derives a fingerprint from the local machine (CPU, MAC, machine-id, etc.). In environments where that fingerprint is unreliable or shared across users — most commonly Docker containers, multi-tenant servers, or hosted SaaS deployments — set hardware_id_override on the options to a stable per-user identifier (e.g. a UUID stored in your user record). When set, generate_hardware_id() returns it verbatim and skips all local fingerprinting.

Combined with a Corporate (multi-seat) license, this lets each user inside the container consume one seat against the same license key:

import uuid
from exisone import ExisOneClient, ExisOneClientOptions

# One client instance per signed-in user
user_pseudo_hwid = current_user.license_hardware_id  # UUID stored on the user row

client = ExisOneClient(ExisOneClientOptions(
    base_url="https://your-api-host",
    access_token="exo_at_PUBLIC_SECRET",
    hardware_id_override=user_pseudo_hwid,
))

# generate_hardware_id() now returns the override (no /etc/machine-id reads)
hwid = client.generate_hardware_id()

# Activate consumes one seat on the corporate license for this user
result = client.activate(
    activation_key=corporate_key,
    email=current_user.email,
    hardware_id=hwid,
    product_name="MyProduct",
)

# On every container start (or periodic re-check), validate the same hwid
validation = client.validate(corporate_key, hwid, "MyProduct")

# When a user is removed, release their seat
client.deactivate(corporate_key, hwid, "MyProduct")
Persist the override. Generate the per-user UUID once and store it on the user record. If you regenerate it on each request you will consume a new seat every time.
API change in 0.8.0: generate_hardware_id() is now an instance method (was @staticmethod in 0.7.0). Calls on an instance — client.generate_hardware_id() — work unchanged. Calls on the class itself need to import the helper directly: from exisone import generate_hardware_id.

For a complete Docker integration walkthrough (including admin trial check on container startup), see the Docker AI Prompt.

Common Errors

Changelog

See Also