Skip to main content

keysat-licensing-client (Python)

Python client for Keysat — a Bitcoin-native self-hosted software licensing service that runs on Start9.

Verifies signed license keys offline using the issuer's public key, and (optionally) wraps the HTTP API for live validation, purchase, and free-license redemption.

Install

pip install keysat-licensing-client            # offline only
pip install keysat-licensing-client[online]    # + httpx for the online client

Requires Python 3.10+.

Five-line offline check

import time
from keysat_licensing_client import Verifier, PublicKey

ISSUER_PUBKEY_PEM = open("assets/issuer.pub").read()  # bake this into your app
verifier = Verifier(PublicKey.from_pem(ISSUER_PUBKEY_PEM))

# verify_with_time checks the signature AND rejects an expired key in one
# call (perpetual keys, expires_at == 0, never expire). Use verify() if
# you'd rather inspect an expired key than reject it.
ok = verifier.verify_with_time(key_from_user, int(time.time()))  # raises kind="expired" if past expiry
# ok.expires_at is a unix timestamp; 0 = perpetual
print(f"licensed for product {ok.product_id}, expires {ok.expires_at}")

Online check (with revocation + fingerprint binding)

from keysat_licensing_client import Client

client = Client("https://license.example.com")
r = client.validate(
    key_from_user,
    product_slug="my-product",
    fingerprint="machine-fingerprint",
)
if not r.ok:
    print("server rejected:", r.reason)
    # 'revoked', 'fingerprint_mismatch', 'not_found', 'product_mismatch', etc.

The recommended pattern is offline-first, online-augmented: do the offline Verifier.verify() at boot. If that succeeds, also do an async/background client.validate() to catch revocations and seat mismatches. If the network fails, treat it as "status unknown" — don't gate the user on your server's uptime.

Purchase flow (drives the whole BTCPay round trip)

from keysat_licensing_client import Client, StartPurchaseOptions
import webbrowser

client = Client("https://license.example.com")
session = client.start_purchase(
    "my-product",
    StartPurchaseOptions(buyer_email="bob@example.com"),
)
webbrowser.open(session.checkout_url)
license_key = client.wait_for_license(session.invoice_id)
# Save license_key wherever you decided to store keys (config dir, keychain, env).

To buy a specific tier, set StartPurchaseOptions(policy_slug=...) to a slug from list_public_policies (below); omit it to use the product's default policy.

Tier picker (public policies)

List the buyer-visible tiers for a product — same data the server's /buy/<slug> page reads, so an in-app picker stays in sync with the operator's admin setup. No auth required.

tiers = client.list_public_policies("my-product")
for p in tiers.policies:
    print(p.slug, p.name, p.price_sats, "sats", p.max_machines, "seats")
# tiers.product.entitlements_catalog maps entitlement slugs -> human labels.

Machine seat management

For seat-limited licenses (max_machines > 1), claim and release seats by fingerprint. Each returns a MachineResponse (ok, reason, active_count, max_machines).

client.activate(key, fingerprint, hostname="bob-laptop", platform="macos")
client.heartbeat(key, fingerprint)   # call periodically to keep the seat live
client.deactivate(key, fingerprint)  # release the seat

Free-license code redemption

For codes the seller created with kind free_license (no payment):

result = client.redeem_free_license(
    "my-product",
    "PRESSPASS",
)
print("redeemed:", result.license_key)

Fingerprint binding

The SDK doesn't decide WHAT to use as a fingerprint — that's a product choice. Common sources, ordered by robustness:

  • Linux: /etc/machine-id
  • macOS: ioreg -d2 -c IOPlatformExpertDevice
  • Windows: registry MachineGuid
  • Fallback: random UUID written into your app's config dir on first run

Mix in a per-product salt so fingerprints from your app can't be replayed against someone else's licensing server:

fp_input = f"{APP_NAME}|{machine_id}"
# Pass this raw string to validate(...); the server SHA-256s it before storing.

License

MIT OR Apache-2.0. See the upstream LICENSE file at github.com/keysat-xyz/keysat.

Metadata

Release files for keysat-licensing-client 0.3.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 keysat-licensing-client 0.3.0
File Size Uploaded
keysat_licensing_client-0.3.0.tar.gz 17.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for keysat-licensing-client 0.3.0
File Interpreter ABI Platform
keysat_licensing_client-0.3.0-py3-none-any.whl Python 3 none any Details

Total release size: 33.2 kB

Release files / keysat_licensing_client-0.3.0.tar.gz

Download URL keysat_licensing_client-0.3.0.tar.gz
Size 17.2 kB
Tags Source
SHA-256 checksum
How to use checksums
ed2063b8fea8497de5a5121582bc142ffe8cf2ecddf60ea91edb2cbf3d4ff15d
BLAKE2b-256 checksum
How to use checksums
ebc3d3a30afe24e972425402a6e9f6d245bc23b5fbdd9424e335c9ced3a52931
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.6

Release files / keysat_licensing_client-0.3.0-py3-none-any.whl

Download URL keysat_licensing_client-0.3.0-py3-none-any.whl
Size 16.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cc81ee229db98832a5ee2d2a207fb3587215544cafe3748c8ca04190ef7f9a64
BLAKE2b-256 checksum
How to use checksums
25ae5f98b6f94d7cbedd8b9574c62659a0140e0c1e47f29f2002a47186bf6171
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.6

Release history Release notifications | RSS feed

This release

0.3.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