Skip to content

Python SDK

The Python 3.9+ package is dycrypt-license, imported as dycrypt_license. Its only runtime dependency is cryptography.

Until the package is published to PyPI, install it from the DYCRYPT SDK checkout:

Terminal window
pip install /path/to/dycrypt/sdk/python

The drop-in module at sdk/python/dropin/dycrypt_license.py exposes the same API.

import sys
from dycrypt_license import fingerprint
if "--fingerprint" in sys.argv:
print(fingerprint())
raise SystemExit(0)

Use this before issuing an Offline or offline-activated Hybrid licence.

from dycrypt_license import LicenseVerifier, OnlinePolicy
with open("product-public-key.pem", encoding="utf-8") as handle:
public_key_pem = handle.read()
verifier = LicenseVerifier(
public_key_pem,
"your-product-key",
issuer_url="https://app.dycrypt.com",
org_slug="your-org-slug",
check_id="<the product's check id>",
check_token="dycc_...",
online_policy=OnlinePolicy.ADVISORY,
timeout=5.0,
state_path="/var/lib/your-app/dycrypt-state.json",
)

Omit the issuer settings and state_path for Offline. Build the verifier once and share it safely between threads.

check_id and check_token are required alongside issuer_url; the constructor raises if you supply one without the others, rather than failing later on every check. Copy all four values from the product’s Quick Start Guide. The token ships inside your application, so treat it as identifying rather than secret.

Every answer from DYCRYPT is signed with the product’s key, and the verifier checks that signature before recording a successful check. require_receipt defaults to True and there is no reason to change it — an issuer that cannot sign is exactly the attack it exists to stop.

state_path preserves successful-check timestamps across application restarts. Protect that file with the same operating-system permissions as other application state.

with open("customer.lic", encoding="utf-8") as handle:
result = verifier.verify(handle.read())
enforce(result)
with open("customer.lic", encoding="utf-8") as handle:
result = verifier.check(handle.read())
enforce(result)

check() is blocking. Run it at startup and from the background schedule your application owns. The SDK performs local verification before any request is made.

A renewed licence answers check() with result.valid == False and result.online.status == "ACTIVE" — the old string no longer verifies, but the issuer confirms a current one exists. If the product has turned on self-service fetch, ask for it directly instead of prompting the customer to install a new file by hand:

current = verifier.fetch_latest(license_text)
if current is not None:
with open("customer.lic", "w", encoding="utf-8") as handle:
handle.write(current)

fetch_latest() presents the licence you already hold as the credential — the current one, or an earlier, superseded term for the same licence id — and returns None if the issuer refuses, is unreachable, or answers with something that does not itself verify. It runs the result back through verify() before returning it, so a caller never has to trust the transport. Call it only when you decide to, never automatically from inside check(). It is opt-in per product and refused once the licence you present is past its own expiry and grace; see the licence check API for the full protocol.

from dycrypt_license import LicenseStatus
def enforce(result):
if result.status == LicenseStatus.ACTIVE:
return
if result.status == LicenseStatus.GRACE:
print("Licence is inside its expiry grace period")
return
if result.status == LicenseStatus.STALE:
raise SystemExit("Reconnect to refresh the licence status")
if result.status == LicenseStatus.PENDING:
raise SystemExit("Licence starts at %s" % result.starts_at)
raise SystemExit("Unlicensed (%s): %s" % (result.status, result.reason))

ACTIVE and GRACE are the only statuses where result.valid is true.

max_users = result.integer("maxUsers", 5)
edition = result.string("edition", "standard")
export_enabled = result.boolean("exportEnabled", False)
features = result.string_list("features", [])

Typed accessors never coerce a wrong JSON type. They return the fallback you provide.