Skip to main content

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_id is required for hardware-bound licenses. Omit it and you get invalid_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)

Source distribution for keyguard-sdk 0.1.0
File Size Uploaded
keyguard_sdk-0.1.0.tar.gz 20.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for keyguard-sdk 0.1.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page