Skip to main content
This is the most important page in the documentation. Read it before shipping your integration.

License key input UX

Desktop apps

Use a dialog or popup window on first launch. After successful validation, store the key locally (e.g., in a config file, registry, or app data directory) so users don’t re-enter it every time. Provide a “Deactivate” or “Change License” option in settings.

Enter License Key

XXXX-XXXX-XXXX-XXXX
PurchaseActivate

CLI tools

Support multiple input methods:
  1. Command-line flag: --license-key XXXX-XXXX-XXXX-XXXX
  2. Environment variable: AUTHFORGE_LICENSE_KEY
  3. Config file: ~/.yourapp/config.json or ~/.yourapp/license
  4. Interactive prompt: Ask on first run if no key is found

Game mods and plugins

Read the key from a config file in the mod/plugin directory (e.g., plugins/your-mod/license.txt or config.yml). Don’t block the game’s main thread with a dialog; load the key at plugin initialization and fail gracefully.

Error handling on login

Handle each error code with an appropriate user-facing message. Never expose internal details to end users.
Never expose no_credits to end users; this is your billing issue, not theirs. Show a generic “temporarily unavailable” message.

Go

Use errors.Is against the exported authforge.Err* values (Go SDK; Error handling). If go build fails while compiling the SDK with undefined: errors, your authforge.go is out of date: the module must import the standard errors package for those sentinels. Pull the latest SDK sources (see the note on Go SDK; Installation).

Network error retry

On network failures, retry 2–3 times with exponential backoff before giving up:

Check-in and session failure handling

The onFailure callback fires with "heartbeat_failed" when an online check-in fails (if you enabled them) or when the signed session expires at the end of the grace period. You don’t have to classify failures yourself. Since SDK 1.4.0 the error passed with "heartbeat_failed" carries the error code and a transient/definitive classification, and the SDK has already acted on it by the time your callback runs:
  • Definitive failures: revoked, expired, hwid_mismatch, blocked, session_expired, malformed_request, app_disabled, and invalid_app (only when they arrive in a well-formed AuthForge error body, {"status": "failed", "error": "<code>"}), plus the SDK-local signature_mismatch, plus session_expired when the session TTL (the grace period) runs out (the Rust SDK reports this as expired, AuthForgeError::Expired, until its next major release). The SDK has already cleared the stored session (isAuthenticated() is false) and stopped check-ins when the callback runs. Retrying will not restore this session; save, tell the user, and end the session or send them back to activation.
  • Transient failures: everything else. That includes network errors, timeouts, rate_limited, system_error, no_credits, demo_quota_exceeded, app_burn_cap_reached, bad_request, invalid_key, replay_detected, revoke_requires_session, any non-JSON response (http_error_<status>, for every status), invalid JSON, unknown codes, and unexpected_response (a failed check-in whose body is not a well-formed AuthForge error, for example a proxy’s JSON error page). The SDK keeps the session and checks in again on the next interval. Once the session TTL has passed, the next transient failure is reported as a definitive session_expired instead (expired in Rust).
The SDK also retries some failures before reporting them: rate_limited (or HTTP 429 with no error code) after 2 seconds, then 5 seconds; a network failure once after 2 seconds. no_credits, demo_quota_exceeded, and app_burn_cap_reached are not retried immediately; the SDK waits for the next interval. How each SDK exposes the classification on the error passed to the callback: Callbacks run on the SDK’s background thread with no SDK lock held, so calling Logout(), IsAuthenticated(), or Login() (in your language’s naming) from the callback is safe; in C++ and Rust, destroying or dropping the client there is safe too. Hand UI work off to your UI thread.
Don’t kill the app immediately. The user could lose unsaved work. Save first. A transient check-in failure might be a momentary network blip; a definitive one still deserves a moment to save before the session ends. Have the callback signal your main thread (a threading.Event, an AbortController, a CancellationTokenSource, a std::atomic<bool>, a Go channel, an mpsc channel) and let the main thread save and exit. Exiting the process from inside the callback is a last resort, and only after saving.
Without an onFailure callback (1.4.1+), a transient background check failure writes AuthForge: background check failed (<code>); retrying next interval to stderr and check-ins continue, in every SDK. A definitive failure, including the session_expired a transient failure becomes once the session TTL has passed (Expired in Rust), still exits the process in the Python, Node.js, C#, and C++ SDKs, as does a rejected login. The Go and Rust SDKs never exit the process: a definitive failure clears the session and stops check-ins silently.

