Python client for Keysat — a Bitcoin-native self-hosted software licensing service that runs on Start9. Verifies signed license keys offline and wraps the HTTP API for purchase, redemption, and revocation checks.
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file keysat_licensing_client-0.3.0.tar.gz.
File metadata
- Download URL: keysat_licensing_client-0.3.0.tar.gz
- Upload date:
- Size: 17.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ed2063b8fea8497de5a5121582bc142ffe8cf2ecddf60ea91edb2cbf3d4ff15d
|
|
| MD5 |
4dd231e247150b0978a93abcbc1e35df
|
|
| BLAKE2b-256 |
ebc3d3a30afe24e972425402a6e9f6d245bc23b5fbdd9424e335c9ced3a52931
|
File details
Details for the file keysat_licensing_client-0.3.0-py3-none-any.whl.
File metadata
- Download URL: keysat_licensing_client-0.3.0-py3-none-any.whl
- Upload date:
- Size: 16.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.6
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cc81ee229db98832a5ee2d2a207fb3587215544cafe3748c8ca04190ef7f9a64
|
|
| MD5 |
f3b9d3033b4b44db62a3c62ec384e125
|
|
| BLAKE2b-256 |
25ae5f98b6f94d7cbedd8b9574c62659a0140e0c1e47f29f2002a47186bf6171
|