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

# Go SDK

> Integrate AuthForge into your Go application with a lightweight module and zero external dependencies.

## Requirements

* Go 1.21 or later
* No external dependencies (standard library only)

## Installation

The module path is **`github.com/AuthForgeCC/authforge-go`**. Add a released version (pin a **`v1.x.y`** tag you rely on):

```bash theme={null}
go get github.com/AuthForgeCC/authforge-go@v1.0.1
```

Imports always use `github.com/AuthForgeCC/authforge-go`. Run `go mod tidy` after editing `go.mod`.

<Tabs>
  <Tab title="Local replace (forks / air-gap)">
    ```go theme={null}
    module example.com/myapp

    go 1.21

    require github.com/AuthForgeCC/authforge-go v0.0.0

    replace github.com/AuthForgeCC/authforge-go => ../path/to/authforge-go
    ```

    Point `replace` at a directory that contains the SDK’s `go.mod`, then run `go mod tidy`.
  </Tab>
</Tabs>

## Quick start

```go theme={null}
package main

import (
	"errors"
	"fmt"
	"os"

	"github.com/AuthForgeCC/authforge-go"
)

func main() {
	client, err := authforge.New(authforge.Config{
		AppID:         "YOUR_APP_ID",
		AppSecret:     "YOUR_APP_SECRET",
		PublicKey:     "YOUR_PUBLIC_KEY",
		HeartbeatMode: "server",
		OnFailure: func(errMsg string) {
			fmt.Fprintf(os.Stderr, "Auth failed: %s\n", errMsg)
			os.Exit(1)
		},
	})
	if err != nil {
		panic(err)
	}

	result, err := client.Login("XXXX-XXXX-XXXX-XXXX")
	if err != nil {
		switch {
		case errors.Is(err, authforge.ErrInvalidKey):
			fmt.Fprintln(os.Stderr, "Invalid license key.")
		default:
			fmt.Fprintf(os.Stderr, "Login failed: %v\n", err)
		}
		os.Exit(1)
	}

	fmt.Printf("Authenticated! Expires: %d\n", result.ExpiresIn)
	select {}
}
```

## Config reference

```go theme={null}
client, err := authforge.New(authforge.Config{
	AppID:             "YOUR_APP_ID",              // Required
	AppSecret:         "YOUR_APP_SECRET",          // Required
	PublicKey:         "YOUR_PUBLIC_KEY",          // Required - Ed25519 public key from your AuthForge dashboard (base64)
	HeartbeatMode:     "server",                   // Required: "server" or "local"
	HeartbeatInterval: 15 * time.Minute,           // Optional (any value ≥ 1s)
	APIBaseURL:        "https://auth.authforge.cc",// Optional
	OnFailure:         nil,                        // Optional: func(error string)
	RequestTimeout:    15 * time.Second,           // Optional
	SessionTTL:        0,                          // Optional: time.Duration. 0 = server default (24h). Clamped to [1h, 7d].
	HWIDOverride:      "",                         // Optional: custom identity (for example "tg:123456789")
})
```

### `SessionTTL`

Requested lifetime of the session token returned by `/auth/validate`. Leave at `0` to accept the server default (24 hours). The server clamps the final value to `[1h, 7d]`. The requested TTL is preserved across heartbeat refreshes, so long-lived apps using LOCAL mode can extend their offline window up to 7 days.

## Billing

* Each successful `Login()` or `ValidateLicense()` costs **1 credit** (one `/auth/validate` debit).
* Heartbeats cost **1 credit per 10 successful calls** (billed on every 10th heartbeat). Any `HeartbeatInterval ≥ 1s` is economically safe.
* Revocations take effect on the **next** heartbeat regardless of interval.

## Validate license (no heartbeat)

`ValidateLicense` performs the same `/auth/validate` request and signature verification as `Login`, but does **not** persist session fields on the client, start the heartbeat goroutine, or invoke `OnFailure` for validate failures (network errors still return an error; repeated network failure does not call `OnFailure` when using `ValidateLicense`).

The SDK recognizes **`AUTHFORGE_SDK_TEST_NONCE`** for integration tests only; never set it in production (it pins the validate nonce).

## Methods reference

```go theme={null}
result, err := client.Login(licenseKey)
result, err = client.ValidateLicense(licenseKey) // same validate + signatures, no heartbeat / session persistence
client.Logout()
ok := client.IsAuthenticated()
session := client.SessionData()
appVars := client.AppVariables()
licenseVars := client.LicenseVariables()
```

`Login` and `ValidateLicense` return a `LoginResult` with:

* `SessionToken`
* `ExpiresIn`
* `AppVariables`
* `LicenseVariables`
* `RequestID`

## Error handling

The Go SDK exposes sentinel errors so you can branch with `errors.Is`:

```go theme={null}
if err != nil {
	switch {
	case errors.Is(err, authforge.ErrInvalidApp):
		// invalid app credentials
	case errors.Is(err, authforge.ErrInvalidKey):
		// invalid key
	case errors.Is(err, authforge.ErrExpired):
		// license expired
	case errors.Is(err, authforge.ErrRevoked):
		// license revoked
	case errors.Is(err, authforge.ErrHwidMismatch):
		// HWID slots full
	case errors.Is(err, authforge.ErrNoCredits):
		// no credits
	case errors.Is(err, authforge.ErrBlocked):
		// blocked
	case errors.Is(err, authforge.ErrRateLimited):
		// rate limited (validate only)
	case errors.Is(err, authforge.ErrReplayDetected):
		// replay detected (validate only)
	case errors.Is(err, authforge.ErrSignatureMismatch):
		// signature verification failed
	default:
		// transport or unexpected error
	}
}
```

## Heartbeat modes

```go theme={null}
// SERVER mode
client, _ := authforge.New(authforge.Config{
	AppID:         "...",
	AppSecret:     "...",
	PublicKey:     "YOUR_PUBLIC_KEY",
	HeartbeatMode: "server",
})

// LOCAL mode
client, _ := authforge.New(authforge.Config{
	AppID:         "...",
	AppSecret:     "...",
	PublicKey:     "YOUR_PUBLIC_KEY",
	HeartbeatMode: "local",
})
```

See [Heartbeat Modes](/concepts#heartbeat-modes) for a detailed comparison.

## GitHub

Source, releases, and issues: [AuthForgeCC/authforge-go](https://github.com/AuthForgeCC/authforge-go).