The retry window pattern

Use this for transient failures only. When a transient check-in fails, start a countdown and show a non-intrusive warning. The SDK keeps checking in on its own interval; there is no success callback, so treat a full check-in interval without another transient failure as recovery and cancel the countdown. On a definitive failure, skip the countdown: save, tell the user, and end the session.
If a hard cutoff is all you need, skip the countdown: the session TTL already works as a retry window. Transient failures keep checking in until ttl_seconds runs out, and the next failure after that arrives as a definitive session_expired.

Application-specific behavior

Always save before terminating


The onFailure callback pattern

Every SDK language follows the same pattern. Your callback receives a reason string and an optional exception. For "heartbeat_failed", branch on the error’s transient flag: a transient failure means the SDK kept the session and will check in again; a definitive one means the session is already cleared.

Poor connectivity and the grace period

The default behavior is built for applications where users may not always have internet access:
  • The initial login() (activation) call always requires network; make this clear in your app’s system requirements.
  • After activation, the SDK re-verifies the stored session signature locally during the grace period and makes no network calls unless online check-ins are enabled.
  • The grace period ends when the session TTL elapses (default 24 hours; configurable via the SDK’s ttlSeconds / session_ttl_seconds / SessionTTL option, clamped to 1 hour minimum and 7 days maximum). Revocations don’t take effect until the SDK makes a new server call.
  • With online check-ins enabled, each successful check-in refreshes the session automatically; long-running apps stay authenticated indefinitely as long as check-ins succeed, but network is required while the app runs.

Check-in failure tolerance

With online check-ins enabled, the SDK classifies every failed check-in for you (see Check-in and session failure handling):
  • Transient failures (network errors, timeouts, rate_limited, system_error, no_credits, unexpected_response, …) are reported to onFailure with a transient classification. The SDK keeps the session and continues checking in on the next interval, until the session TTL runs out; after that, the next failure is reported as session_expired (expired in Rust).
  • Definitive failures (revoked, expired, hwid_mismatch, blocked, session_expired, …) stop check-ins and invalidate the locally stored session before onFailure runs, so the grace period can’t keep the app running on it.
Transient failures keep the app running if you return normally from onFailure, or if you set no callback at all (1.4.1+), in which case the SDK writes a one-line warning to stderr and keeps checking in. Without a callback, definitive failures still exit the process in the Python, Node.js, C#, and C++ SDKs; Go and Rust clear the session and never exit. First launch always requires network. Document this in your app’s requirements.

Machines that can never connect

If a customer’s machine has no internet access at all, the grace period cannot help: it needs one online activation and lapses within 7 days. For those customers use offline license files: you mint a signed .authforge file in the cloud, deliver it out-of-band, and the SDK verifies it with loginFromFile using only your public key, app id, and the machine HWID. Do not embed the App Secret in those binaries. Keep the default online login() for everyone else, and read Offline licensing best practices before shipping the offline path.

Multi-instance and multi-window

If your app can be opened multiple times on the same machine:
  • Only one instance should authenticate. Use a lockfile or IPC mechanism to coordinate.
  • All instances share the same HWID, so extra instances won’t consume HWID slots.
  • Each login() call consumes a credit. If your app opens 10 windows, don’t call login 10 times.

Version updates

  • The same license keys work across all versions of your app. You don’t need to regenerate keys when pushing an update.
  • Use app variables to enforce a minimum version:

Anti-tampering tips

1

Protect your App Secret

Don’t log the app secret anywhere. Don’t store it in plain text in the binary. Use environment variables or encrypted configuration.
2

Authenticate early

Call login() early in your app’s startup, not lazily. Don’t let the app run unprotected code paths before authentication.
3

Minimize error details

Don’t expose internal auth failure details to the user. “The signature check failed” helps attackers; just say “Authentication failed.”
4

Don't trust the client

Assume the binary can be modified. Critical business logic that depends on license status should check variables, not just a boolean flag.
5

Enable online check-ins for high-value software

Local session re-verification during the grace period can be bypassed by freezing the system clock. Online check-ins require a valid signed response from the API on every interval.