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)
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:
current_seats— number of active activationsmax_seats— configured limit;Nonemeans unlimitedhas_capacity—Trueif at least one more activation will succeedis_unlimited—Truewhen there is no seat limitremaining— computed seats still available,Nonewhen unlimited, clamped at 0
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.10.0: New Corporate Seat Usage API —
New
get_seat_usage(activation_key)method returns aSeatUsagedataclass with current seat consumption, max seats, remaining capacity, and unlimited flag for corporate licenses. Additive change. - 0.9.0: Fix Hardware ID Stability —
Removed
uuid.getnode()(which returned a random value when no hardware MAC was available, producing a different ID on every call)./proc/cpuinfois now filtered to drop volatile lines (cpu MHz,bogomips, etc.) so repeated reads on the same Linux machine produce the same fingerprint. MAC enumeration now skips virtual / VPN / VM adapters by name prefix (docker,br-,veth,tun, etc.) so container and VPN lifecycle events do not cause fingerprint churn. - 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