> ## Documentation Index
> Fetch the complete documentation index at: https://docs.authforge.cc/llms.txt
> Use this file to discover all available pages before exploring further.

# SDK Integration Best Practices

> How to handle license key input, errors, heartbeat failures, offline mode, and anti-tampering in your AuthForge-protected application.

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.

<div className="not-prose my-6 max-w-md rounded-xl border border-zinc-200 bg-white p-5 shadow-sm dark:border-zinc-700 dark:bg-zinc-950">
  <p className="text-center text-base font-semibold text-zinc-900 dark:text-zinc-100">Enter License Key</p>

  <div className="mt-4 rounded-md border border-zinc-300 bg-zinc-50 px-3 py-2 text-center font-mono text-sm tracking-wide text-zinc-800 dark:border-zinc-600 dark:bg-zinc-900 dark:text-zinc-200">
    XXXX-XXXX-XXXX-XXXX
  </div>

  <div className="mt-5 flex flex-wrap items-center justify-between gap-3 text-sm">
    <span className="rounded-md border border-zinc-300 px-3 py-2 text-zinc-700 dark:border-zinc-600 dark:text-zinc-300">Purchase</span>

    <span className="rounded-md px-3 py-2 font-medium text-white" style={{ backgroundColor: "#7F77DD" }}>
      Activate
    </span>
  </div>
</div>

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

```python theme={null}
import os

key = (
    args.license_key
    or os.environ.get("AUTHFORGE_LICENSE_KEY")
    or load_from_config()
    or input("Enter license key: ")
)
```

### 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.

| SDK error           | Cause                                     | What to show the user                                                                   |
| ------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------- |
| `invalid_app`       | Your App ID or Secret is wrong            | "Authentication failed. Please contact support." (This is a developer error.)           |
| `invalid_key`       | The license key doesn't exist             | "Invalid license key. Please check and try again."                                      |
| `revoked`           | The license was revoked                   | "This license has been deactivated. Contact support."                                   |
| `expired`           | The license has expired                   | "Your license has expired. \[Renew link]"                                               |
| `hwid_mismatch`     | All HWID slots are full                   | "This license is already in use on another device. Contact support to reset."           |
| `no_credits`        | App developer ran out of credits          | "Authentication service temporarily unavailable. Please try again later."               |
| `blocked`           | HWID or IP is blacklisted                 | "Authentication failed. Please contact support."                                        |
| `malformed_request` | Request body is missing or not valid JSON | This is a developer integration error. Fix request serialization.                       |
| `system_error`      | Unexpected server-side failure            | "Authentication service temporarily unavailable. Please try again." Retry with backoff. |
| Network error       | No connectivity                           | "Could not connect to authentication server. Check your internet connection."           |

<Warning>
  Never expose `no_credits` to end users; this is your billing issue, not theirs. Show a generic "temporarily unavailable" message.
</Warning>

### Go

