Keysat 94654f6526 v0.2.0 — policy_slug on start_purchase + list_public_policies
Mirrors the TS SDK 0.2.0 + Rust SDK 0.2.0 changes for parity across
all four language clients.

StartPurchaseOptions gains `policy_slug: str | None`. New
`Client.list_public_policies(product_slug)` returns a
`PublicPoliciesResponse` carrying the buyer-visible tier list (slug,
name, price_sats, entitlements, recurring/trial flags, "Most popular"
flag). Same data the licensing service's /buy/<slug> page reads
server-side — public endpoint, no auth.

  client.start_purchase("recap", StartPurchaseOptions(
      policy_slug="pro",
      buyer_email="buyer@example.com",
  ))

  tiers = client.list_public_policies("recap")
  for p in tiers.policies:
      print(p.slug, p.name, p.price_sats, p.entitlements)

Backwards compatible — existing positional / kwarg callers continue
to work since policy_slug defaults to None.
2026-05-09 09:08:51 -05:00
2026-05-07 10:39:37 -05:00
2026-05-07 10:39:37 -05:00
2026-05-07 10:39:37 -05:00
2026-05-07 10:39:37 -05:00
2026-05-07 10:39:37 -05:00

keysat-licensing-client (Python)

Python client for Keysat — a self-hosted Bitcoin-paid software licensing server 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

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))

ok = verifier.verify(key_from_user)  # raises LicensingError on bad sig
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).

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}"
# The SDK SHA-256s this for you when you pass it to validate(...).

License

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

S
Description
No description provided
Readme MIT 61 KiB
Languages
Python 100%