keyguard-sdk (Python)
The official Python SDK for KeyGuard — a license
verification service for people who sell software and don't want licensing
to become their second job. MIT licensed, Python 3.8+, one dependency
(requests).
Quickstart
1. Install
pip install keyguard-sdk # verify + hardware fingerprint
pip install "keyguard-sdk[offline]" # adds offline license-file verification (ed25519)
2. Verify a license
from keyguard import KeyGuardClient
from keyguard.fingerprint import get_hardware_id
client = KeyGuardClient(api_key="live_...") # your product API key
hardware_id = get_hardware_id(product_key="live_...")
result = client.verify("PERS-ONA0-VALID-0001", hardware_id=hardware_id)
if result.valid:
print("Licensed until", result.expires_at)
That's a real request: POST {base_url}/api/v1/licenses/verify with
{"api_key", "license_key", "hardware_id"} in the JSON body, and a flat
{"valid", "environment", "expires_at", ...} back. Two things trip people
up on day one:
hardware_idis required for hardware-bound licenses. Omit it and you getinvalid_payload, not a helpful error page.- The API key goes in the request body, not a header. That is the API contract.
3. React to the outcome
import time
from keyguard import VerifyStatus
if result.status is VerifyStatus.EXPIRED:
# vendor_info carries the seller's contact details + renewal link
show_renewal_screen(result.vendor_info["renewal_url"])
elif result.status is VerifyStatus.MACHINE_LIMIT:
print(f"All {result.max_machines} machine slots are in use")
elif result.status is VerifyStatus.RATE_LIMITED:
time.sleep(result.retry_after or 60)
A runnable end-to-end demo lives in examples/demo.py:
KEYGUARD_API_KEY=live_... KEYGUARD_LICENSE_KEY=KG-... python examples/demo.py
Handling things that go wrong
API-level outcomes never raise — they come back as a VerifyResult with
valid=False and a status you can switch on. Transport problems (DNS,
TLS, timeouts, a proxy returning an HTML error page) raise
keyguard.KeyGuardError.
import time
from keyguard import KeyGuardClient, KeyGuardError, VerifyStatus
client = KeyGuardClient(api_key="live_...")
try:
result = client.verify("KG-XXXX-XXXX", hardware_id=hardware_id)
except KeyGuardError as exc:
# Network is down, DNS is broken, server returned junk.
# Your call whether this fails open or closed — pick deliberately.
print(f"Could not reach the licensing server: {exc}")
else:
if result.status is VerifyStatus.INVALID_KEY:
print("That key doesn't belong to this product.")
elif result.status is VerifyStatus.RATE_LIMITED:
# The edge limiter is a composite of per-IP and per-license buckets.
# Retry-After is only present when the limiter knows the cooldown —
# fall back to your own backoff when it isn't.
time.sleep(result.retry_after or 60)
elif result.valid:
run_app()
The full status taxonomy:
| Status | Meaning | Extra attributes |
|---|---|---|
VALID |
License active; machine activated implicitly on first verify | environment, expires_at, custom_fields |
EXPIRED |
License past expiration | expired_at, vendor_info (name, phone, email, renewal_url, whatsapp_url, telegram_url) |
MACHINE_LIMIT / DOMAIN_LIMIT |
Activation slots exhausted | max_machines |
RATE_LIMITED |
HTTP 429 from the edge limiter | retry_after (seconds, or None) |
BLOCKED |
License blocked by the vendor | — |
INVALID_KEY |
Key not found for this product | — |
INVALID_API_KEY |
Wrong or archived API key | — |
INVALID_PAYLOAD |
Missing required fields (did you forget hardware_id?) |
— |
DOMAIN_REQUIRED / DOMAIN_NOT_SUPPORTED / INVALID_DOMAIN |
Web-product binding mismatch | — |
UNAVAILABLE |
Vendor account deleted | — |
UNKNOWN |
Unrecognized reason; the raw body is in result.raw |
— |
Web products
For products with platform_type = "web", pass domain instead of
hardware_id:
client.verify("KG-XXXX-XXXX", domain="mysite.com")
Activate / deactivate
There are no standalone activate/deactivate endpoints — this is deliberate,
not a gap in the SDK. The first successful verify() for a new
hardware_id consumes a machine slot; slots are freed from the vendor
dashboard. client.activate() and client.deactivate() exist only to raise
NotSupportedError with that explanation, so you don't have to learn it
from a 404.
Hardware fingerprints
get_hardware_id(product_key) reads the most stable OS identifier it can
find — Windows MachineGuid (with WMI UUID and disk serial as fallbacks),
Linux /etc/machine-id (dbus machine-id, dmidecode UUID), macOS
IOPlatformUUID — salts it with your product key, and returns a SHA-256 hex
digest. The raw identifier never leaves the device, and the salt means the
same machine produces a different fingerprint for every vendor, so
fingerprints can't be correlated across products.
We intentionally prefer stable identifiers over unique ones: a fingerprint that changes after an OS update quietly eats a customer's activation slot, which is a worse failure than two VMs colliding.
Offline license files
For machines that will never touch the internet, vendors can issue signed offline license files (envelope v2). Verification is fully local with the product's public key:
from keyguard.offline import verify_offline_file
payload = verify_offline_file("license.kglic", public_key_b64="<product public key>")
print(payload["license_key"], payload["expiration_date"])
Legacy HMAC envelopes are rejected with LegacyEnvelopeError — that format
was sunset because the symmetric secret had to ship inside the app to be
checked offline, which is to say it checked nothing. HMAC files are still
validated server-side.
When NOT to use this
Save yourself the integration work if any of these describe you:
- You need floating/concurrent-seat licenses. KeyGuard counts activated machines, not simultaneous sessions. A checkout/checkin seat pool is a different product.
- You want enforcement decided on your own server. Verification happens against KeyGuard's API; if your compliance posture forbids a third-party dependency in the licensing path, run your own license server instead.
- You only sell SaaS. If users authenticate to your backend anyway, your existing auth and billing already answer "is this account allowed" — you don't need per-machine licensing at all.
- Offline machines must be revocable. Offline files are irrevocable by construction; cryptography can't fix physics.
How it works under the hood
Online verify is a single signed-in-spirit JSON round trip: your API key in
the body authenticates the product, the server checks the license state, the
machine slot ledger, and the rate-limit buckets, and answers with a flat
{"valid", ...} payload.
Offline files are an ed25519 envelope: the vendor's private key signs the canonical JSON of the license claims (UTF-8, sorted keys, compact separators — byte-identical to the server's canonical form), and this SDK verifies the signature with the product's public key. The hardware fingerprint that binds a license to a machine is a salted SHA-256 of the OS machine identifier; only the digest ever crosses the network.
Development
cd sdks/python
PYTHONPATH=. python -m unittest discover -s tests -v
License
MIT — see LICENSE.
Metadata
Release files for keyguard-sdk 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| keyguard_sdk-0.1.0.tar.gz | 20.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| keyguard_sdk-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 36.8 kB
Release files / keyguard_sdk-0.1.0.tar.gz
| Download URL | keyguard_sdk-0.1.0.tar.gz |
|---|---|
| Size | 20.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f7ce48f8444a134604b4f9b28fcf1f31fdc329ce15fd9dd34650999b146be5bc
|
|
BLAKE2b-256 checksum How to use checksums |
4278629e4635b8ee78cb9f0deba2115b39355cce645e340404f1f469aaa4a7f0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.12
|
Release files / keyguard_sdk-0.1.0-py3-none-any.whl
| Download URL | keyguard_sdk-0.1.0-py3-none-any.whl |
|---|---|
| Size | 16.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a6eb4bdd9b9638e7b6bf3c2be574347074d2aaae460f3585cf30d9e6fa0820bf
|
|
BLAKE2b-256 checksum How to use checksums |
82e213f2e2351fbf9c366d24a57f6e4ce688e93891f15b90076f774a1d254ace
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.12
|