ExisOne Python SDK
Embed our cross-platform Python library to generate hardware IDs, activate and validate licenses, and send support tickets.
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
- Hardware ID:
generate_hardware_id()— cross-platform fingerprint (salted SHA-256), or returnshardware_id_overridewhen set New in 0.8.0 - Activate:
activate(key, email, hardware_id, product_name, version=None)→ returnsActivationResultwith success/error details and server version info - Validate:
validate(key, hardware_id, product_name=None, version=None)→ returnsValidationResultwith status, expiration, features - Deactivate:
deactivate(key, hardware_id, product_name)(requires the same hardware) - Generate key:
generate_activation_key(product_name, email, plan_id=None, validity_days=None)(requiresgeneratepermission) - Support ticket:
send_support_ticket(product_name, email, subject, message)(requiresemailpermission) - Offline Validation:
validate_offline(offline_code, hardware_id)→ validates locally without server connectionvalidate_smart(key_or_code, hardware_id, product_name=None)→ auto-detects online/offline, falls back gracefullydeactivate_smart(key_or_code, hardware_id, product_name)→ opportunistic server sync
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
- Customer runs your app and sees their Hardware ID (from
generate_hardware_id()) - Customer contacts you with their Hardware ID (email/phone/support ticket)
- You generate an Offline Activation Code in the License Keys page
- Customer enters the code into your app
- Your app validates locally using
validate_offline()orvalidate_smart()
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
| Method | Required Permission | Notes |
|---|---|---|
validate() | verify | Server enforces in /api/license/validate |
generate_activation_key() | generate | Publisher operations |
deactivate() | None | Allowed only when server-side hardware matches bound license |
send_support_ticket() | email | Sends email via tenant/global SMTP |
activate() | None | Current 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
| Variable | Description |
|---|---|
EXISONE_BASEURL | Default base URL if not specified in options |
Dependencies
requests— HTTP clientcryptography— RSA signature verification for offline validation
Compatibility
- Python 3.8, 3.9, 3.10, 3.11, 3.12, 3.13
- Windows, Linux, macOS
- Full type hints (PEP 561 compliant)
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")
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
- 403 verify permission required: Add
verifyto the token in Access Tokens UI. - Activation key not found in this tenant: Token and key must belong to same tenant.
- 400 Bad Request on activation: Ensure product name is correct.
- version_outdated: Client version is below the minimum required. Check
minimum_required_versionin the response and prompt user to upgrade.
Changelog
- 0.8.0: New Hardware ID Override —
New
hardware_id_overridefield onExisOneClientOptions; when set,generate_hardware_id()returns it verbatim and skips local fingerprinting. Designed for Docker containers and multi-tenant servers where each user should consume one seat on a corporate license. Minor breaking change:generate_hardware_id()is now an instance method (was@staticmethod). See the new Docker AI Prompt. - 0.7.0: Initial Python SDK release with full feature parity to .NET SDK.
See Also
- .NET SDK Documentation — If you're building with C#/.NET
- Node.js SDK Documentation — For Node.js/Electron apps
- Swift SDK Documentation — For macOS/iOS apps
- REST API Documentation — Direct API integration
- PyPI Package Page