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). 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 generating a machine fingerprint. |
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 |
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, network_error (login only), or offline_login_failed. 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 reported by a different SDK or language: HWID fingerprints are not portable across SDKs, so collect the HWID from the exact SDK build that will load the file (or use the HWID override with an identifier you control)
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.