This file is optimized for AI coding agents (Cursor, Copilot, Claude Code, etc.). It contains everything needed to correctly integrate AuthForge licensing into a project.
AuthForge is a license key validation service. Your app activates by sending a license key + hardware ID to POST /auth/validate; the server checks revocation, expiry, HWID binding, and credits, then returns an Ed25519-signed session with a TTL. By default the app then runs through the grace period: it keeps running on the signed session with no network calls, and a background check fails when the session TTL expires. Optionally, enable online check-ins (online_heartbeat=True): periodic POST /auth/heartbeat calls for fast revocation and concurrent-use detection. If the license is revoked or expired, the check-in fails and you handle it (typically exit the app).
There is also a separate mode for machines that can never reach the internet: offline license files (.authforge). The operator mints a signed file in the AuthForge cloud; login_from_file() verifies it locally with the app public key and the machine HWID, with zero network calls. Do not ship the App Secret in those builds (app_secret=None). Only use it when the user explicitly asks for air-gapped / offline-file licensing. The default integration is always online login() + grace period. To collect the HWID for a bound file, write an activation request (.authforge-request) with create_activation_request / write_activation_request. It is not a license, is not signed, and does not mint anything. Prefer it over printing the raw HWID.
A third, separate mode is self-serve offline activation: activate_offline(license_key, path=...) makes one online call (POST /auth/offline/activate) that turns the license key into a lifetime .authforge file bound to this machine, verifies it locally, writes it to path and authenticates the client exactly like login_from_file(). Later launches use login_from_file() with no network. Unlike air-gapped files (which still ship with app_secret=None), this mode needs the App Secret because it is an online call. The app owner must opt the app in, and only perpetual, seat-limited licenses qualify. Use it only when the user wants one-time activation followed by permanent offline use (for example a one-time-purchase desktop app); a refund or chargeback revokes the license online but cannot reach files already issued.
- 1
login()orvalidate_license()= 1 credit (one/auth/validatedebit each). - 10 online check-ins = 1 credit (billed on every 10th successful
/auth/heartbeatper license). Grace period checks are local and free. - 1 offline file mint = 1 credit (charged to the operator when the file is minted).
login_from_file()/verify_license_file()cost nothing. - 1 new offline activation = 2 credits; re-activating the same machine is free (
activate_offline(), charged to the app owner; a refused activation costs nothing). - Keep
heartbeat_intervalat>= 10seconds (900/ 15 min is the typical desktop default)./auth/heartbeatis limited to 6 requests/minute per license key, and revocations still take effect on the next check-in.
Prefer pip install authforge-sdk from PyPI (installs the cryptography dependency). Imports remain from authforge import .... For a vendored single-file layout, copy authforge.py and add cryptography to your environment. Requires Python 3.9+.
import sys
import threading
from typing import Optional
from authforge import AuthForgeClient, AuthForgeError
license_lost = threading.Event()
def on_failure(reason: str, exc: Optional[Exception]) -> None:
if reason == "heartbeat_failed" and isinstance(exc, AuthForgeError) and exc.transient:
return # network blip / rate_limited: the SDK checks in again next interval
print(f"AuthForge: {reason}", file=sys.stderr)
if exc is not None:
print(exc, file=sys.stderr)
# on_failure can run on the heartbeat thread: signal the main thread instead of exiting here.
license_lost.set()
def main() -> None:
client = AuthForgeClient(
app_id="YOUR_APP_ID",
app_secret="YOUR_APP_SECRET",
public_key="YOUR_PUBLIC_KEY", # required: base64 Ed25519 key from the dashboard
on_failure=on_failure,
)
license_key = input("Enter license key: ").strip()
if not client.login(license_key):
print("Login failed.", file=sys.stderr)
sys.exit(1)
# --- Your application code starts here ---
print("Running with a valid license.")
while not license_lost.is_set():
license_lost.wait(1.0) # replace with short units of work that check license_lost between them
# --- Your application code ends here ---
# Save the user's work here, then stop.
client.logout()
sys.exit(1)
if __name__ == "__main__":
main()This activates once and then runs through the grace period (no network) until the session TTL expires. To detect revocations quickly or catch concurrent use, add online_heartbeat=True (and optionally tune heartbeat_interval).
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
app_id |
str |
yes | n/a | Application ID |
app_secret |
str | None |
for online APIs | n/a | Application secret. Required for login / validate_license / self_ban. Pass None or "" for login_from_file only; do not ship it in air-gapped binaries. |
public_key |
str | Sequence[str] |
yes | n/a | Base64 Ed25519 public key from the dashboard. Accepts one key, a list, or a comma-separated string; the SDK trusts a signature matching any entry (key rotation) |
heartbeat_mode |
str | None |
no | None |
Deprecated shim: "LOCAL" maps to the default (grace period), "SERVER" maps to online_heartbeat=True. Emits a DeprecationWarning when provided (case-insensitive) |
heartbeat_interval |
int |
no | 900 |
Seconds between background checks (minimum 10); applies to grace period checks and online check-ins |
api_base_url |
str |
no | https://auth.authforge.cc |
API base URL |
on_failure |
Callable[[str, Optional[Exception]], None] | None |
no | None |
Called on activation/check-in/network failure (not used by validate_license, activate_offline or self_ban, which report failures without it). If omitted, a transient background check failure prints a one-line warning to stderr and check-ins continue; any other failure exits via os._exit(1) |
request_timeout |
int |
no | 15 |
HTTP timeout (seconds) |
ttl_seconds |
int | None |
no | None (server default: 86400) |
The grace period duration: how long the app keeps running on the signed session without contacting AuthForge. Server clamps to [3600, 604800] (1h to 7d); preserved across check-in refreshes. |
hwid_override |
str | None |
no | None |
Optional custom HWID/subject string. When set to a non-empty value (for example tg:123456789), the SDK sends it instead of deriving the HWID from the OS machine id. |
online_heartbeat |
bool (keyword-only) |
no | False |
Enable online check-ins: periodic POST /auth/heartbeat for fast revocation and concurrent-use detection |
For Telegram/Discord bot flows, prefer immutable IDs (tg:<user_id>, discord:<user_id>) instead of usernames.
Earlier releases required heartbeat_mode="LOCAL" or "SERVER" as the 4th argument. It is now optional and deprecated (still accepted, but emits a DeprecationWarning):
heartbeat_mode="LOCAL": remove the argument; the grace period is the default.heartbeat_mode="SERVER": replace withonline_heartbeat=True.
The attribute client.heartbeat_mode still exists for back-compat and reflects the effective policy ("SERVER" when online check-ins are enabled, "LOCAL" otherwise).
| Method | Returns | Description |
|---|---|---|
login(license_key: str) |
bool |
Activates: validates the license online, verifies signatures, starts the background check thread |
validate_license(license_key: str) |
ValidateLicenseResult |
Same validate + signatures as login; no session persistence or background checks; never calls on_failure or os._exit |
login_from_file(path_or_text: str) |
bool |
Offline mode: verifies a .authforge file locally (no network), authenticates the client, never starts background checks. Failures -> on_failure("offline_login_failed", exc) + False; never os._exit |
activate_offline(license_key, path=None) |
bool |
Self-serve offline activation: one online call -> lifetime HWID-bound .authforge file, verified locally before an atomic write to path, then applied like login_from_file. Needs the App Secret. Returns True; raises AuthForgeError on any failure; never calls on_failure or os._exit |
self_ban(*, license_key=None, session_token=None, revoke_license=True, blacklist_hwid=True, blacklist_ip=True) |
dict |
/auth/selfban: blacklists HWID/IP, optionally revokes (session mode only). Raises ValueError on any failure, a network failure as AuthForgeError network_error / timeout; never calls on_failure or os._exit |
verify_license_file(path_or_text, *, now=None) |
VerifyLicenseFileResult |
Same offline checks without changing client state |
get_offline_license() |
dict | None |
jti, expires_at, hwid_policy, … of the offline file in use; after activate_offline() also replay (True when the server returned a file this machine already had) |
get_session_kind() |
"online" | "offline" | None |
Kind of session the client holds; None when logged out |
get_hwid() |
str |
HWID this client sends; the customer reports it so the operator can mint a bound file |
create_activation_request(**kwargs) |
str |
Unsigned .authforge-request for this machine. No network, no secret, callable before login(). Hostname omitted unless include_machine_name=True |
write_activation_request(path, **kwargs) |
None |
Writes that file as UTF-8 |
logout() |
None |
Stops background checks and clears session state |
is_authenticated() |
bool |
Whether a session token is present and marked authenticated |
get_session_data() |
dict | None |
Decoded signed payload map |
get_app_variables() |
dict | None |
App-scoped variables |
get_license_variables() |
dict | None |
License-scoped variables |
Full set (KNOWN_SERVER_ERRORS): invalid_app, invalid_key, expired, revoked, hwid_mismatch, no_credits, app_burn_cap_reached, blocked, rate_limited, replay_detected, app_disabled, session_expired, revoke_requires_session, bad_request, malformed_request, demo_quota_exceeded, system_error, offline_activation_disabled, offline_activation_requires_perpetual, offline_activation_requires_seats, offline_activation_limit_reached, plus the SDK code offline_file_rejected. Unrecognized codes are passed through verbatim.
Notes:
replay_detectedis validate-only.rate_limitedcan be returned by/auth/validateand/auth/heartbeat(heartbeat is license-limited at 6/min and has no app-layer IP limit)./auth/heartbeatreturnshwid_mismatchwhen the HWID is no longer bound to the license (for example after an HWID reset) andblockedwhen the HWID/IP is blacklisted or not whitelisted.app_burn_cap_reachedmeans the app's configured credit burn cap is hit;revoke_requires_sessionmeans a pre-session self-ban tried to revoke a license (only session-authenticated self-ban can revoke).session_expiredis also raised locally when the grace period (session TTL) runs out.
Heartbeat failure classification (AuthForgeError.transient / is_transient_error()):
- Fatal (
DEFINITIVE_ERROR_CODES, the only allowlist):revoked,expired,hwid_mismatch,blocked,session_expired,malformed_request,app_disabled,invalid_app,signature_mismatch, plus theactivate_offline()codesoffline_activation_disabled,offline_activation_requires_perpetual,offline_activation_requires_seats,offline_activation_limit_reachedandoffline_file_rejected. The SDK clears the stored session (aslogout()) and stops check-ins before callingon_failure. - Transient: everything else, including
network_error,timeout,rate_limited,system_error,no_credits,demo_quota_exceeded,app_burn_cap_reached,bad_request,invalid_key, everyhttp_error_<status>,unexpected_responseand unknown codes. The session is kept and the SDK checks in again next interval; after the session TTL passes, the next transient failure becomes a fatalsession_expired. - A failed check-in is a verdict only if the body is a JSON object with
"status": "failed"and a non-empty stringerror; any other failure body becomesunexpected_response(message includes the rawstatus/error). rate_limited(or a 429 with no code) is retried after 2s then 5s; the 429 codesno_credits,demo_quota_exceeded,app_burn_cap_reachedare not retried immediately.on_failureruns on the heartbeat thread with no SDK lock held: callinglogout(),is_authenticated()orlogin()from it is safe. An in-flight check-in never restores a session afterlogout()/login().
vars_map = client.get_license_variables() or {}
tier = vars_map.get("tier")client.logout()# Step 1 (customer machine): print the HWID so the operator can bind the file to it.
print(client.get_hwid())
# Step 2 (operator): mint the .authforge file in the dashboard or via
# POST /v1/licenses/{licenseKey}/offline-files and deliver it out-of-band.
# Step 3 (customer machine): authorize with the file. No network, no check-ins.
if not client.login_from_file("license.authforge"):
# on_failure already received ("offline_login_failed", ValueError(code)) where code is one of
# bad_armor | bad_signature | unsupported_version | malformed_payload | wrong_app | expired | hwid_mismatch
sys.exit(1)Offline file error codes (in check order): bad_armor, bad_signature, unsupported_version, malformed_payload, wrong_app, expired, hwid_mismatch.
import os
import sys
from authforge import AuthForgeClient, AuthForgeError
LICENSE_PATH = os.path.join(os.path.expanduser("~"), ".myapp", "license.authforge")
client = AuthForgeClient(
app_id="YOUR_APP_ID",
app_secret="YOUR_APP_SECRET", # REQUIRED here: activate_offline is an online call
public_key="YOUR_PUBLIC_KEY",
on_failure=lambda reason, exc: None, # receives ("offline_login_failed", exc) from login_from_file
)
if not (os.path.exists(LICENSE_PATH) and client.login_from_file(LICENSE_PATH)):
# No file yet, or it failed (e.g. hwid_mismatch after a hardware change): activate once.
key = input("License key: ").strip()
try:
client.activate_offline(key, path=LICENSE_PATH) # verifies, writes atomically, authenticates
except AuthForgeError as exc:
if exc.code == "offline_activation_limit_reached":
print("This license is used on its maximum number of machines. Contact the vendor.")
elif exc.transient:
print(f"Could not activate right now ({exc.code}); try again.")
else:
print(f"Activation failed: {exc.code}")
sys.exit(1)- Returns
True; raisesAuthForgeErroron any failure. Missing App Secret raises the usualValueError("app_secret is required ...")before any request. - On failure nothing is written to
path(an existing file is left untouched), the call does not authenticate the client (an existing session is kept, not logged out), andon_failure/os._exitare never called. - Codes:
offline_activation_disabled(app not opted in),offline_activation_requires_perpetual,offline_activation_requires_seats(shared/unlimited keys cannot self-serve),offline_activation_limit_reached(machine cap used; HWID resets do not free it; tell the user to contact the vendor), serverhwid_mismatch(no free seat),offline_file_rejected(SDK: the returned file failed local verification; the verifier's reason is instr(exc)),file_write_failed(transient). Existing codes (invalid_key,revoked,no_credits,rate_limited, ...) keep their meaning and classification. - Billing: 1 new offline activation = 2 credits (app owner); re-activating the same machine is free and returns the identical file.
Server error codes appear as AuthForgeError (a ValueError subclass) in the exc passed to on_failure: exc.code is the code (e.g. invalid_key, revoked, hwid_mismatch), str(exc) the message. Reasons are login_failed, heartbeat_failed, or offline_login_failed; network failures arrive once as login_failed or heartbeat_failed with exc.code network_error or timeout. activate_offline() and self_ban() never call on_failure: they raise, a network failure as AuthForgeError with that same code. For heartbeat_failed, exc is always an AuthForgeError; use exc.transient to tolerate outages.
import sys
import threading
from typing import Optional
from authforge import AuthForgeError
license_lost = threading.Event() # the main loop checks this, saves work, then exits
def on_failure(reason: str, exc: Optional[Exception]) -> None:
if reason == "heartbeat_failed" and isinstance(exc, AuthForgeError) and exc.transient:
return # network / rate_limited / no_credits / unexpected_response: SDK retries next interval
code = exc.code if isinstance(exc, AuthForgeError) else exc
if code in {"invalid_key", "expired", "revoked", "hwid_mismatch", "blocked"}:
print(f"License issue: {code}", file=sys.stderr)
license_lost.set() # sys.exit() on the heartbeat thread would only end that threadIf the main thread blocks instead of looping, _thread.interrupt_main() from the callback raises KeyboardInterrupt there so try / finally cleanup runs. os._exit(1) inside the callback is a last resort: it skips all cleanup, so save the user's work first.
- Do not hardcode the app secret as a plain string literal in source: use environment variables or encrypted config
- Do not embed the App Secret in air-gapped /
login_from_filebuilds: passNoneor""; verification only needs app id + public key. (Self-serveactivate_offline()builds are the exception: that call is online and needs the secret.) - Do not call
activate_offline()on every launch: trylogin_from_file(path)first and activate only when there is no valid file - Do not confuse the server's
hwid_mismatchfromactivate_offline()(no free seat) withoffline_file_rejected(the returned file failed local verification) - Do not skip the
on_failurecallback: without it, transient check-in failures only print a stderr warning, but definitive ones (and a failedlogin()) terminate the process viaos._exit(1)without your cleanup - Do not call
os._exit()fromon_failureas the normal shutdown path: signal the main thread (athreading.Event, or_thread.interrupt_main()) so it can save work and exit cleanly - Do not treat every
heartbeat_failedas a network blip or every one as fatal: checkexc.transient. Fatal failures have already cleared the session, so do not keep the app running on it - Do not call
login()on every app action: call it once at startup; the grace period (or online check-ins) handles the rest - Do not pass
heartbeat_modein new code: it is deprecated. Use the default grace period, oronline_heartbeat=Truewhen you need fast revocation or concurrent-use detection - Do not treat the grace period as persistent offline licensing: it is session continuation after one successful online activation, and revocations are only picked up at the next online validate or check-in
- Do not reach for
login_from_file()unless the user explicitly needs air-gapped / offline-file licensing: the default is onlinelogin()+ grace period - Do not expect an online revoke to disable an offline file that is already on a customer machine: the file stays valid until its own
expiresAt; prefer short expiries and HWID-bound files - Do not mint or accept
hwid.mode: "any"files casually: anyone who copies an unbound file has a working license - Do not call
login_from_file()with another app's public key or app id: the file is rejected withbad_signature/wrong_appby design - Do not try to build
.authforgefiles client-side: only the AuthForge cloud holds the signing key; there is no BYO issuer - Do not call
self_ban()or any other online method afterlogin_from_file(): an offline session has no server session (get_session_kind()is"offline"), soself_ban()raisesValueError("offline_session")without contacting the server and online check-ins never start; machines that can reach AuthForge should use onlinelogin() - Do not bind an offline file to an HWID collected before upgrading to 2.0: HWID v2 replaced the old fingerprint, so pre-2.0 HWIDs no longer match; collect it again with
create_activation_request()(since 2.0 any AuthForge SDK gives the same HWID for the same app id and machine)
The HWID is sha256("authforge-hwid-v2|" + app_id + "|" + machine_id) (lowercase hex), where machine_id is the trimmed, lowercased OS machine id: macOS IOPlatformUUID, Linux /etc/machine-id (then /var/lib/dbus/machine-id), Windows MachineGuid (64-bit registry view). Without a valid OS id the SDK falls back to legacy:<pre-2.0 fingerprint>. For containers, use hwid_override.
hwid_vectors.json is generated by authforge-node/generate_hwid_vectors.mjs. Regenerating it means copying the file unmodified into all six SDK repos in the same change.
activation_request_vectors.json is generated by authforge-node/generate_activation_request_vectors.mjs. Regenerating it means copying the file unmodified into all six SDK repos and platform/frontend/src/test/fixtures/activation_request_vectors.json in the same change. The vector sdk value is a frozen encoding fixture, not the live SDK tag.