Use [`errors.Is`](https://pkg.go.dev/errors#Is) against the exported `authforge.Err*` values ([Go SDK; Error handling](/sdk/go#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](/sdk/go#installation)).

### Network error retry

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

```python theme={null}
import time

MAX_RETRIES = 3
for attempt in range(MAX_RETRIES):
    if client.login(license_key):
        break
    if attempt < MAX_RETRIES - 1:
        time.sleep(2 ** attempt)  # 1s, 2s, 4s
else:
    print("Could not connect to authentication server.")
    exit(1)
```

***

## Heartbeat failure handling

<Warning>
  **Don't kill the app immediately.** The user could lose unsaved work. A heartbeat failure might be a momentary network blip.
</Warning>

### The grace period pattern

When a heartbeat fails, start a countdown. Show a non-intrusive warning. If the next heartbeat succeeds, cancel the countdown.

```python theme={null}
import threading

grace_timer = None
GRACE_PERIOD = 300  # 5 minutes

def on_failure(reason, exception):
    global grace_timer
    if reason == "heartbeat_failed":
        if grace_timer is None:
            grace_timer = threading.Timer(GRACE_PERIOD, force_shutdown)
            grace_timer.start()
            show_warning("License verification failed. "
                        "The app will close in 5 minutes unless connectivity is restored.")
    elif reason == "login_failed":
        force_shutdown()

def on_heartbeat_success():
    global grace_timer
    if grace_timer:
        grace_timer.cancel()
        grace_timer = None
        hide_warning()

def force_shutdown():
    save_application_state()
    show_message("License verification failed. Your work has been saved.")
    exit(1)
```

### Application-specific behavior

| App type                                 | On heartbeat failure                                                                                    |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| **Creative tools** (editors, DAWs, IDEs) | Auto-save the project. Show a dialog with a "Retry" button. Only close after N consecutive failures.    |
| **Games**                                | Pause the game and show a modal overlay. Don't close to desktop.                                        |
| **CLI / batch tools**                    | Log the failure. Continue current operation. Exit after it completes if still failing.                  |
| **Services / daemons**                   | Log with severity. Continue running through transient failures. Only shut down after sustained failure. |

### Always save before terminating

```python theme={null}
def on_failure(reason, exception):
    save_application_state()  # Always save first
    log_failure(reason, exception)  # Log for debugging
    show_user_message(reason)  # Inform the user
    # Then exit after a delay or user acknowledgment
```

***

## The `onFailure` callback pattern

Every SDK language follows the same pattern. Your callback receives a `reason` string and an optional exception:

<CodeGroup>
  ```python Python theme={null}
  def on_failure(reason: str, exception: Exception | None):
      save_state()

      if reason == "login_failed":
          show_error("Authentication failed.")
      elif reason == "heartbeat_failed":
          show_warning("License check failed. Retrying...")

      log(f"AuthForge failure: {reason}", exception)
      # Return normally; the SDK will exit if you don't handle it

  client = AuthForgeClient(
      app_id="...",
      app_secret="...",
      heartbeat_mode="SERVER",
      on_failure=on_failure,
  )
  ```

  ```csharp C# theme={null}
  void OnFailure(string reason, Exception? exception)
  {
      SaveState();

      switch (reason)
      {
          case "login_failed":
              ShowError("Authentication failed.");
              break;
          case "heartbeat_failed":
              ShowWarning("License check failed. Retrying...");
              break;
      }

      Log($"AuthForge failure: {reason}", exception);
  }

  var client = new AuthForgeClient(
      appId: "...",
      appSecret: "...",
      heartbeatMode: "SERVER",
      onFailure: OnFailure
  );
  ```

  ```cpp C++ theme={null}
  void on_failure(const std::string& reason, const std::exception* ex) {
      save_state();

      if (reason == "login_failed") {
          show_error("Authentication failed.");
      } else if (reason == "heartbeat_failed") {
          show_warning("License check failed. Retrying...");
      }

      log("AuthForge failure: " + reason, ex);
  }

  authforge::AuthForgeClient client(
      "...", "...", "SERVER",
      900, "https://auth.authforge.cc", on_failure
  );
  ```
</CodeGroup>

***

## Offline and poor connectivity

### When to use LOCAL heartbeat mode

LOCAL mode is designed for applications where users may not always have internet access:

* The initial `login()` call **always** requires network; make this clear in your app's system requirements.
* After login, LOCAL mode verifies the stored signature locally without network calls.
* In SERVER mode, each successful heartbeat extends the session automatically; long-running apps stay authenticated indefinitely as long as heartbeats succeed.
* In LOCAL mode, the session expires when the session token's TTL elapses (default 24 hours; configurable via the SDK's `ttlSeconds` / `session_ttl_seconds` / `SessionTTL` option, up to 7 days). Revocations don't take effect until the SDK makes a new server call.

### SERVER mode tolerance

Even in SERVER mode, individual heartbeat failures don't immediately trigger shutdown (the SDK handles this internally). The failure callback fires when the SDK determines the session is truly invalid; not on every transient network blip.

**First launch always requires network.** Document this in your app's requirements.

***

## 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.

```python theme={null}
import os
import sys

LOCK_FILE = "/tmp/yourapp.lock"

def acquire_lock():
    if os.path.exists(LOCK_FILE):
        return False  # Another instance is running
    with open(LOCK_FILE, "w") as f:
        f.write(str(os.getpid()))
    return True

def release_lock():
    if os.path.exists(LOCK_FILE):
        os.remove(LOCK_FILE)

if not acquire_lock():
    # Secondary instance; skip auth, connect to primary via IPC
    connect_to_primary()
else:
    # Primary instance; authenticate
    if client.login(license_key):
        run_app()
    release_lock()
```

***

## 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](/features/variables) to enforce a minimum version:

```python theme={null}
if client.login(license_key):
    min_version = client.app_variables.get("minVersion")
    if min_version and current_version < min_version:
        print(f"Please update to version {min_version} or later.")
        exit(1)
```

***

## Anti-tampering tips

<Steps>
  <Step title="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.
  </Step>

  <Step title="Authenticate early">
    Call `login()` early in your app's startup, not lazily. Don't let the app run unprotected code paths before authentication.
  </Step>

  <Step title="Minimize error details">
    Don't expose internal auth failure details to the user. "The signature check failed" helps attackers; just say "Authentication failed."
  </Step>

  <Step title="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.
  </Step>

  <Step title="Use SERVER heartbeat mode for high-value software">
    LOCAL mode can be bypassed by freezing the system clock. SERVER mode requires a valid signed response from the API.
  </Step>
</Steps>
