Integration Guide
Everything an integrator needs: the gate, activation, trials, enforcement, revocation, errors.
The two roles
┌──────────────────────┐ public key + license.lic ┌──────────────────────┐
│ ISSUER (you) │ ───────────────────────────▶ │ CUSTOMER APP │
│ holds private key │ ◀──────── HWID + payment ─── │ embeds public key │
└──────────────────────┘ └──────────────────────┘
The private key never leaves your issuing machine. The customer app ships with only the public key — compiled in, pinned by fingerprint, never stored where it can be swapped.
LicenseGate — the whole API most apps need
gate = LicenseGate(
app_name="MyProduct", # per-machine namespace (must be unique per product)
public_key=VENDOR_PUBLIC_KEY, # embedded constant
expected_fingerprint=FINGERPRINT,# startup self-check
config_dir=cfg_dir,
trial_days=14,
enable_watchdog=True, # background re-validation
interval_seconds=600,
on_violation=lambda reason, detail: shutdown(),
)
status = gate.start() # startup: creates trial on first run
if not status: # LicenseStatus is truthy when the app may run
show_block_screen(status.message)
# later, after a purchase:
result = gate.activate_token(user_pasted_text) # or gate.activate_file(path)
if result.ok:
toast(f"Licensed to {result.payload.client}")
gate.check() # re-resolve state any time
gate.status_summary() # JSON-serializable dict for UIs / logs
gate.deactivate() # archive license (falls back to trial/expired)
LicenseStatus fields
| Field | Meaning |
|---|---|
state | licensed · licensed_invalid · trial_active · trial_expired · tampered · missing · error |
ok / bool() | True when the app may run (licensed or trial active). |
days_left | Days of validity or trial remaining. |
client | Customer name from the license payload. |
message | Human-readable sentence ready to display. |
to_dict() | JSON-safe summary for logs and support tickets. |
licensed_invalid instead of quietly reverting to an active trial.In-app activation (your UI, our validation)
# Method A — paste into YOUR text field:
result = gate.activate_token(text_entry.get().strip())
# Method B — file picker:
result = gate.activate_file(chosen_path)
Both validate signature → expiry → HWID → revocation → clock before storing. Rejections return canonical reasons your UI can map to messages: see the error table below.
Trials
Trial management is inside the gate. On first run gate.start() issues a self-signed, machine-bound trial license protected by the same clock guard as real licenses:
- Editing the trial file → signature failure →
tampered - Copying another machine's trial → HWID mismatch →
tampered - Rolling back the clock → high-water mark →
clock_tampering - Deleting the DB / trial data → start date ratcheted in redundant marks → trial does NOT restart
Revocation
# publish a signed list from your machine:
rizmi license revoke --license-id deploy-001 --private-key k.pem -o crl.json
# hand it to validators:
validator = LicenseValidator(pub_key_pem, revocation_list=open("crl.json").read())
A CRL with a bad signature is rejected loudly (revocation_list_invalid), never silently ignored.
Error codes
| Code | Cause | User guidance |
|---|---|---|
missing | No license found. | Activate or buy. |
expired | Past exp + grace_days. | Renew. |
hwid_mismatch | Bound to another machine. | Re-issue for this machine's HWID. |
tampered | Signature/row integrity failed. | Re-install; contact support. |
clock_tampering | Clock rolled back/frozen. | Fix system clock. |
revoked | ID on a revocation list. | Contact vendor. |
decode_error | Wrong key / corrupt token. | Check public key matches issuer. |
unsupported_schema | Newer schema than this build. | Upgrade the library. |
Grace period
Licenses carry grace_days: after exp, validation keeps succeeding and flags in_grace_period so your UI can warn ("renew within N days") while still enforcing eventually. Hard-stop happens at exp + grace_days.
Long-running processes
Servers and daemons expire while running. Enable the watchdog (enable_watchdog=True) so the gate re-validates every interval and fires on_violation(reason, detail) on expiry/tamper — your callback decides what "stop serving" means.
Multi-app machines
Every store is namespaced by app_name: separate directory, separate HMAC key, separate fallback files. Two py-rizmi apps cannot read or overwrite each other's state, and a DB copied between apps verifies under neither. Optionally share one clock ratchet across all your products (RizmiConfig(shared_clock_namespace=True)) so any app observing "tomorrow" protects them all.
Migrating old installs
rizmi migrate-to-sqlite run --config-dir ~/.config/MyApp --app-name MyApp
Idempotent; imports legacy trial key material so pre-existing trials keep their original dates.