# Error Codes Reference Source: https://docs.authforge.cc/api/errors Complete reference of error codes returned by the AuthForge Developer API and public auth endpoints. ## Developer API errors All Developer API errors return a JSON object with an `error` code and a human-readable `message`: ```json theme={null} { "error": "not_found", "message": "Resource not found" } ``` | HTTP Status | Error Code | Description | | ----------- | ----------------- | ------------------------------------------------------------------------------------------------------------ | | 400 | `bad_request` | Invalid or missing request parameters. Check the request body and query parameters. | | 401 | `invalid_api_key` | Missing, malformed, or revoked API key. Verify your `Authorization: Bearer` header. | | 402 | `no_credits` | Insufficient credits to complete the operation. Purchase more credits in the dashboard. | | 403 | `forbidden` | The target app or license doesn't belong to your account. | | 404 | `not_found` | The license key, webhook, or endpoint doesn't exist. | | 429 | `rate_limited` | Too many requests. Back off and retry with exponential backoff. Burst limit: 100 req/s, sustained: 50 req/s. | | 500 | `internal_error` | Unexpected server error. Retry the request. If persistent, contact support. | *** ## Public auth errors (SDK) The public auth endpoints (`/auth/validate`, `/auth/heartbeat`, and `/auth/selfban`) return errors as real HTTP status codes with a JSON body of the form `{"status": "failed", "error": ""}`: ```json theme={null} { "status": "failed", "error": "invalid_key" } ``` The HTTP status code matches the class of failure: | HTTP Status | Error codes | | ----------- | --------------------------------------------------------------------- | | 400 | `bad_request` | | 401 | `invalid_app`, `invalid_key`, `session_expired`, `replay_detected` | | 403 | `blocked`, `app_disabled`, `hwid_mismatch`, `revoke_requires_session` | | 410 | `expired`, `revoked` | | 429 | `rate_limited`, `no_credits`, `app_burn_cap_reached` | | 500 | `system_error` | Successful responses return `200` with `status: "success"`. Public auth requests are protected by two layers of throttling: * API Gateway stage throttling: burst `200`, sustained `100 req/s` (default for every route on this API: `/auth/validate`, `/auth/heartbeat`, and `/auth/selfban`) * Application-level per-minute limits on `/auth/validate` only: * `30/min` per IP * `5/min` per license key `/auth/heartbeat` is **not** IP rate-limited at the application layer. A flood of heartbeats only burns the victim's credits, so a per-IP limit would add no security value and would interfere with legitimate high-frequency clients (e.g., 1 Hz server-side apps). Additional application limits on `/auth/selfban`: `10/min` per IP (all self-ban requests), and `3/min` per license key for **pre-session** requests that send `licenseKey` + app credentials. When a validate request exceeds either limit, the response is `HTTP 429` with: ```json theme={null} { "status": "failed", "error": "rate_limited" } ``` Successful `/auth/validate` responses include the `X-RateLimit-Remaining` header (the lower of the remaining IP and per-license validate quotas). **`/auth/heartbeat` success responses do not include this header**: application-level rate buckets apply only where documented above. The official SDKs parse both the HTTP status code and the JSON `error` field, so `429` responses surface as `rate_limited` automatically; no special handling is needed in your integration. ### Validate errors (`/auth/validate`) | Error Code | Description | User-facing guidance | | ---------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------- | | `invalid_app` | The App ID or App Secret is incorrect, or the app has been deactivated. | This is a developer configuration error. Show "Authentication failed." | | `app_disabled` | The app owner's account is suspended. | Show "Authentication service temporarily unavailable." | | `invalid_key` | The license key doesn't exist or doesn't belong to this app. | Show "Invalid license key. Please check and try again." | | `revoked` | The license has been revoked. | Show "This license has been deactivated. Contact support." | | `expired` | The license has passed its expiration date. | Show "Your license has expired." with a renewal link if applicable. | | `hwid_mismatch` | All HWID slots are full and the current device's HWID doesn't match any bound device. | Show "This license is already in use on another device. Contact support to reset." | | `no_credits` | The app developer's account has insufficient credits. | Show "Authentication service temporarily unavailable. Please try again later." Never expose this to end users. | | `app_burn_cap_reached` | The app hit its configured hourly/daily credit burn cap. | Show "Authentication service temporarily unavailable. Please try again later." | | `blocked` | The device's HWID or IP is blacklisted, or not on a required whitelist. | Show "Authentication failed. Contact support." | | `rate_limited` | Too many requests from this IP or license key. Back off and retry after 60 seconds. | Retry with exponential backoff and jitter; wait at least 60 seconds before sustained retries. | | `replay_detected` | The nonce has already been used. Generate a fresh nonce for each request (SDKs do this automatically). | This is a developer integration error if you are not using an official SDK. | | `bad_request` | The request body is malformed (missing fields, invalid types). Includes a `details` array. | This is a developer integration error. | ### Heartbeat errors (`/auth/heartbeat`) | Error Code | Description | User-facing guidance | | ---------------------- | -------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `session_expired` | The session token is invalid or has expired. The SDK should re-authenticate. | Prompt the user to re-enter their license key or restart the app to log in again. | | `revoked` | The license has been revoked since the last heartbeat. | Show "This license has been deactivated. Contact support." | | `expired` | The license has expired since the last heartbeat. | Show "Your license has expired." with a renewal link if applicable. | | `app_disabled` | The app owner's account has been suspended. | Show "Authentication service temporarily unavailable." | | `no_credits` | The app developer's account has insufficient credits. Returned on the billing milestone (every 10th successful heartbeat). | Show "Authentication service temporarily unavailable. Please try again later." Never expose this to end users. | | `app_burn_cap_reached` | The app hit its configured hourly/daily credit burn cap during heartbeat billing. | Show "Authentication service temporarily unavailable. Please try again later." | | `bad_request` | The request body is malformed (missing fields, invalid types). Includes a `details` array. | This is a developer integration error. | | `system_error` | An unexpected failure occurred while processing the request (for example, credit deduction). | Show "Authentication service temporarily unavailable." Retry with backoff. | `rate_limited` and `replay_detected` are **never** returned from `/auth/heartbeat`. Heartbeats are not IP rate-limited and do not enforce nonce replay detection; replay protection is provided by the signed, short-lived session token. ### Self-ban errors (`/auth/selfban`) | Error Code | Description | User-facing guidance | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- | | `revoke_requires_session` | A pre-session self-ban request attempted `revokeLicense: true`. License revoke is only allowed for session-authenticated self-ban requests. | This is a developer integration issue. Retry with `revokeLicense: false`, or call self-ban after login with `sessionToken`. | | `invalid_app` | `appId` or `appSecret` is invalid for pre-session self-ban. | Treat as integration/configuration error. | | `invalid_key` | The provided pre-session `licenseKey` does not belong to the app. | Treat as integration/configuration error. | | `session_expired` | `sessionToken` is missing/invalid/expired for post-session self-ban. | Re-authenticate before requesting self-ban. | | `replay_detected` | Pre-session nonce was reused. | Always send a fresh nonce (official SDKs do this automatically). | | `rate_limited` | Too many self-ban requests: **10/min per IP** (all modes), **3/min per license key** (pre-session requests with `licenseKey` only). | Back off and retry with jittered exponential backoff. | | `expired` | License is expired. | Prompt renewal/support workflow. | | `revoked` | License was already revoked. | Treat as already-locked state. | | `app_disabled` | App or app owner is suspended/paused. | Show generic service-unavailable messaging. | | `system_error` | Unexpected server error. | Retry with backoff; alert support if persistent. | *** ## SDK client-side errors In addition to server errors, the SDK may detect issues locally before or after a server response: | Error | Description | | -------------------- | --------------------------------------------------------------------------------------------------- | | `nonce_mismatch` | The nonce in the server response doesn't match the one sent in the request. Possible replay attack. | | `signature_mismatch` | The Ed25519 signature on the server response is invalid. Possible tampering. | | HTTP/network errors | Connection timeout, DNS failure, or non-200 HTTP status. The SDK retries internally before failing. | These errors trigger the `onFailure` callback with the `"login_failed"` or `"heartbeat_failed"` reason. *** ## Best practices for error handling 1. **Never expose internal error codes to end users.** Map them to user-friendly messages. 2. **Log the actual error code** for debugging, but show generic messages to users. 3. **Retry on transient errors** (`no_credits` from your account, network errors) with exponential backoff. 4. **Don't retry on permanent errors** (`invalid_key`, `revoked`, `hwid_mismatch`); prompt the user to take action instead. 5. **Handle `no_credits` gracefully**: this is a billing issue on your end, not the user's problem. See [SDK Best Practices](/sdk/best-practices#error-handling-on-login) for detailed error handling guidance with code examples. # Licenses API Reference Source: https://docs.authforge.cc/api/licenses Create, list, update, and delete license keys programmatically via the AuthForge Developer API. ## Create licenses Create one or more license keys for an application. ### Request body | Field | Type | Required | Description | | -------------- | -------------- | -------- | ------------------------------------------------ | | `appId` | string | Yes | The application to issue licenses for | | `quantity` | integer | No | Number of licenses to create (1–100, default 1) | | `expiresAt` | string \| null | No | ISO 8601 expiration date, or `null` for lifetime | | `maxHwidSlots` | integer | No | Max devices per license (1–16, default 1) | | `label` | string | No | Optional label for your records (max 128 chars) | ### Example: minimal ```bash theme={null} curl -X POST https://api.authforge.cc/v1/licenses \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{"appId": "YOUR_APP_ID", "quantity": 1}' ``` ### Example: all options ```bash theme={null} curl -X POST https://api.authforge.cc/v1/licenses \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "appId": "YOUR_APP_ID", "quantity": 5, "expiresAt": "2026-12-31T23:59:59Z", "maxHwidSlots": 2, "label": "Stripe order #12345" }' ``` ### Response (201) ```json theme={null} { "licenses": [ { "licenseKey": "A3K9-BFWX-7NP2-QHDT", "appId": "YOUR_APP_ID", "status": "active", "expiresAt": "2026-12-31T23:59:59Z", "maxHwidSlots": 2, "createdAt": "2026-04-09T22:00:00.000Z" } ] } ``` ### Errors | HTTP | Code | Cause | | ---- | ------------- | -------------------------------------- | | 400 | `bad_request` | Invalid or missing parameters | | 402 | `no_credits` | Insufficient credits | | 403 | `forbidden` | The app doesn't belong to your account | *** ## List licenses List all licenses for an application with cursor-based pagination. ### Query parameters | Param | Type | Required | Description | | -------- | ------- | -------- | ------------------------------------------ | | `appId` | string | Yes | The application to list licenses for | | `limit` | integer | No | Results per page (1–200, default 50) | | `cursor` | string | No | Pagination cursor from a previous response | ### Example ```bash theme={null} curl "https://api.authforge.cc/v1/licenses?appId=YOUR_APP_ID&limit=50" \ -H "Authorization: Bearer af_live_your_key" ``` ### Response (200) ```json theme={null} { "licenses": [ { "licenseKey": "A3K9-BFWX-7NP2-QHDT", "appId": "YOUR_APP_ID", "status": "active", "expiresAt": "2026-12-31T23:59:59Z", "maxHwidSlots": 2, "hwidList": [], "createdAt": "2026-04-09T22:00:00.000Z" } ], "cursor": null } ``` ### Pagination If `cursor` is non-null, more results are available. Pass it as a query parameter in the next request: ```bash theme={null} # First page curl "https://api.authforge.cc/v1/licenses?appId=YOUR_APP_ID&limit=100" \ -H "Authorization: Bearer af_live_your_key" # Next page (use cursor from previous response) curl "https://api.authforge.cc/v1/licenses?appId=YOUR_APP_ID&limit=100&cursor=eyJsaW..." \ -H "Authorization: Bearer af_live_your_key" ``` *** ## Get a license Get full details for a single license including HWID bindings and label. ### Example ```bash theme={null} curl "https://api.authforge.cc/v1/licenses/A3K9-BFWX-7NP2-QHDT" \ -H "Authorization: Bearer af_live_your_key" ``` ### Response (200) ```json theme={null} { "licenseKey": "A3K9-BFWX-7NP2-QHDT", "appId": "YOUR_APP_ID", "status": "active", "expiresAt": "2026-12-31T23:59:59Z", "maxHwidSlots": 2, "hwidList": ["abc123-hwid-fingerprint"], "hwid": null, "label": "Stripe order #12345", "createdAt": "2026-04-09T22:00:00.000Z" } ``` ### Errors | HTTP | Code | Cause | | ---- | ----------- | ------------------------------------------------ | | 403 | `forbidden` | The license's app doesn't belong to your account | | 404 | `not_found` | License key doesn't exist | *** ## Update a license Perform an action on a license: revoke, activate, extend expiration, or reset HWID bindings. ### Request body | Field | Type | Required | Description | | ----------- | ------ | ------------ | --------------------------------------------------------------- | | `action` | string | Yes | One of: `revoke`, `activate`, `extend`, `reset-hwid` | | `expiresAt` | string | For `extend` | New ISO 8601 expiration date (required when action is `extend`) | ### Revoke a license Immediately disables the license. End users will fail validation on their next heartbeat or login attempt. ```bash theme={null} curl -X PUT https://api.authforge.cc/v1/licenses/A3K9-BFWX-7NP2-QHDT \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{"action": "revoke"}' ``` ### Re-activate a revoked license Restores a previously revoked license back to active status. ```bash theme={null} curl -X PUT https://api.authforge.cc/v1/licenses/A3K9-BFWX-7NP2-QHDT \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{"action": "activate"}' ``` ### Extend expiration Sets a new expiration date. Use for subscription renewals or granting additional time. ```bash theme={null} curl -X PUT https://api.authforge.cc/v1/licenses/A3K9-BFWX-7NP2-QHDT \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{"action": "extend", "expiresAt": "2027-06-30T23:59:59Z"}' ``` ### Reset HWID bindings Clears all bound hardware IDs, allowing the license to be activated on new devices. ```bash theme={null} curl -X PUT https://api.authforge.cc/v1/licenses/A3K9-BFWX-7NP2-QHDT \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{"action": "reset-hwid"}' ``` ### Response (200) ```json theme={null} { "ok": true, "license": { "licenseKey": "A3K9-BFWX-7NP2-QHDT", "appId": "YOUR_APP_ID", "status": "revoked", "expiresAt": "2026-12-31T23:59:59Z", "maxHwidSlots": 2, "hwidList": [], "createdAt": "2026-04-09T22:00:00.000Z" } } ``` *** ## Delete a license Permanently delete a license. This cannot be undone. ### Example ```bash theme={null} curl -X DELETE https://api.authforge.cc/v1/licenses/A3K9-BFWX-7NP2-QHDT \ -H "Authorization: Bearer af_live_your_key" ``` ### Response (200) ```json theme={null} { "ok": true } ``` ### Errors | HTTP | Code | Cause | | ---- | ----------- | ------------------------------------------------ | | 403 | `forbidden` | The license's app doesn't belong to your account | | 404 | `not_found` | License key doesn't exist | # Developer API Overview Source: https://docs.authforge.cc/api/overview Authenticate, manage licenses, and automate your AuthForge integration with the server-to-server REST API. The AuthForge Developer API lets you manage licenses programmatically from your own backend. Use it to create licenses after Stripe payments, revoke access, manage variables, configure webhooks, and more. ## Base URL ``` https://api.authforge.cc/v1/ ``` All endpoints are prefixed with `/v1/`. ## Authentication Every request must include your API key in the `Authorization` header as a Bearer token: ```bash theme={null} Authorization: Bearer af_live_your_key_here ``` API keys are created in the [Dashboard](https://app.authforge.cc/dashboard) under **Settings → API Keys**. Keys are prefixed with `af_live_` for identification. Each key is scoped to your account and can manage licenses across all of your applications; the target app is specified per-request via the `appId` field or URL parameter. API keys are shown only once when created. Store them securely. If compromised, delete the key in the dashboard and create a new one. ## Rate limits | Limit | Value | | --------- | ------------------- | | Burst | 100 requests/second | | Sustained | 50 requests/second | If you exceed the rate limit, you'll receive `HTTP 429` with the Developer API error shape (`error` + `message`). Back off and retry with exponential backoff. Public SDK endpoints on `auth.authforge.cc` (`/auth/validate`, `/auth/heartbeat`, `/auth/selfban`) use **different** limits and a different JSON body on failure: `{ "status": "failed", "error": "rate_limited" }`. `/auth/validate` has extra per-IP and per-license application limits; `/auth/heartbeat` is not IP rate-limited at the application layer. See [Error Codes Reference](/api/errors#public-auth-errors-sdk). ## Credits The Developer API itself does not consume credits. Credits are only consumed by SDK authentication (validate and heartbeat calls). See [Managing Credits](/best-practices/credit-management) for details. ## Response format All responses return JSON. Successful responses include the requested data directly: ```json theme={null} { "licenses": [...], "cursor": null } ``` Error responses include an `error` code and a human-readable `message`: ```json theme={null} { "error": "not_found", "message": "Resource not found" } ``` ## Pagination List endpoints use cursor-based pagination. If more results are available, the response includes a `cursor` value. Pass it as a query parameter in the next request: ```bash theme={null} # First page curl "https://api.authforge.cc/v1/licenses?appId=YOUR_APP_ID&limit=100" \ -H "Authorization: Bearer af_live_your_key" # Next page curl "https://api.authforge.cc/v1/licenses?appId=YOUR_APP_ID&limit=100&cursor=eyJsaW..." \ -H "Authorization: Bearer af_live_your_key" ``` When `cursor` is `null`, you've reached the last page. ## Endpoints ### Licenses | Method | Path | Description | | ------ | ------------------------------------------------------------ | --------------------------------------- | | POST | [`/v1/licenses`](/api/licenses#create-licenses) | Create one or more licenses | | GET | [`/v1/licenses`](/api/licenses#list-licenses) | List licenses for an app | | GET | [`/v1/licenses/:licenseKey`](/api/licenses#get-a-license) | Get a single license | | PUT | [`/v1/licenses/:licenseKey`](/api/licenses#update-a-license) | Revoke, activate, extend, or reset HWID | | DELETE | [`/v1/licenses/:licenseKey`](/api/licenses#delete-a-license) | Permanently delete a license | ### Variables | Method | Path | Description | | ------ | ---------------------------------------------------------------------------- | --------------------- | | GET | [`/v1/apps/:appId/variables`](/api/variables#get-app-variables) | Get app variables | | PUT | [`/v1/apps/:appId/variables`](/api/variables#set-app-variables) | Set app variables | | GET | [`/v1/licenses/:licenseKey/variables`](/api/variables#get-license-variables) | Get license variables | | PUT | [`/v1/licenses/:licenseKey/variables`](/api/variables#set-license-variables) | Set license variables | ### Webhooks | Method | Path | Description | | ------ | -------------------------------------------------------------------------- | ----------------- | | POST | [`/v1/apps/:appId/webhooks`](/api/webhooks#create-a-webhook) | Create a webhook | | GET | [`/v1/apps/:appId/webhooks`](/api/webhooks#list-webhooks) | List webhooks | | PUT | [`/v1/apps/:appId/webhooks/:webhookId`](/api/webhooks#update-a-webhook) | Update a webhook | | DELETE | [`/v1/apps/:appId/webhooks/:webhookId`](/api/webhooks#delete-a-webhook) | Delete a webhook | | POST | [`/v1/apps/:appId/webhooks/:webhookId/test`](/api/webhooks#test-a-webhook) | Send a test event | ### Security | Method | Path | Description | | ------ | --------------------------------------------------------------------------- | ---------------------- | | GET | [`/v1/apps/:appId/security`](/api/security#get-security-config) | Get security config | | PUT | [`/v1/apps/:appId/security`](/api/security#update-security-config) | Update security config | | POST | [`/v1/apps/:appId/security/blacklist`](/api/security#add-to-blacklist) | Add to blacklist | | DELETE | [`/v1/apps/:appId/security/blacklist`](/api/security#remove-from-blacklist) | Remove from blacklist | | POST | [`/v1/apps/:appId/security/whitelist`](/api/security#add-to-whitelist) | Add to whitelist | | DELETE | [`/v1/apps/:appId/security/whitelist`](/api/security#remove-from-whitelist) | Remove from whitelist | ## Error codes See the [Error Codes Reference](/api/errors) for a complete list of error codes across all endpoints. # Security API Reference Source: https://docs.authforge.cc/api/security Manage HWID and IP blacklists and whitelists via the AuthForge Developer API. Control which devices and IP addresses can authenticate with your application. *** ## Response signature model (Ed25519) AuthForge signs `/auth/validate` and `/auth/heartbeat` success responses with a per-app Ed25519 private key. * SDKs verify `signature` against the exact base64 `payload` string using your app's `publicKey`. * `appSecret` authenticates validate requests, but is **not** used for response signature verification. * Response shape includes `keyId` so clients can identify which signing key version produced the signature. ### Success response shape ```json theme={null} { "status": "success", "payload": "", "signature": "", "keyId": "" } ``` This prevents network-level payload forgery without access to your app's private signing key. *** ## Tamper self-ban endpoint (public auth) When your app detects tampering (anti-debug, runtime integrity checks, patch detection), you can call: Trigger a self-ban request against the public auth host (`https://auth.authforge.cc`). ### Request modes `/auth/selfban` supports two request styles: 1. **Pre-session**: `appId`, `appSecret`, `licenseKey`, `hwid`, `nonce` 2. **Post-session**: `appId`, `sessionToken`, `hwid` Common optional flags: | Field | Type | Default | Meaning | | --------------- | ------- | ----------------------------------------- | ------------------------------------------ | | `revokeLicense` | boolean | `false` pre-session / `true` post-session | Revoke the license key (post-session only) | | `blacklistHwid` | boolean | `true` | Add current HWID to app HWID blacklist | | `blacklistIp` | boolean | `true` | Add caller IP to app IP blacklist | ### Critical safety rule Pre-session requests cannot revoke by key. If a pre-session request sets `revokeLicense: true`, the API returns `revoke_requires_session`. This prevents accidentally revoking arbitrary or attacker-supplied keys before the key is proven by a valid authenticated session. ### Example (pre-session, blacklist only) ```bash theme={null} curl -X POST https://auth.authforge.cc/auth/selfban \ -H "Content-Type: application/json" \ -d '{ "appId": "YOUR_APP_ID", "appSecret": "YOUR_APP_SECRET", "licenseKey": "AF-XXXX-XXXX-XXXX", "hwid": "device-hwid", "nonce": "fresh-random-nonce", "revokeLicense": false, "blacklistHwid": true, "blacklistIp": true }' ``` ### Example (post-session, full lockout) ```bash theme={null} curl -X POST https://auth.authforge.cc/auth/selfban \ -H "Content-Type: application/json" \ -d '{ "appId": "YOUR_APP_ID", "sessionToken": "SESSION_TOKEN_FROM_VALIDATE", "hwid": "device-hwid", "revokeLicense": true, "blacklistHwid": true, "blacklistIp": true }' ``` *** ## Get security config Retrieve the current blacklist and whitelist configuration for an application. ### Path parameters | Param | Type | Description | | ------- | ------ | ------------------ | | `appId` | string | The application ID | ### Example ```bash theme={null} curl https://api.authforge.cc/v1/apps/YOUR_APP_ID/security \ -H "Authorization: Bearer af_live_your_key" ``` ### Response (200) ```json theme={null} { "hwidBlacklist": ["a1b2c3d4e5f6..."], "hwidWhitelist": [], "ipBlacklist": ["203.0.113.50"], "ipWhitelist": [] } ``` Empty arrays indicate no entries for that list. An empty whitelist means whitelist mode is **not active** (all values are allowed). *** ## Update security config Replace the security configuration. Only included fields are updated; omitted lists remain unchanged. ### Request body | Field | Type | Required | Description | | --------------- | --------- | -------- | ------------------------------ | | `hwidBlacklist` | string\[] | No | HWIDs to block (max 1,000) | | `hwidWhitelist` | string\[] | No | Allowed HWIDs only (max 1,000) | | `ipBlacklist` | string\[] | No | IPs to block (max 1,000) | | `ipWhitelist` | string\[] | No | Allowed IPs only (max 1,000) | ### Example ```bash theme={null} curl -X PUT https://api.authforge.cc/v1/apps/YOUR_APP_ID/security \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "hwidBlacklist": ["a1b2c3d4e5f6...", "7g8h9i0j1k2l..."], "ipBlacklist": ["203.0.113.50"] }' ``` ### Response (200) ```json theme={null} { "hwidBlacklist": ["a1b2c3d4e5f6...", "7g8h9i0j1k2l..."], "hwidWhitelist": [], "ipBlacklist": ["203.0.113.50"], "ipWhitelist": [] } ``` ### Errors | HTTP | Code | Cause | | ---- | ------------- | ---------------------------------------------------- | | 400 | `bad_request` | Invalid entry format, exceeds 1,000 entries per list | | 403 | `forbidden` | The app doesn't belong to your account | ### Clearing a list Set the list to an empty array: ```bash theme={null} curl -X PUT https://api.authforge.cc/v1/apps/YOUR_APP_ID/security \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{"hwidBlacklist": []}' ``` *** ## Add to blacklist Add a single HWID or IP to the blacklist. ### Request body | Field | Type | Required | Description | | ------- | ------ | -------- | ------------------------------------ | | `type` | string | Yes | `"hwid"` or `"ip"` | | `value` | string | Yes | The HWID hash or IP address to block | ### Example ```bash theme={null} curl -X POST https://api.authforge.cc/v1/apps/YOUR_APP_ID/security/blacklist \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{"type": "hwid", "value": "a1b2c3d4e5f6..."}' ``` ### Response (200) ```json theme={null} { "ok": true } ``` *** ## Remove from blacklist Remove a single HWID or IP from the blacklist. ### Request body | Field | Type | Required | Description | | ------- | ------ | -------- | ------------------- | | `type` | string | Yes | `"hwid"` or `"ip"` | | `value` | string | Yes | The entry to remove | ### Example ```bash theme={null} curl -X DELETE https://api.authforge.cc/v1/apps/YOUR_APP_ID/security/blacklist \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{"type": "hwid", "value": "a1b2c3d4e5f6..."}' ``` ### Response (200) ```json theme={null} { "ok": true } ``` *** ## Add to whitelist Add a single HWID or IP to the whitelist. Enabling a whitelist restricts authentication to only listed entries. ### Request body | Field | Type | Required | Description | | ------- | ------ | -------- | ------------------------------------ | | `type` | string | Yes | `"hwid"` or `"ip"` | | `value` | string | Yes | The HWID hash or IP address to allow | ### Example ```bash theme={null} curl -X POST https://api.authforge.cc/v1/apps/YOUR_APP_ID/security/whitelist \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{"type": "ip", "value": "198.51.100.10"}' ``` ### Response (200) ```json theme={null} { "ok": true } ``` Adding an entry to a whitelist activates allowlist mode for that type. All non-listed HWIDs or IPs will be blocked. *** ## Remove from whitelist Remove a single HWID or IP from the whitelist. If the whitelist becomes empty, allowlist mode is deactivated. ### Request body | Field | Type | Required | Description | | ------- | ------ | -------- | ------------------- | | `type` | string | Yes | `"hwid"` or `"ip"` | | `value` | string | Yes | The entry to remove | ### Example ```bash theme={null} curl -X DELETE https://api.authforge.cc/v1/apps/YOUR_APP_ID/security/whitelist \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{"type": "ip", "value": "198.51.100.10"}' ``` ### Response (200) ```json theme={null} { "ok": true } ``` *** ## Limits | Constraint | Value | | --------------------- | ----------------------------- | | Max entries per list | 1,000 | | HWID value max length | 128 characters | | IP value max length | 45 characters (IPv4 and IPv6) | | HWID value min length | 1 character | | IP value min length | 7 characters | ## Evaluation order During authentication, lists are checked in this order: 1. IP blacklist (reject if matched) 2. IP whitelist (reject if list is non-empty and IP not listed) 3. HWID blacklist (reject if matched) 4. HWID whitelist (reject if list is non-empty and HWID not listed) **Blacklist always takes precedence.** An entry present on both blacklist and whitelist is blocked. # Variables API Reference Source: https://docs.authforge.cc/api/variables Manage app variables and license variables via the AuthForge Developer API. Variables are key-value pairs delivered to SDK clients during authentication. App variables go to every user; license variables go to a specific license holder. **Limits:** Max 50 keys, 4 KB total JSON size, flat values only (string, number, boolean). Key max length: 64 characters. *** ## Get app variables Retrieve all variables for an application. ### Path parameters | Param | Type | Description | | ------- | ------ | ------------------ | | `appId` | string | The application ID | ### Example ```bash theme={null} curl https://api.authforge.cc/v1/apps/YOUR_APP_ID/variables \ -H "Authorization: Bearer af_live_your_key" ``` ### Response (200) ```json theme={null} { "maintenanceMode": false, "maxUploadSizeMb": 50, "motd": "Welcome to v2.0!", "minVersion": "1.5.0" } ``` Returns an empty object `{}` if no variables are set. *** ## Set app variables Replace all variables for an application. This is a full replacement; omitted keys are removed. ### Path parameters | Param | Type | Description | | ------- | ------ | ------------------ | | `appId` | string | The application ID | ### Request body A JSON object of key-value pairs. Values must be strings, numbers, or booleans. ```bash theme={null} curl -X PUT https://api.authforge.cc/v1/apps/YOUR_APP_ID/variables \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "maintenanceMode": false, "maxUploadSizeMb": 100, "motd": "New features available!", "minVersion": "2.0.0" }' ``` ### Response (200) ```json theme={null} { "maintenanceMode": false, "maxUploadSizeMb": 100, "motd": "New features available!", "minVersion": "2.0.0" } ``` ### Errors | HTTP | Code | Cause | | ---- | ------------- | ---------------------------------------------------------------------- | | 400 | `bad_request` | Invalid body, too many keys (>50), exceeds 4 KB, or invalid value type | | 403 | `forbidden` | The app doesn't belong to your account | ### Clearing all variables Send an empty object to remove all variables: ```bash theme={null} curl -X PUT https://api.authforge.cc/v1/apps/YOUR_APP_ID/variables \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{}' ``` *** ## Get license variables Retrieve all variables for a specific license. ### Path parameters | Param | Type | Description | | ------------ | ------ | --------------------------------------- | | `licenseKey` | string | The license key (URL-encoded if needed) | ### Example ```bash theme={null} curl https://api.authforge.cc/v1/licenses/A3K9-BFWX-7NP2-QHDT/variables \ -H "Authorization: Bearer af_live_your_key" ``` ### Response (200) ```json theme={null} { "plan": "pro", "maxProjects": 10, "customerName": "Acme Corp" } ``` *** ## Set license variables Replace all variables for a specific license. Full replacement; omitted keys are removed. ### Path parameters | Param | Type | Description | | ------------ | ------ | --------------- | | `licenseKey` | string | The license key | ### Request body A JSON object of key-value pairs. ```bash theme={null} curl -X PUT https://api.authforge.cc/v1/licenses/A3K9-BFWX-7NP2-QHDT/variables \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "plan": "enterprise", "maxProjects": 50, "customerName": "Acme Corp", "features": "export,api,priority-support,sso" }' ``` ### Response (200) ```json theme={null} { "plan": "enterprise", "maxProjects": 50, "customerName": "Acme Corp", "features": "export,api,priority-support,sso" } ``` ### Errors | HTTP | Code | Cause | | ---- | ------------- | ---------------------------------------------------------------------- | | 400 | `bad_request` | Invalid body, too many keys (>50), exceeds 4 KB, or invalid value type | | 403 | `forbidden` | The license's app doesn't belong to your account | | 404 | `not_found` | License key doesn't exist | *** ## Validation rules | Rule | Constraint | | ------------------------- | ------------------------------ | | Max keys | 50 per variable set | | Max total JSON size | 4,096 bytes | | Key max length | 64 characters | | Allowed value types | `string`, `number`, `boolean` | | Nesting | Not allowed (flat values only) | | Arrays, objects as values | Not allowed | # Webhooks API Reference Source: https://docs.authforge.cc/api/webhooks Create, list, update, delete, and test webhooks via the AuthForge Developer API. Manage webhook endpoints that receive real-time notifications when license events occur. *** ## Create a webhook Register a new webhook endpoint for an application. ### Path parameters | Param | Type | Description | | ------- | ------ | ------------------ | | `appId` | string | The application ID | ### Request body | Field | Type | Required | Description | | --------- | --------- | -------- | ---------------------------------------------- | | `url` | string | Yes | HTTPS endpoint URL to receive events | | `events` | string\[] | Yes | Array of event names to subscribe to | | `enabled` | boolean | No | Whether the webhook is active (default `true`) | ### Available events `license.validated`, `license.created`, `license.revoked`, `license.activated`, `license.hwid_bound`, `license.hwid_reset`, `license.deleted` ### Example ```bash theme={null} curl -X POST https://api.authforge.cc/v1/apps/YOUR_APP_ID/webhooks \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-server.com/webhooks/authforge", "events": ["license.created", "license.revoked"], "enabled": true }' ``` ### Response (201) ```json theme={null} { "webhookId": "wh_a1b2c3d4e5f6", "appId": "YOUR_APP_ID", "url": "https://your-server.com/webhooks/authforge", "events": ["license.created", "license.revoked"], "enabled": true, "secret": "whsec_7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c", "createdAt": "2026-04-10T12:00:00.000Z" } ``` The `secret` field is returned only on creation. Store it securely; you'll need it to verify webhook signatures. It cannot be retrieved again. ### Errors | HTTP | Code | Cause | | ---- | ------------- | ------------------------------------------------------ | | 400 | `bad_request` | Invalid URL, empty events array, or invalid event name | | 403 | `forbidden` | The app doesn't belong to your account | | 400 | `bad_request` | Max 5 webhooks per app exceeded | *** ## List webhooks List all webhooks for an application. ### Example ```bash theme={null} curl https://api.authforge.cc/v1/apps/YOUR_APP_ID/webhooks \ -H "Authorization: Bearer af_live_your_key" ``` ### Response (200) ```json theme={null} { "webhooks": [ { "webhookId": "wh_a1b2c3d4e5f6", "appId": "YOUR_APP_ID", "url": "https://your-server.com/webhooks/authforge", "events": ["license.created", "license.revoked"], "enabled": true, "lastDeliveryAt": "2026-04-10T15:30:00.000Z", "lastDeliveryStatus": 200, "createdAt": "2026-04-10T12:00:00.000Z" } ] } ``` The `secret` field is not returned in list responses for security. *** ## Update a webhook Update the URL, events, or enabled status of an existing webhook. ### Path parameters | Param | Type | Description | | ----------- | ------ | ------------------ | | `appId` | string | The application ID | | `webhookId` | string | The webhook ID | ### Request body All fields are optional; only included fields are updated. | Field | Type | Description | | --------- | --------- | ----------------------------- | | `url` | string | New endpoint URL | | `events` | string\[] | New event subscriptions | | `enabled` | boolean | Enable or disable the webhook | ### Example ```bash theme={null} curl -X PUT https://api.authforge.cc/v1/apps/YOUR_APP_ID/webhooks/wh_a1b2c3d4e5f6 \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "events": ["license.created", "license.revoked", "license.validated"], "enabled": true }' ``` ### Response (200) ```json theme={null} { "ok": true } ``` *** ## Delete a webhook Permanently delete a webhook endpoint. ### Example ```bash theme={null} curl -X DELETE https://api.authforge.cc/v1/apps/YOUR_APP_ID/webhooks/wh_a1b2c3d4e5f6 \ -H "Authorization: Bearer af_live_your_key" ``` ### Response (200) ```json theme={null} { "ok": true } ``` *** ## Test a webhook Send a test event to the webhook endpoint. Returns the HTTP status code from your server. ### Example ```bash theme={null} curl -X POST https://api.authforge.cc/v1/apps/YOUR_APP_ID/webhooks/wh_a1b2c3d4e5f6/test \ -H "Authorization: Bearer af_live_your_key" ``` ### Response (200) ```json theme={null} { "ok": true, "status": 200 } ``` The test sends a signed `test.ping` event with sample data to your endpoint. *** ## Webhook payload format Every webhook delivery sends an HTTP POST with these headers and body: ### Headers | Header | Value | | ----------------------- | ----------------------------------------------------------- | | `Content-Type` | `application/json` | | `X-AuthForge-Event` | Event name (e.g., `license.revoked`) | | `X-AuthForge-Timestamp` | ISO 8601 timestamp | | `X-AuthForge-Signature` | `sha256=` where `` is `HMAC-SHA256(secret, body)` | ### Body ```json theme={null} { "event": "license.revoked", "timestamp": "2026-04-10T15:30:00.000Z", "data": { "licenseKey": "A3K9-BFWX-7NP2-QHDT", "appId": "YOUR_APP_ID", "status": "revoked" } } ``` The `data` object varies by event type but always includes `licenseKey` and `appId`. ### Signature verification ```javascript theme={null} const crypto = require("crypto"); const signatureHeader = req.headers["x-authforge-signature"] || ""; const signature = signatureHeader.startsWith("sha256=") ? signatureHeader.slice("sha256=".length) : ""; const expected = crypto .createHmac("sha256", webhookSecret) .update(rawBody) .digest("hex"); const sigBuf = Buffer.from(signature, "hex"); const expectedBuf = Buffer.from(expected, "hex"); const valid = sigBuf.length === expectedBuf.length && crypto.timingSafeEqual(sigBuf, expectedBuf); ``` See [Webhooks](/features/webhooks#signature-verification) for full verification examples in Node.js and Python. # Managing Credits Source: https://docs.authforge.cc/best-practices/credit-management Understand credit consumption, estimate monthly usage, and optimize your AuthForge spending. AuthForge uses a credit-based billing model. Understanding how credits are consumed helps you estimate costs and avoid running out. ## Credit consumption | Operation | Cost | | ----------------------------------------------------- | -------- | | Successful license validation (`POST /auth/validate`) | 1 credit | | Every 10 successful heartbeats | 1 credit | Failed validations (invalid key, expired, revoked) do **not** consume credits. Only successful authentications are billed. Heartbeat credits are debited on every 10th successful call. ## Estimating monthly usage ### Formula ``` monthly_credits = validate_credits + heartbeat_credits validate_credits = active_licenses × avg_logins_per_day × 30 heartbeat_credits = active_licenses × avg_hours_per_day × (60 / heartbeat_interval_min) × 30 / 10 ``` ### Example calculation Suppose you have: * 500 active licenses * Users log in once per day on average * Users run the app 8 hours per day * Heartbeat interval: 15 minutes (default) ``` validate_credits = 500 × 1 × 30 = 15,000 heartbeat_credits = 500 × 8 × 4 × 30 / 10 = 48,000 Total = 63,000 credits/month ``` The **100k tier (\$30/month)** covers this with headroom. Bumping the heartbeat interval to 30 minutes roughly halves the heartbeat portion. ### Quick reference (SERVER mode, 15-min heartbeat) | Active licenses | Usage pattern | Estimated monthly credits | Recommended tier | | --------------- | ---------------------- | ------------------------- | ---------------- | | 50 | 4 hrs/day, daily login | \~4,500 | 10k (\$10) | | 200 | 8 hrs/day, daily login | \~25,200 | 30k (\$15) | | 500 | 8 hrs/day, daily login | \~63,000 | 100k (\$30) | | 1,000 | 8 hrs/day, daily login | \~126,000 | 500k (\$100) | | 5,000 | 8 hrs/day, daily login | \~630,000 | 1M (\$150) | These numbers scale linearly with heartbeat frequency. Doubling the interval halves the heartbeat column; switching to LOCAL mode (see below) drops it to near zero. ## Credit tiers | Tier | Credits | Price | Per 1k credits | | ---- | --------- | ----- | -------------- | | 10k | 10,000 | \$10 | \$1.00 | | 30k | 30,000 | \$15 | \$0.50 | | 100k | 100,000 | \$30 | \$0.30 | | 500k | 500,000 | \$100 | \$0.20 | | 1M | 1,000,000 | \$150 | \$0.15 | Higher tiers offer better per-credit pricing. Choose the tier that covers your monthly estimate with a comfortable margin. ## Auto-refill Set up auto-refill to automatically purchase credits when your balance drops below a threshold. This prevents your users from experiencing `no_credits` failures. ### Setup 1. Go to the [Dashboard](https://app.authforge.cc/dashboard) → **Settings → Billing** 2. Add a payment method (credit card via Stripe) 3. Enable **Auto-refill** 4. Configure: * **Tier**: Which credit package to purchase (10k, 30k, 100k, etc.) * **Threshold**: Trigger a purchase when balance falls below this number * **Cooldown**: Minimum time between auto-refill purchases (30 minutes to 24 hours) ### Recommendations | Monthly usage | Threshold | Tier | Cooldown | | ------------- | --------- | ------------ | -------- | | \< 10k | 2,000 | 10k | 24 hours | | 10k–30k | 5,000 | 30k | 12 hours | | 30k–100k | 10,000 | 100k | 6 hours | | 100k+ | 20,000 | 100k or 500k | 2 hours | Set the threshold high enough to cover your usage during the cooldown period. If you consume 1,000 credits per hour and your cooldown is 6 hours, set the threshold to at least 6,000. ## Low balance alerts Configure email alerts when your balance drops below a threshold: 1. Go to **Settings → Billing → Alerts** 2. Enable **Low balance email** 3. Set your **alert threshold** You'll receive an email when your balance falls below the threshold, giving you time to purchase more credits or adjust auto-refill settings. ## Optimizing credit usage ### Use LOCAL heartbeat mode If you don't need instant revocation enforcement, switch to LOCAL heartbeat mode. This eliminates heartbeat credit consumption almost entirely; the SDK only makes a network call when the session token expires (24 hours by default, up to 7 days if the SDK set a custom `ttlSeconds`). ```python theme={null} client = AuthForgeClient( app_id="...", app_secret="...", heartbeat_mode="LOCAL", # Minimal credit usage ) ``` **Credit savings:** For a user running the app 8 hours/day, SERVER mode at a 15-minute interval consumes \~32 heartbeats ≈ 3.2 credits/day. LOCAL mode needs at most 1 re-validate per session-TTL-window (\~1 credit every 24h by default). That's roughly 3x cheaper at a 15-minute interval and scales much further if you shorten the interval. ### Increase heartbeat interval If you want SERVER mode but don't need 15-minute checks, increase the interval: ```python theme={null} client = AuthForgeClient( app_id="...", app_secret="...", heartbeat_mode="SERVER", heartbeat_interval=1800, # 30 minutes instead of 15 ) ``` Doubling the interval halves your heartbeat credit consumption. ### Avoid redundant logins If your app can open multiple windows or instances, authenticate once and share the session: ```python theme={null} # Don't authenticate in every window/instance # Use a lockfile or IPC to coordinate if is_primary_instance(): client.login(license_key) else: wait_for_primary_auth() ``` See [SDK Best Practices: Multi-instance](/sdk/best-practices#multi-instance-and-multi-window) for implementation details. ## Monitoring usage Check your current balance and transaction history in the dashboard: * **Settings → Billing** shows your current balance * **Transaction history** shows credits purchased, consumed, and auto-refilled * **Usage stats** show authentication volume per app ## What happens when credits run out When your account has zero credits: 1. **SDK validate calls** receive a `no_credits` error. The SDK treats this as a login failure. 2. **Heartbeat milestones** (every 10th heartbeat) fail with `no_credits`. The SDK triggers the failure callback. Users already authenticated continue running until their next heartbeat milestone or session expiry. They aren't immediately disconnected. Your users see "Authentication service temporarily unavailable"; they don't know it's a billing issue on your end. Set up auto-refill to prevent this. # Security Best Practices Source: https://docs.authforge.cc/best-practices/security Harden your AuthForge integration against tampering, reverse engineering, and abuse. AuthForge provides the infrastructure for license verification, but how you integrate it affects how resistant your application is to cracking and abuse. Follow these practices to maximize protection. ## Protect your App Secret The App Secret is the most sensitive value in your integration. It's used to authenticate `/auth/validate` requests. If an attacker obtains it, they can impersonate your app and consume licenses/credits. Response signatures are verified with your app's public key, but protecting the App Secret still matters because it gates every new login. Don't embed the secret as a string literal in your source code. Use environment variables, encrypted config files, or a secrets manager. ```python theme={null} # Bad client = AuthForgeClient(app_secret="550e8400-e29b-41d4-a716-446655440000") # Good client = AuthForgeClient(app_secret=os.environ["AUTHFORGE_SECRET"]) ``` Ensure your logging framework doesn't accidentally capture it. Redact secrets from error reports and crash dumps. If you suspect the secret has been leaked, rotate it immediately in the dashboard (**App Settings → Rotate Secret**). All running clients will fail on the next heartbeat, prompting a restart with the new secret. Add your config file or `.env` to `.gitignore`. Use CI/CD secrets for deployment. ## Authenticate early Call `login()` as early as possible in your application's startup. Don't let the app run meaningful code paths before authentication succeeds. ```python theme={null} # Bad; app runs unprotected for several seconds initialize_app() load_plugins() setup_ui() # ... much later client.login(key) # Good; nothing runs before auth client = AuthForgeClient(...) if not client.login(key): exit(1) initialize_app() ``` ## Minimize error details Don't expose internal error information to the user. Detailed error messages help attackers understand your verification logic. ```python theme={null} # Bad; reveals implementation details print(f"Signature verification failed: expected {expected}, got {actual}") print(f"Nonce mismatch: sent {sent_nonce}, received {recv_nonce}") # Good; generic message print("Authentication failed. Please try again or contact support.") ``` Log the detailed error internally for debugging, but show only generic messages in the UI. ## Use SERVER heartbeat mode for high-value software LOCAL heartbeat mode can be bypassed by: * Freezing or rolling back the system clock * Patching the SDK to skip the local signature check SERVER mode requires a valid signed response from the AuthForge API on every heartbeat. For software where piracy has significant business impact, use SERVER mode. ## Don't trust the client Your binary can be decompiled, patched, and modified. Design with this assumption: * **Don't gate features with a simple boolean.** Instead of `if (licensed) { runProFeature() }`, read [license variables](/features/variables#license-variables) and check specific capabilities. * **Don't store the license status in a predictable location.** Avoid a single `isLicensed = true` field that can be patched. * **Spread auth checks.** Don't check the license in a single function; verify state at multiple points in your application. ## Obfuscation (defense in depth) While not a substitute for proper license verification, code obfuscation raises the difficulty of cracking: | Language | Tools | | -------- | ------------------------------------------- | | Python | PyArmor, Cython compilation, Nuitka | | C# | ConfuserEx, .NET Reactor, Eazfuscator | | C++ | LLVM obfuscation passes, Themida, VMProtect | ## Network security * **Always use HTTPS.** The SDK communicates with `https://auth.authforge.cc` by default. Never override the base URL to use HTTP. * **Pin the certificate** if your platform supports it, to prevent MITM with a trusted CA compromise. * **Validate the nonce on `/auth/validate`.** The SDK does this automatically; the nonce in the response must match the one you sent. Heartbeats don't use nonce replay detection; they rely on the signed short-lived session token instead. ## Self-ban safely If your anti-tamper checks detect runtime manipulation, call `/auth/selfban` to lock out future auth attempts. * **Prefer post-session self-ban for revoke.** Revoke by key only when you have a valid `sessionToken` from a successful login. * **Never revoke by key pre-session.** Before activation, a `licenseKey` value can be attacker-controlled. Revoking at that stage can ban random or other customers' keys. * **Use pre-session for containment only.** Pre-session self-ban should be limited to `blacklistHwid` and/or `blacklistIp`. * **Keep nonce hygiene.** Pre-session self-ban uses nonce replay protection; always send a fresh nonce. * **Treat self-ban as high risk action.** Add local anti-tamper confidence thresholds so noisy detections do not mass-ban legitimate users. The API enforces this rule: pre-session self-ban requests with `revokeLicense: true` are rejected with `revoke_requires_session`. ## Rate limiting `/auth/validate` is rate-limited in addition to API Gateway throttling: * **Per IP address:** up to **30** `/auth/validate` requests per minute. * **Per license key:** up to **5** `/auth/validate` requests per minute (in addition to the per-IP limit). `/auth/heartbeat` is **not** IP rate-limited at the application layer; a flood of heartbeats only burns the victim app's credits (which the server already handles via `no_credits` / `app_burn_cap_reached`), so the usual "denial of service" shape flips into "denial of wallet" for the attacker. API Gateway burst/sustained throttling still applies. Official SDKs apply retry logic automatically when `/auth/validate` returns `rate_limited`. If you are building a **custom integration**, treat `rate_limited` as a transient error: implement **exponential backoff** (with jitter) and avoid tight retry loops so you stay within the limits above. ## HWID and IP security Use [blacklists and whitelists](/features/security) to control access: * **Blacklist known-bad HWIDs**: If you discover a cracked copy, blacklist the HWID to prevent re-authentication. * **Use IP whitelists for enterprise**: Restrict authentication to known office IP ranges. * **Monitor for anomalies**: If a license key is being validated from many different HWIDs (more than the slot count allows, via resets), investigate abuse. ## API key security For your Developer API keys: * **Don't expose API keys in client-side code.** API calls should only be made from your server. * **Use separate keys for different environments** (development, staging, production). * **Audit key usage**: If a key is compromised, delete it and create a new one. Keys are scoped to your account, so a compromised key gives access to all your apps' license management. ## Webhook security * **Always verify the signature** on incoming webhook requests using the `X-AuthForge-Signature` header. * **Use HTTPS** for your webhook endpoint. * **Don't expose your webhook secret** in client code or logs. * **Validate the event type**: only process events you expect. See [Webhooks: Signature Verification](/features/webhooks#signature-verification) for implementation examples. ## Incident response If you discover a cracked version of your software: 1. **Identify the HWID** from your auth logs (if available via webhooks or the dashboard). 2. **Blacklist the HWID** to block future authentication from that machine. 3. **Rotate your App Secret** if you suspect it was extracted from the binary. 4. **Update your binary** with stronger obfuscation and push an update. 5. **Review your integration** against this checklist. # UX Patterns for Licensed Software Source: https://docs.authforge.cc/best-practices/ux-patterns Best practices for creating a smooth user experience in license-gated applications. Good licensing UX is invisible when things work and helpful when they don't. These patterns ensure your customers have a smooth experience. ## First-run experience The first time a user launches your app, they need to enter a license key. Make this frictionless: ### Desktop apps Show a clean, focused dialog. Don't overwhelm with options; just a single input field and an activate button.

Welcome to YourApp

Enter your license key to get started

XXXX-XXXX-XXXX-XXXX
Purchase Activate
* Auto-format the input as the user types (add dashes automatically). * Accept keys with or without dashes. * Show a "Purchase" link for users who don't have a key yet. * Trim whitespace; users often copy keys with trailing spaces. ### CLI tools Support multiple input methods to minimize friction: ```bash theme={null} # Via flag yourapp --license-key XXXX-XXXX-XXXX-XXXX # Via environment variable export AUTHFORGE_LICENSE_KEY=XXXX-XXXX-XXXX-XXXX yourapp # Interactive prompt (if no key found) yourapp > No license key found. Enter your key: _ ``` ## Storing the key locally After successful validation, save the key locally so users don't re-enter it on every launch. ```python theme={null} import json import os CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".yourapp") CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json") def save_license_key(key): os.makedirs(CONFIG_DIR, exist_ok=True) config = load_config() config["licenseKey"] = key with open(CONFIG_FILE, "w") as f: json.dump(config, f) def load_license_key(): config = load_config() return config.get("licenseKey") ``` On subsequent launches, load the stored key and authenticate silently: ```python theme={null} stored_key = load_license_key() if stored_key and client.login(stored_key): # Authenticated silently; go straight to the app run_app() else: # Show the license key dialog key = show_license_dialog() if client.login(key): save_license_key(key) run_app() ``` ## Settings page Provide a settings or "About" page where users can see their license status: | Field | Value | | ----------- | ---------------------------------------- | | License key | `A3K9-****-****-QHDT` (partially masked) | | Status | Active | | Plan | Pro | | Expires | December 31, 2026 | | Devices | 1 of 2 slots used | | | \[Deactivate] | * Mask the license key (show first and last groups only). * Show the expiry date prominently. * Include a "Deactivate" button that clears the stored key and returns to the license dialog. * If using license variables, show the plan tier. ## Expiration and renewal When a license is approaching expiration, show gentle reminders: ```python theme={null} from datetime import datetime, timedelta if client.login(license_key): expires_at = client.license_variables.get("expiresAt") if expires_at: expiry = datetime.fromisoformat(expires_at) days_left = (expiry - datetime.now()).days if days_left <= 7: show_banner( f"Your license expires in {days_left} days. " "Renew now to avoid interruption.", action_url="https://yoursite.com/renew" ) ``` When the license has expired, show a clear message with a path to renewal:

Your license has expired

Your license expired on December 31, 2026.
Renew to continue using YourApp.

Enter New Key Renew
## Trial mode Use the Developer API to issue time-limited trial licenses: ```javascript theme={null} // Create a 14-day trial license const trial = await fetch("https://api.authforge.cc/v1/licenses", { method: "POST", headers: { Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ appId: process.env.AUTHFORGE_APP_ID, quantity: 1, expiresAt: new Date(Date.now() + 14 * 86400000).toISOString(), label: "trial", }), }); ``` In the app, show the remaining trial period: ```python theme={null} if is_trial: days_left = calculate_days_remaining() show_persistent_banner(f"Trial: {days_left} days remaining. Upgrade to keep using YourApp.") ``` ## Upgrade prompts When a user on a lower tier tries to use a gated feature, show a helpful prompt instead of silently failing: ```python theme={null} def export_project(): features = client.license_variables.get("features", "") if "export" not in features: show_upgrade_dialog( title="Export requires Pro", message="Upgrade to the Pro plan to unlock project export.", action_url="https://yoursite.com/pricing" ) return # ... actual export logic ``` ## Graceful degradation If your app has a free tier or limited mode, fall back to it instead of killing the app entirely: ```python theme={null} if client.login(license_key): run_full_app() elif has_free_tier: show_notice("Running in free mode. Some features are limited.") run_limited_app() else: show_error("Invalid license key.") exit(1) ``` ## Error messages Always show user-friendly messages. Map internal error codes to helpful text: | Error | User sees | | --------------- | ------------------------------------------------------------------------- | | `invalid_key` | "Invalid license key. Please check and try again." | | `expired` | "Your license has expired. \[Renew now]" | | `revoked` | "This license has been deactivated. Contact support." | | `hwid_mismatch` | "This license is in use on another device. Contact support to transfer." | | `no_credits` | "Service temporarily unavailable. Please try again in a few minutes." | | Network error | "Couldn't connect to the license server. Check your internet connection." | Never show internal error codes like `no_credits` or `invalid_app` to end users. ## Offline first launch The initial authentication always requires a network connection. Make this clear:

Connection Required

YourApp needs an internet connection for first-time activation. After that, it works offline.

Retry
After initial activation with LOCAL heartbeat mode, the app works offline until the session token expires (24 hours by default; up to 7 days if the SDK was configured with a longer `ttlSeconds`). # Core Concepts Source: https://docs.authforge.cc/concepts Key terms and concepts in AuthForge: apps, licenses, HWIDs, credits, heartbeats, and more. ## Apps An app represents the software you're protecting. Each app has: * **App ID**: A UUID that identifies your app. Public (embedded in your binary). * **App Secret**: A UUID used to authenticate `/auth/validate` requests. Keep this secret. Rotatable via the dashboard. * **Public Key**: A base64 Ed25519 public key used by SDKs to verify signed `/auth/validate` and `/auth/heartbeat` success responses. * **Settings**: Configuration for your app (variables, security rules, webhooks). You can create multiple apps under one account. Each app has its own license keys, variables, and security settings. ## License Keys License keys are alphanumeric strings in the format `XXXX-XXXX-XXXX-XXXX` (using A–Z excluding I and O, digits 2–9). Each key is bound to a single app. A license has the following properties: | Field | Description | | -------------- | --------------------------------------------------------- | | `licenseKey` | The key string itself | | `appId` | The app this license belongs to | | `status` | `active` or `revoked` | | `expiresAt` | ISO 8601 expiration date, or `null` for lifetime licenses | | `maxHwidSlots` | How many devices can use this key simultaneously (1–16) | | `hwidList` | Array of bound hardware IDs | | `label` | Optional label for your records (e.g., order ID) | | `createdAt` | When the license was created | Generate keys in the dashboard or programmatically via the [Developer API](/api/licenses). ## HWID (Hardware ID) A hardware fingerprint of the user's machine. Each SDK collects stable system identifiers (such as MAC address, CPU, hostname, or disk serial; varying by language and platform) and computes a SHA-256 hash. The result is a 64-character hex string. The exact inputs differ per SDK to use the most reliable identifiers available in each language's standard library. The backend treats the HWID as an opaque string; it only needs to be stable and unique per machine. **Important:** HWID strings are **SDK-specific**. The same physical computer can produce different HWIDs when using different AuthForge SDKs (for example, Python vs C#). See [Hardware locking (HWID)](/features/hwid-locking) for how that affects seats, support, and security lists. The HWID is sent during license validation. If the license hasn't seen this HWID before and has available slots, the server binds it. If all slots are full and the HWID doesn't match any bound device, authentication fails with `hwid_mismatch`. ## HWID Slots The number of machines a single license key can be active on simultaneously. When a user authenticates from a new device: * If there's an open slot, the HWID is bound automatically. * If all slots are full, authentication fails. To let a customer move to a new machine, reset their HWID bindings from the dashboard or via the API (`reset-hwid` action). ## Credits Credits are the unit of billing in AuthForge. Every billable operation deducts credits from your account. | Operation | Cost | | ----------------------------- | -------- | | Successful license validation | 1 credit | | 10 successful heartbeats | 1 credit | Heartbeat billing is debited on every 10th successful heartbeat. Heartbeat credits are **not** prepaid; the session itself is short-lived (see [Session TTL](#session-tokens)), so "every 10th heartbeat" is literally how many billable events happened. Purchase credits in the dashboard. Available tiers: | Tier | Credits | Price | | ---- | --------- | ----- | | 10k | 10,000 | \$10 | | 30k | 30,000 | \$15 | | 100k | 100,000 | \$30 | | 500k | 500,000 | \$100 | | 1M | 1,000,000 | \$150 | Set up [auto-refill](/best-practices/credit-management) to automatically purchase credits when your balance drops below a threshold. ## Heartbeats After a successful login, the SDK runs periodic background checks to verify the license is still valid. The default interval is **15 minutes**. If a heartbeat detects the license has been revoked, expired, or the session is invalid, the SDK triggers your `onFailure` callback (or terminates the process if no callback is set). ### Heartbeat Modes On each interval, the SDK sends `POST /auth/heartbeat` with the session token. The server verifies the token + signature chain, checks the license status, and returns a new signed response. SDKs verify every heartbeat signature with your app's Ed25519 public key. **Pros:** Catches revocations immediately on the next heartbeat. Real-time enforcement. Any heartbeat interval ≥ 1s is economically safe; heartbeats cost 1 credit per 10 calls. **Cons:** Requires network connectivity. **Use when:** You need instant revocation enforcement, your users are always online, or you're protecting high-value software. On each interval, the SDK re-verifies the stored session signature locally without making a network call. When the session token expires (default 24 hours, configurable per-SDK up to 7 days), the SDK triggers the failure handler. In SERVER mode, each successful heartbeat mints a fresh session token, so long-running apps never expire. **Pros:** Works offline after initial login. Zero heartbeat credit consumption. **Cons:** Revocations don't take effect until the next server-side check. If the app stays offline longer than the session TTL (\~24h by default) without a successful heartbeat, the local session expires and the SDK triggers the failure handler. **Use when:** Your users may be offline, you want to minimize credit usage, or instant revocation isn't critical. ## Session Tokens A successful `/auth/validate` response includes a signed **session token** that the SDK stores and replays on every heartbeat. The token carries the HWID binding, license key, and (if the SDK requested a custom lifetime) a `ttl` claim. | Property | Value | | ---------------- | --------------------------------------------------------------------------------------- | | Default lifetime | 24 hours | | Minimum lifetime | 1 hour (3600s) | | Maximum lifetime | 7 days (604800s) | | Refresh | Every successful heartbeat mints a fresh token, preserving the originally requested TTL | | Revocation | Revoking a license invalidates all of its tokens on the next heartbeat | SDKs expose a `ttlSeconds` / `session_ttl_seconds` / `SessionTTL` configuration option so you can tune how long the SDK can keep running without reaching the server (LOCAL mode) before forcing a re-login. The server silently clamps out-of-range values. ### Signed payload fields Successful `/auth/validate` and `/auth/heartbeat` responses include a **signed JSON payload** (base64 in the `payload` field) in addition to the nested **session** JWT in `sessionToken`. Besides `appVariables` / `licenseVariables` on validate, the payload includes: | Field | Meaning | | ------------------ | ---------------------------------------------------------------------------------------------------------- | | `expiresIn` | Unix **seconds** when the **session** token expires (historical field; same instant as `sessionExpiresAt`) | | `sessionExpiresAt` | ISO 8601 **session** expiry (for display and clarity) | | `licenseExpiresAt` | ISO 8601 **license** expiry, or `null` for lifetime keys | | `maxHwidSlots` | Seat limit for this license | | `hwidCount` | Number of HWIDs currently bound | | `licenseLabel` | Optional dashboard label (only present when set) | Heartbeats repeat the entitlement fields when applicable so long-running clients see updates (for example if an admin changes seats or expiry) without calling validate again. ## Developer API A server-to-server REST API for automating license management from your own backend. Authenticated with API keys (prefixed `af_live_`). Use the Developer API to: * Create licenses programmatically (e.g., after a Stripe payment) * Revoke, activate, extend, or reset HWID on licenses * List and query licenses * Manage app variables, webhooks, and security settings See the [API Reference](/api/overview) for full documentation. ## App Variables Key-value pairs set per app, delivered to every SDK client during authentication in the `appVariables` field of the signed payload. Use cases: * **Feature flags**: `"maintenanceMode": true` * **Remote config**: `"maxUploadSizeMb": 50` * **Messages**: `"motd": "v2.0 releasing Friday!"` * **Version gating**: `"minVersion": "1.5.0"` Limits: max 50 keys, 4 KB total, flat values only (string, number, or boolean). Set via the dashboard or the [Variables API](/api/variables). ## License Variables Key-value pairs set per license, delivered only to that specific license holder during authentication in the `licenseVariables` field. Use cases: * **Plan tiers**: `"plan": "pro"` * **Per-user limits**: `"maxProjects": 10` * **Custom metadata**: `"customerName": "Acme Corp"` Same limits as app variables. Set via the dashboard or the [Variables API](/api/variables). ## Webhooks Real-time HTTP notifications sent to your server when license events occur. Each delivery is HMAC-SHA256 signed with your webhook secret for verification. Supported events: | Event | Trigger | | -------------------- | --------------------------------- | | `license.validated` | Successful authentication via SDK | | `license.created` | License key generated | | `license.revoked` | License revoked | | `license.activated` | Revoked license re-activated | | `license.hwid_bound` | HWID bound to a license slot | | `license.hwid_reset` | HWID bindings cleared | | `license.deleted` | License permanently deleted | Max 5 webhooks per app. See [Webhooks](/features/webhooks) for setup and verification details. ## Blacklists & Whitelists Per-app access control lists for HWIDs and IP addresses: * **HWID blacklist**: Block specific hardware IDs from authenticating. * **HWID whitelist**: When set, only listed HWIDs can authenticate (allowlist mode). * **IP blacklist / whitelist**: Same concept for IP addresses. Blacklist takes precedence over whitelist. Max 1,000 entries per list. See [Security](/features/security) for configuration details. # FAQ & Troubleshooting Source: https://docs.authforge.cc/faq Answers to common AuthForge licensing, SDK, billing, and security questions. ## Frequently Asked Questions ### Licensing #### What happens when my credits run out? Auth calls return `no_credits`. Active sessions with SERVER heartbeat fail on the next heartbeat. LOCAL mode sessions continue until the session token expires (24 hours by default, or whatever TTL the SDK requested; up to 7 days). Set up auto-refill to prevent this. #### Can a customer use one key on multiple devices? Yes. Configure `maxHwidSlots` (1-16) when generating the license. Each new device uses one slot. #### How do I let a customer move to a new computer? Reset their HWID bindings from the dashboard (`app` -> `license` -> `Reset HWID`) or via the Developer API. #### What license key format does AuthForge use? `XXXX-XXXX-XXXX-XXXX` using `A-Z` (excluding `I` and `O`) and digits `2-9`. ### SDK & Integration #### My users are getting hwid\_mismatch errors Their HWID slots are full. Either increase `maxHwidSlots` on the license or reset their HWID bindings. HWIDs can change after OS reinstalls, hardware upgrades, or VM migrations. #### Heartbeats are using too many credits Heartbeats are cheap: **10 successful heartbeats cost 1 credit** (billed on every 10th call). So even a 1-heartbeat-per-second server app costs roughly 8,640 credits/day. If that's still more than you want, increase the heartbeat interval from the default 15 minutes (the credit cost scales directly with the number of heartbeats sent), or switch to LOCAL mode so the SDK re-verifies locally until the session token expires (\~24h by default). Revocations always apply on the **next** server heartbeat, regardless of interval. #### The SDK can't reach the API Check that `auth.authforge.cc` is reachable. The SDK uses HTTPS on port `443`. Some corporate firewalls and China's GFW may block it. The SDK respects the `apiBaseUrl` constructor param if you need to proxy. #### I'm getting rate\_limited errors Only `/auth/validate` is rate-limited: 5 requests per license per minute and 30 per IP per minute. Heartbeats are **not** IP rate-limited. This usually means something is calling `login()` in a loop instead of once at startup. ### Billing #### Do failed auth attempts use credits? No. Only successful validations (1 credit) and successful heartbeats (1 credit per 10) consume credits. #### What payment methods do you accept? Credit and debit cards via Stripe. No PayPal or crypto at this time. #### Can I get a refund on credits? Contact support. Unused credits don't expire. ### Security #### Is my App Secret safe in the binary? The App Secret authenticates `/auth/validate` requests, not webhook delivery and not Developer API access. Extracting it doesn't let an attacker create or revoke licenses. However, obfuscating your binary is still recommended. #### Can someone replay a captured auth request? `/auth/validate` requests include a unique nonce and the server rejects duplicate nonces. Validate and heartbeat success responses are Ed25519-signed by AuthForge with your app's private signing key. SDKs verify every signed payload using your app's public key. `/auth/validate` additionally enforces per-request nonce matching; heartbeats rely on the short-lived session token + signature chain for replay protection. # Commerce Source: https://docs.authforge.cc/features/commerce Stripe and Lemon Squeezy integration for AuthForge; paste your keys, map products to license templates, and AuthForge handles webhooks, licenses, and optional buyer email. The Commerce pipeline is AuthForge’s **recommended payment integration**: a managed license fulfilment backend for your store. You paste your provider credentials in the dashboard, map prices to license templates, and AuthForge verifies inbound webhooks, deduplicates retries, creates portal-ready licenses, and can email the buyer; without you hosting a webhook handler or calling the Developer API for each purchase. Commerce supports **[Stripe](#setup)** and **[Lemon Squeezy](/guides/lemon)** out of the box; you can connect either or both on the same account and have mappings for each route into your AuthForge apps through the same pipeline. If you must own the webhook endpoint yourself (custom Checkout payloads, unusual bundling, or other cases Commerce does not cover), use **[Custom Stripe webhooks](/guides/stripe)** instead. Both paths issue the same licenses through the same APIs. ## What you get * **One-click Stripe setup**: paste your secret key and webhook secret; AuthForge stores them encrypted and starts receiving events. * **Product → license mapping**: declare how each Stripe product or price maps to a license (duration, HWID slots, label, custom variables). * **Idempotent by design**: every inbound webhook is recorded in an event ledger keyed on `(provider, externalEventId)`. Duplicate deliveries never create duplicate licenses. * **Async processing**: webhooks enqueue onto SQS. The worker runs the license action. A DLQ captures poison pills. * **Portal-ready licenses**: the customer’s email is attached to the license automatically, so buyers can immediately sign into the [self-service portal](/features/portal). * **Automatic buyer email**: the Commerce worker emails the license key to the buyer as soon as their purchase clears, so you don't need to wire up fulfilment yourself. Opt out per-app in the Portal Policy panel if you'd rather send it from your own system via the `license.created` webhook. * **Event browser**: every event that hits your account is visible in the dashboard with provider, status, retry metadata, and linked license. ## How it works ```mermaid theme={null} sequenceDiagram participant Stripe participant AF as AuthForge Commerce participant Ledger as Inbound Ledger participant Queue as Event Queue participant Worker as License Worker participant DB as Licenses Stripe->>AF: Webhook (checkout.session.completed) AF->>AF: Verify signature with merchant's secret AF->>Ledger: Insert (provider, externalEventId) AF->>Queue: Enqueue normalized event AF-->>Stripe: 200 OK Queue->>Worker: Deliver event Worker->>DB: Create license + attach email Worker-->>Queue: Ack (or DLQ on failure) ``` Each step is independently observable and retryable. The webhook endpoint is intentionally thin; it persists and acknowledges fast. All the expensive work happens in the worker. ## Setup Commerce lives under the main dashboard nav at **Dashboard → Commerce**. It is global per-account (not per-app), because one Stripe account typically pays licenses into several AuthForge apps. You map individual Stripe prices to specific apps inside Commerce. ### 0. Create an AuthForge app Commerce delivers licenses *into* an app. If you don't have one yet, open **Dashboard → Applications → New application** first, then come back. ### 1. Connect your Stripe account AuthForge uses the **platform\_keys** connection model: each merchant supplies their own Stripe credentials, stored encrypted in DynamoDB under a KMS key we never export. You will provide **two** secrets, both from your own Stripe dashboard. #### 1a. Register the webhook endpoint in Stripe On the Commerce page, AuthForge shows you a webhook URL that looks like: ``` https://api.authforge.cc/billing/webhook/stripe/ ``` Copy it, then in the **Stripe Dashboard**: 1. Go to **Developers → Webhooks → Add endpoint**. 2. Paste the URL from AuthForge. 3. Subscribe to at least **`checkout.session.completed`**. Add any of the following you'll use: * `invoice.paid`; subscription renewals * `customer.subscription.deleted`; subscription cancellations * `charge.refunded`; refunds * `charge.dispute.created`; chargebacks 4. Save. Stripe now shows a **Signing secret** that starts with `whsec_`. Keep that tab open. #### 1b. Paste the webhook signing secret Back on the AuthForge Commerce page, paste the `whsec_…` value from step 1a into the **"Stripe webhook signing secret"** field. #### 1c. Paste your Stripe API key This is a **different** secret from the signing secret. Get it in Stripe under **Developers → API keys**: * Either your **Secret key** (`sk_live_…` or `sk_test_…`), or * A **Restricted key** (`rk_live_…`) with read access to **Checkout Sessions**, **Invoices**, and **Payment Intents**: recommended for safety. Paste it into the **"Stripe secret API key"** field and click **Connect Stripe**. Stripe strips line items from webhook payloads by default (notably for `checkout.session.completed`, which is every Payment Link purchase). AuthForge calls Stripe's API with your key to find out which product the customer bought. **Without an API key, webhooks will be logged as `skipped · api_key_required_for_line_items` and no licenses will be created.** ### 2. Map your products to licenses On the same Commerce page, scroll to **Product mappings** and click **New mapping**. **One mapping = one Stripe price → one AuthForge app.** Each (app, price) pair can map to exactly one license policy; if you try to create a second mapping for the same price in the same app, you'll get a conflict error; edit the existing row instead. | Field | Purpose | | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | AuthForge application | The app whose licenses are generated (pick from your existing apps). | | Stripe price ID | Find it in Stripe → Product catalogue → open a product → the Prices tab. Looks like `price_1N…`. | | Stripe product ID | Optional. Used for reporting; matching is done on the price ID. | | What kind of product is this? | Pick the scenario that matches what you sell; see the table below. | | Access length / billing period (days) | **One-time:** blank = perpetual license; a number = fixed term from purchase (renewals never extend). **Subscription:** days per billing cycle—each renewal extends by this amount. **Add-on:** days added per purchase. | | HWID slots | How many simultaneous devices per key. | | License label | Plain-text tag stored on the license (`"Pro Plan"`, `"Lifetime"`, etc.). Shows in the dashboard and is passed to the SDK. | | Respect Stripe quantity | If ON, buying quantity 3 creates 3 licenses. | #### Pick the right product type | What you sell | Product type | Days field | | ------------------------------------------------------------ | ----------------------------------------------------- | -------------------------------------------------------- | | One-time perpetual license (pay once, never expires) | **One-time purchase (lifetime or fixed length)** | Blank | | One-time fixed-term license (pay once, expires after N days) | **One-time purchase (lifetime or fixed length)** | `N` (e.g. `90`). Use a non-recurring Stripe/Lemon price. | | Monthly subscription | **Subscription; auto-renews and extends the license** | `30` | | Yearly subscription | **Subscription; auto-renews and extends the license** | `365` | | Top-up SKU that adds 30 more days to an existing license | **Add-on; extends an existing customer's license** | `30` | | A SKU that revokes a customer's license when bought (rare) | **Revocation product** | n/a | **One mapping covers the full lifecycle.** A subscription mapping creates the license on first purchase, extends it on every renewal, and revokes it on cancellation, refund, or chargeback; all from the same row. You do not need (and cannot create) a second mapping for the same price to handle renewals. **One-time** mappings create licenses at checkout with your chosen access length; recurring renewal invoices do not extend them—pair with a non-recurring provider price for single-payment SKUs. **Subscription** mappings extend the license by the billing-period days on each renewal. Renewals (`invoice.paid`), cancellations (`customer.subscription.deleted`), refunds (`charge.refunded` / `refund.created`), and chargebacks (`charge.dispute.created`) are dispatched on event type and target the same license that was created at purchase time; AuthForge stores the Stripe customer and subscription IDs on the license so the right license is found even when one customer holds several. ### 3. Test Run a test-mode purchase against your Stripe account. Within a few seconds you should see: * A new row under **Commerce → Events** with status `processed`. * A new license in the target app's **Licenses** tab, with the customer's email attached. * A `license.created` webhook (if you have [webhooks](/features/webhooks) configured). * An email to the buyer's address with their license key and a link to the self-service portal (unless you've opted out under **App settings → Self-service portal → Commerce fulfilment**). If the event shows `skipped` with reason `api_key_required_for_line_items`, you're missing step 1c. If it shows `no_mapping_for_price`, you haven't mapped that price yet (step 2). **AuthForge has no test/live mode of its own.** Stripe's `livemode: false` flag is ignored; whatever Stripe sends is processed the same way and the license created is real in either case. Buyer delivery emails fire for both test-mode and live-mode events, so use a throwaway address when testing if you don't want the notification. ## Buyer delivery email When a purchase completes, the Commerce worker sends the buyer a plain, dark-themed email with: * The license key(s) they just bought, in a monospace block. * A "Open license portal" CTA that deep-links to `portal.authforge.cc`. * A support contact, if you've set one under the Portal Policy panel. This delivery is **best-effort**: the license has already been written and the `license.created` webhook has already fired transactionally before the email is queued. An SES hiccup will log and move on, it won't put the event back in the queue and won't cause duplicate keys. If you need guaranteed delivery, subscribe to `license.created` from your own server. ### Turning it off per-app Some publishers already have their own order-confirmation email and would rather send the license key from there. Open the app in the dashboard → **Self-service portal** panel → **Commerce fulfilment** section, toggle **"Do not email buyers; I'll handle fulfilment via webhook"**. The `license.created` webhook still fires, so your own system can take over. ## The event ledger Every inbound webhook is persisted before processing. You can filter it in the dashboard: * **By provider**: `stripe` or `lemon`. * **By event type**: `checkout.session.completed`, `customer.subscription.deleted`, etc. * **By status**: `pending`, `processed`, `skipped`, `failed`, or `held`. * **By license key**: quickly find every event that touched a given license. The ledger is keyed on `(provider, externalEventId)`, so if Stripe retries a delivery (which it will) AuthForge will recognize the duplicate and skip the license action. ## Troubleshooting The ledger's status + reason columns are designed so you rarely need to look elsewhere. Match whatever you see in the dashboard against the tables below, then use the one-click **Replay** action once the root cause is fixed. ### Common errors These show up as `skipped` or `failed` rows in the events table, with the reason visible in the row detail. | Reason | What it means | Fix | | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | `no_mapping_for_price` | The event's price ID has no mapping under your account. | Create the mapping in the dashboard, then replay the event. | | `ambiguous_mapping_multiple_apps` | The same price is mapped to more than one app on your account. | Delete the duplicate so only one app owns the price, then replay. | | `api_key_required_for_line_items` | Stripe stripped `line_items` from the webhook payload and we don't have a Stripe API key to fetch them. | Paste the Stripe secret key in the connection panel (step 1c above), then replay. | | `line_items_not_found` | The API fallback ran but Stripe returned no line items. Typically a non-purchase event (zero-quantity bundle, test artefact). Usually safe to leave. | None; informational. | | `unmapped_event_type` | Provider sent an event type Commerce does not currently normalize. | None; informational. | | `no_license_to_extend` / `no_license_to_revoke` | A renewal or cancel landed before any licenses existed for the app. | Check whether the matching `purchase_succeeded` event was also missed. If so, that's the one to fix first. | ### The `held` state If the Commerce worker fails to process an event repeatedly, the retry system will eventually move the event into a `held` state. Held rows stay in the ledger with a distinct chip colour until you look at them; nothing auto-expires. | Held reason | What it means | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `transient_retry_exhausted` | The error looked transient (throttling, timeout, upstream 5xx). The system auto-replayed once, it failed again, and now it's waiting for you to decide. | | `non_transient` | The error didn't match a known transient pattern, so the system never retried it automatically. | | `unknown` | The worker crashed before it could record a structured error. | ### Replaying an event Any event in `held` or `failed` state shows a **Replay** action in the events table. Click it, confirm in the modal, and the event is re-enqueued with the stored normalised payload; nothing is re-fetched from the provider, no signatures are re-verified. The row transitions back to `pending` and then through the normal lifecycle. Most triage looks like: 1. Open **Dashboard → Commerce → Events**, filter to **Held** or **Failed**. 2. Read the reason column, fix the root cause (missing mapping, missing API key, etc.). 3. Click **Replay** on the row. Done. If you have a large batch of held rows from the same root cause, fix the cause first and then replay each row; there is no multi-select today, but replay is cheap and safe to click repeatedly. ## Customer "right to be forgotten" When a buyer asks you to delete the personal data you hold about them; for example under GDPR Article 17; you can run the deletion from the dashboard API: ```bash theme={null} curl -X POST "https://api.authforge.cc/apps/$APP_ID/customers/forget" \ -H "Authorization: Bearer $DASHBOARD_ID_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "customerEmail": "alice@example.com", "providerCustomerId": "cus_xxx", "reason": "Ticket #1234" }' ``` You must supply at least one of `customerEmail` or `providerCustomerId`; provide both if you have them so the lookup widens. The endpoint: * finds every license on this app whose email or provider customer id matches, removes those personal fields, and revokes the license so it can no longer authenticate; * redacts the matching rows in the inbound commerce event ledger; the row itself is kept (so payment-history audit trails still show that an event happened), but the buyer's email and customer id are tombstoned out of the payload; * writes a dashboard audit log entry capturing who initiated the deletion and how many records were touched. The audit row never stores the email value itself, so the audit log itself remains compliant. The response gives you a count to forward back to the customer: ```json theme={null} { "ok": true, "licensesRedacted": 1, "ledgerRowsScanned": 42, "ledgerRowsRedacted": 3 } ``` What the endpoint does **not** do for you: * It does not delete the customer inside Stripe or Lemon Squeezy. Deletion in your payment processor is a separate request you make on your provider's dashboard or API; payment processors typically retain financial records under a separate legal basis. * It does not delete the customer's [self-service portal](/features/portal) account. That account is owned by the end user, not by you, so they delete it themselves from the portal. ## Portal-ready by default Licenses created by the Commerce pipeline automatically carry the buyer’s email, which is the credential the end-user portal uses for magic-code authentication. Publishers don’t need to wire anything extra; the customer can go to `portal.authforge.cc`, enter their license key and email, and manage their own HWID resets. For manually-created licenses (via the dashboard or the Developer API), you can attach an email after the fact; see the [portal feature](/features/portal#email-requirement) for details. ## Related features * **[Portal](/features/portal)**: Self-service surface for end users. Commerce licenses are portal-ready out of the box. * **[Webhooks](/features/webhooks)**: Notify your own systems when licenses change. Orthogonal to Commerce. * **[Variables](/features/variables)**: Values attached per license. Set them in the product mapping so every buyer gets them. * **[Custom Stripe webhooks](/guides/stripe)**: Self-hosted webhook + Developer API when Commerce is not enough. # Hardware locking (HWID) Source: https://docs.authforge.cc/features/hwid-locking How AuthForge binds licenses to machines, and why HWIDs depend on which SDK you ship. License keys can be limited to one or more **hardware IDs (HWIDs)**. Each successful login binds the key to a fingerprint of the user’s machine so the same key cannot be reused on unlimited PCs. ## How locking works 1. The **SDK** collects identifiers from the local system (details vary by language and OS APIs). 2. The SDK hashes that material into a **64-character hex string** and sends it with `POST /auth/validate`. 3. The **server** stores that string as an opaque ID. If the license has free **HWID slots**, the new ID is bound. If all slots are full and this ID is not already bound, validation fails with `hwid_mismatch`. You configure how many simultaneous machines a key may use (**max HWID slots**) when you create or edit licenses. ## HWIDs are SDK-specific (important) **The same physical computer can produce different HWID strings for different AuthForge SDKs.** There is no single “true” hardware fingerprint across Python, C#, C++, Rust, Go, and Node; each SDK implements collection and hashing for its own runtime and platform APIs. What this means in practice: * **One product, one SDK**: Users get a consistent HWID for your app. Seat limits and resets behave as you expect. * **Different apps or SDKs on one machine**: If a user runs two products that each use a different AuthForge SDK (or different versions with different fingerprint logic), those products may report **different** HWIDs to AuthForge even on the same box. That is expected: the server only compares the string your SDK sends. * **Support and dashboards**: When you look up a license’s bound HWIDs, you are seeing **per-integration** identities, not a universal device serial. For blacklist entries in [Security lists](/features/security), the value must match the exact string produced by the SDK you ship. For the general model (slots, binding, reset), see [Core Concepts; HWID](/concepts#hwid-hardware-id). ## Related features * **[Blacklists & whitelists](/features/security)**: Block or allow specific HWID strings (exact match). * **[License API](/api/licenses)**: Generate keys with `maxHwidSlots`, reset bindings, and more. * **[SDK overview](/sdk/overview)**: Choose one SDK per product and keep it consistent across releases for stable fingerprints. * **[HWID override](/features/hwid-override)**: Bind licenses to external identities (for example Telegram user IDs) instead of machine fingerprints. # HWID override Source: https://docs.authforge.cc/features/hwid-override Use custom identity strings (like Telegram or Discord user IDs) as the HWID sent to AuthForge. `hwidOverride` lets your integration send a **custom identifier** instead of the SDK's machine fingerprint. This is useful when your product does not run on a fixed device, such as: * Telegram bots * Discord bots * Browser-based automations * Headless services where "machine identity" is not meaningful ## What it does Normally, each SDK computes a hardware fingerprint and sends it as `hwid`. With HWID override enabled, the SDK sends your provided value as `hwid` instead. AuthForge still applies the same seat logic (`maxHwidSlots`, blacklist/whitelist, reset behavior) because the server treats HWID as an opaque identity string. ## Recommended format Use provider prefixes so identities are explicit and collision-safe: * Telegram: `tg:` * Discord: `discord:` * Internal user: `user:` Use immutable IDs, not usernames. Usernames can change. ## Security considerations * Never trust client-supplied usernames as the binding source. * Prefer IDs from signed platform events (Telegram/Discord user IDs). * Apply rate limits on license-entry commands to reduce key guessing. * Keep keys in private channels/DMs when possible. ## SDK parameters * Node: `hwidOverride` * Python: `hwid_override` * Go: `HWIDOverride` * C#: `hwidOverride` * Rust: `hwid_override` * C++: `hwidOverride` For **server-side or bot integrations** that re-check the same identity often, use **`validateLicense`** (see each SDK’s docs for the exact name) with HWID override set: same trust model as `login`, without heartbeat timers or session cleanup. ## Example use cases * **Telegram bot access gate**: Ask for a key once, then bind to `tg:`. * **Discord premium commands**: Validate key, bind to `discord:`, allow premium command set. * **Managed shared workers**: Bind to tenant identity rather than container host fingerprints. See **[HWID override guide](/guides/hwid-override)** for implementation patterns and **[Telegram bot](/guides/telegram-bot)** / **[Discord bot](/guides/discord-bot)** for end-to-end examples. # Low-latency validation Source: https://docs.authforge.cc/features/low-latency-validation Why AuthForge login stays responsive: one round trip, minimal server work, and fast local crypto checks. “Low latency” here means your **login path** stays a single, straightforward request, so time-to-license is dominated by normal HTTPS latency, not extra licensing ceremony. ## One round trip to authenticate End-user login is designed around **one** `POST /auth/validate` call per attempt: ```mermaid theme={null} sequenceDiagram participant App as Your app participant SDK as AuthForge SDK participant API as AuthForge API App->>SDK: login(licenseKey) SDK->>SDK: Collect HWID, build nonce SDK->>API: POST /auth/validate (single request) API-->>SDK: Signed payload + session material SDK->>SDK: Verify Ed25519 signature locally SDK-->>App: Success or structured error ``` There is no multi-step OAuth redirect, no polling loop, and no second “confirm” call required for a standard successful login. ## What happens on the wire After TLS, the server does focused work: resolve the license, check status and expiration, enforce HWID slots and [security lists](/features/security), then **sign** the response. The SDK verifies that signature with your app's public key, so integrity checks happen **locally** without another network hop. Rolling **nonces** are included so responses are not replayable; the SDK handles nonce generation and verification without complicating your UI code. ## What actually affects “how fast it feels” | Factor | Role | | ------------------ | ------------------------------------------------------------------------------- | | **Network RTT** | Usually the largest part of wall-clock time. | | **Cold TLS / DNS** | First request to a host may be slower; later calls reuse connections. | | **Your UX** | Keep license entry non-blocking; avoid unnecessary work before calling `login`. | AuthForge does not add a separate “license server” hop beyond this API, so you avoid stacking extra latency from a custom middle tier for basic validation. ## Heartbeats are separate After login, the SDK runs [heartbeats](/concepts#heartbeats) on a timer. They are **not** part of the initial validation latency your user waits on at startup (unless you block your UI until a heartbeat completes; usually unnecessary). ## Going deeper * **[Security best practices](/best-practices/security)**: Protecting your app secret and verifying auth early. * **[API errors reference](/api/errors)**: Includes signature, nonce, and replay cases for SDK auth. * **[SDK best practices](/sdk/best-practices)**: Error handling, offline behavior, and heartbeat modes. * **[Core Concepts](/concepts)**: Credits, sessions, and HWID binding. # Self-service portal Source: https://docs.authforge.cc/features/portal A hosted surface where your end users sign in with their license key, verify by email, and manage their own HWID resets. The self-service portal lets your customers handle routine license tasks themselves; without opening a support ticket. Buyers go to `portal.authforge.cc`, enter their license key and email, receive a magic code, and land on a page where they can see license status and reset their HWID subject to policy you define. ## Why it exists HWID resets are the single most common support request for license-gated software. The portal gives customers a policy-controlled way to do it themselves: * **Fewer tickets**: users reset their own devices when their policy allows it. * **Auditable**: every reset is logged on the license with timestamp and source. * **Safe by default**: limits and cooldowns are enforced server-side, not suggested. * **Branded**: accent color and display name are published by your app. ## How sign-in works ```mermaid theme={null} sequenceDiagram participant User participant Portal as portal.authforge.cc participant API as portal-api.authforge.cc participant SES as Amazon SES participant DB as Licenses User->>Portal: Enter license key + email Portal->>API: POST /portal/challenges API->>DB: Look up license API->>API: Compare email (constant-time) API->>SES: Send 6-digit code API-->>Portal: Generic "code sent" response User->>Portal: Enter 6-digit code Portal->>API: POST /portal/sessions API-->>Portal: HMAC-signed session token Portal->>API: GET /portal/me (license + policy) ``` A few deliberate properties of this flow: * **Constant-time response.** The portal always returns the same "if the license and email match, a code was sent" message, whether or not the license exists. This prevents the portal from being used to probe for valid keys. * **Short-lived challenges.** Challenges are stored in DynamoDB with a TTL and are single-use. * **Stateless sessions.** Session tokens are HMAC-signed with a secret in AWS Secrets Manager. No session table to scale. ## Email requirement The portal authenticates on **license key + email**. A license without an email attached is invisible to the portal, by design. * **Commerce-created licenses**: the buyer’s email is attached automatically by the [Commerce pipeline](/features/commerce). No action required. * **Manually-created licenses**: you must attach an email before the portal will recognize it. ### Attaching an email (dashboard) 1. Open the app’s **Licenses** tab. 2. Click the row menu on the license → **Portal Email**. 3. Enter the customer’s email (or clear it). Save. Licenses with an email attached show a small mail icon next to the key, so you can spot portal-ready rows at a glance. ### Attaching an email (Developer API) ```bash theme={null} curl -X PUT https://api.authforge.cc/v1/licenses/LICENSE_KEY/email \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "email": "customer@example.com" }' ``` Pass `"email": null` or `""` to clear it. Requires the `write:licenses` scope. You can also pass an `email` field when creating licenses via `POST /v1/licenses`; see the [Licenses API](/api/licenses). ## Policy Each app has a **portal policy** controlling what end users can do. Configure it in the dashboard under **Settings → Portal**. | Field | Purpose | | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | HWID reset limit | Maximum resets allowed inside the window. | | Reset window | Sliding window (for example 30 days) over which the limit applies. | | Cooldown | Minimum time between consecutive resets. | | Support URL | Optional link shown to users who hit their limit. Auto-prefixed with `https://` if the scheme is omitted. | | Support email | Optional `mailto:` link. Also used as the reply-to hint in Commerce delivery emails. | | **Email license key to buyer** | Whether the Commerce worker sends a "here's your license key" email after purchase. Defaults to on. Turn off if you'd rather fulfill via the `license.created` webhook. See [Commerce → Buyer delivery email](/features/commerce#buyer-delivery-email). | | Display name | Header text shown in the portal (defaults to "License portal") and the buyer-facing line in delivery emails. | | Accent color | Hex color (`#A78BFA`) used for the portal primary button and highlights. A live swatch is shown in the dashboard. | When a user attempts a reset, the policy engine (`evaluateResetPolicy`) walks the license’s reset history and enforces limits before the reset is allowed. Every reset is then recorded via `recordPortalReset`, so the next attempt sees an up-to-date history. ## Branding Branding is applied dynamically. When a user signs into the portal, the portal fetches the policy for their license’s app and: * Swaps the MUI theme to use your accent color. * Replaces the header title with your display name. The login screen itself is generic; branding appears once the user proves ownership of the license. This avoids leaking which app a license belongs to before authentication. ## Hosting The portal is hosted by AuthForge: * **UI**: `https://portal.authforge.cc` (static Vite app, served via AWS Amplify). * **API**: `https://portal-api.authforge.cc` (HttpApi behind a Lambda, deployed by our PortalStack). You do not need to buy a domain or stand up any infrastructure. The portal works for every AuthForge app as soon as policy is published and licenses have emails attached. ## What the user sees After signing in, the user lands on their license home page with: * The license key (truncated), status, expiration, and attached email. * Bound HWIDs and how many slots remain. * A **Reset HWIDs** button, gated by policy. When the user is over their limit or inside a cooldown, the button is disabled with a clear explanation and (optionally) a link to your support URL. ## Related features * **[Commerce](/features/commerce)**: Commerce-created licenses are portal-ready automatically. * **[HWID locking](/features/hwid-locking)**: Portal resets clear the bindings the SDK maintains. * **[Licenses API](/api/licenses)**: Attach or change emails programmatically. ## Security notes * Portal session tokens are HMAC-SHA256 signed; the secret lives in AWS Secrets Manager (`PortalSessionSecret`). * Magic codes are 6 digits, single-use, and expire inside a short TTL. * Every user action (login, verify, reset) is logged with a request ID for support traceability. * The email lookup is case-normalized and trimmed; the comparison itself is constant-time. # Blacklists & Whitelists Source: https://docs.authforge.cc/features/security Control which devices and IPs can authenticate with your AuthForge app using blacklists and whitelists. AuthForge supports per-app access control lists for HWIDs and IP addresses. Use them to block pirated copies, restrict beta access, or geo-limit your application. ## How it works During license validation (`/auth/validate`), the server checks the requesting HWID and IP address against your app's access control lists **before** validating the license itself. ```mermaid theme={null} flowchart TD A["SDK sends /auth/validate"] --> B{"IP blacklisted?"} B -->|Yes| X["Reject: blocked"] B -->|No| C{"IP whitelist active?"} C -->|Yes, IP not listed| X C -->|No, or IP listed| D{"HWID blacklisted?"} D -->|Yes| X D -->|No| E{"HWID whitelist active?"} E -->|Yes, HWID not listed| X E -->|No, or HWID listed| F["Continue to license validation"] ``` ### Evaluation order 1. **IP blacklist**: If the IP is blacklisted, reject immediately. 2. **IP whitelist**: If a whitelist is configured and the IP is NOT on it, reject. 3. **HWID blacklist**: If the HWID is blacklisted, reject. 4. **HWID whitelist**: If a whitelist is configured and the HWID is NOT on it, reject. **Blacklist takes precedence over whitelist.** If an entry appears on both lists, it is blocked. ## HWID blacklist Block specific hardware IDs from authenticating. The HWID is the SHA-256 hash the SDK collects from the user's machine. **Use cases:** * Block a known pirated/cracked machine fingerprint * Revoke access from a specific device without revoking the entire license ```bash theme={null} # Add an HWID to the blacklist curl -X POST https://api.authforge.cc/v1/apps/YOUR_APP_ID/security/blacklist \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "type": "hwid", "value": "a1b2c3d4e5f6..." }' ``` ## HWID whitelist When set, **only** listed HWIDs can authenticate. This is allowlist mode; any HWID not on the list is rejected. **Use cases:** * Restrict a beta to specific testers' machines * Lock down access to known-good devices in an enterprise deployment Enabling a HWID whitelist blocks ALL devices not explicitly listed. Make sure you've added all expected HWIDs before enabling. ## IP blacklist Block specific IP addresses from authenticating. **Use cases:** * Block IPs associated with abuse * Block known VPN/proxy ranges ## IP whitelist When set, only listed IPs can authenticate. Useful for enterprise environments where users connect from known office IPs. ## Configuration ### Via the dashboard Go to your app's **Settings** → **Security**. You'll see four sections for each list type. Add entries and click **Save**. ### Via the Developer API **Get current security config:** ```bash theme={null} curl https://api.authforge.cc/v1/apps/YOUR_APP_ID/security \ -H "Authorization: Bearer af_live_your_key" ``` ```json theme={null} { "hwidBlacklist": ["a1b2c3d4..."], "hwidWhitelist": [], "ipBlacklist": ["203.0.113.50"], "ipWhitelist": [] } ``` **Replace entire security config:** ```bash theme={null} curl -X PUT https://api.authforge.cc/v1/apps/YOUR_APP_ID/security \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "hwidBlacklist": ["a1b2c3d4..."], "ipBlacklist": ["203.0.113.50", "198.51.100.0"] }' ``` You can include only the lists you want to update; omitted lists remain unchanged. **Add/remove individual entries:** ```bash theme={null} # Add to blacklist curl -X POST https://api.authforge.cc/v1/apps/YOUR_APP_ID/security/blacklist \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "type": "hwid", "value": "a1b2c3d4..." }' # Remove from blacklist curl -X DELETE https://api.authforge.cc/v1/apps/YOUR_APP_ID/security/blacklist \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "type": "hwid", "value": "a1b2c3d4..." }' # Add to whitelist curl -X POST https://api.authforge.cc/v1/apps/YOUR_APP_ID/security/whitelist \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "type": "ip", "value": "203.0.113.50" }' ``` ## Limits | Limit | Value | | --------------------- | ----------------------------- | | Max entries per list | 1,000 | | HWID value max length | 128 characters | | IP value max length | 45 characters (supports IPv6) | ## Error response When a request is blocked by a blacklist or whitelist, the SDK receives: ```json theme={null} { "status": "failed", "error": "blocked" } ``` The SDK treats this as a login failure. See [SDK Best Practices](/sdk/best-practices#error-handling-on-login) for guidance on user-facing error messages. ## Next steps * [Security API Reference](/api/security); Full endpoint documentation * [Security Best Practices](/best-practices/security); Hardening your AuthForge integration # Unlimited applications Source: https://docs.authforge.cc/features/unlimited-applications Create as many AuthForge apps as you need under one account: one credit pool, no per-app subscription. AuthForge does **not** charge per application or cap how many products you can register. Your account is billed for **API usage** (validations and heartbeats), not for how many apps or license keys exist. ## One account, many apps Each **app** is a separate container for: * Its own **App ID** and **App Secret** * Its own **license keys**, [variables](/features/variables), [webhooks](/features/webhooks), and [security lists](/features/security) Typical uses: * Multiple commercial products or editions * Separate staging vs production apps (different secrets and keys) * Different brands or teams under the same billing account ## One credit balance [Credits](/best-practices/credit-management) are shared across **all** apps on your account. Successful validations and heartbeats deduct from the same pool; no need to buy credits separately per product. ## Practical limits Reasonable use of many apps is expected. If you need enterprise-scale organization (many teams, compliance boundaries), structure apps and API keys so each product only accesses its own `appId`. ## See also * **[Core Concepts; Apps](/concepts#apps)**: What an app represents in AuthForge. * **[Quickstart](/quickstart)**: Create your first app and ship a build. * **[Credit management](/best-practices/credit-management)**: Auto-refill, alerts, and usage. # App & License Variables Source: https://docs.authforge.cc/features/variables Use key-value variables to deliver feature flags, remote config, and per-user settings through the AuthForge SDK. Variables are key-value pairs delivered to your application through the SDK during authentication. There are two types: **app variables** (global) and **license variables** (per-user). ## App Variables App variables are set per app and delivered to **every** SDK client during authentication in the `appVariables` field of the signed payload. ### Use cases | Variable | Value | Purpose | | ------------------- | -------------------------- | ------------------------------- | | `"maintenanceMode"` | `true` | Disable the app remotely | | `"maxUploadSizeMb"` | `50` | Remote config without an update | | `"motd"` | `"v2.0 releasing Friday!"` | Display a message to all users | | `"minVersion"` | `"1.5.0"` | Block outdated clients | ### Setting app variables **Dashboard:** App Settings → Variables **Developer API:** ```bash theme={null} # Get current variables curl https://api.authforge.cc/v1/apps/YOUR_APP_ID/variables \ -H "Authorization: Bearer af_live_your_key" # Set variables (replaces all variables) curl -X PUT https://api.authforge.cc/v1/apps/YOUR_APP_ID/variables \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "maintenanceMode": false, "maxUploadSizeMb": 50, "motd": "Welcome to v2.0!", "minVersion": "1.5.0" }' ``` ### Reading app variables in the SDK ```python Python theme={null} if client.login(license_key): variables = client.app_variables if variables.get("maintenanceMode"): print("Server is under maintenance. Try again later.") exit(0) motd = variables.get("motd") if motd: print(f"Notice: {motd}") ``` ```csharp C# theme={null} if (client.Login(licenseKey)) { var variables = client.AppVariables; if (variables.TryGetValue("maintenanceMode", out var maint) && maint is true) { Console.WriteLine("Server is under maintenance."); Environment.Exit(0); } if (variables.TryGetValue("motd", out var motd)) { Console.WriteLine($"Notice: {motd}"); } } ``` ```cpp C++ theme={null} if (client.Login(key)) { auto variables = client.GetAppVariables(); if (variables.count("maintenanceMode") && variables["maintenanceMode"] == "true") { std::cout << "Server is under maintenance." << std::endl; return 0; } } ``` *** ## License Variables License variables are set per license and delivered **only** to that specific license holder in the `licenseVariables` field. ### Use cases | Variable | Value | Purpose | | ---------------- | ------------------------------- | --------------------------- | | `"plan"` | `"pro"` | Feature gating by plan tier | | `"maxProjects"` | `10` | Per-user resource limits | | `"customerName"` | `"Acme Corp"` | Custom metadata | | `"features"` | `"export,api,priority-support"` | Feature list for the user | ### Setting license variables **Dashboard:** Open an app → click a license row → Variables **Developer API:** ```bash theme={null} # Get current variables for a license curl https://api.authforge.cc/v1/licenses/A3K9-BFWX-7NP2-QHDT/variables \ -H "Authorization: Bearer af_live_your_key" # Set variables (replaces all variables) curl -X PUT https://api.authforge.cc/v1/licenses/A3K9-BFWX-7NP2-QHDT/variables \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "plan": "pro", "maxProjects": 10, "customerName": "Acme Corp" }' ``` ### Reading license variables in the SDK ```python Python theme={null} if client.login(license_key): plan = client.license_variables.get("plan", "basic") if plan == "pro": enable_pro_features() elif plan == "enterprise": enable_enterprise_features() else: enable_basic_features() max_projects = int(client.license_variables.get("maxProjects", 3)) ``` ```csharp C# theme={null} if (client.Login(licenseKey)) { var plan = client.LicenseVariables.GetValueOrDefault("plan", "basic")?.ToString(); switch (plan) { case "pro": EnableProFeatures(); break; case "enterprise": EnableEnterpriseFeatures(); break; default: EnableBasicFeatures(); break; } } ``` *** ## Limits | Limit | Value | | ------------------------- | -------------------------------- | | Max keys per variable set | 50 | | Max total size (JSON) | 4 KB | | Key max length | 64 characters | | Value types | String, number, boolean | | Nesting | Not supported (flat values only) | These limits apply independently to app variables and license variables. ## How variables are delivered Variables are included in the signed payload returned by `/auth/validate`. This means: 1. Variables are delivered at login time, not on every heartbeat. 2. To pick up variable changes, the user must re-authenticate (restart the app, or wait for LOCAL mode to re-validate). 3. Variables are covered by the same Ed25519 signature as the rest of the payload; they can't be tampered with in transit. ## Next steps * [Feature Flags Guide](/guides/feature-flags); Use app variables as a simple feature flag system * [Tiered Licensing Guide](/guides/tiered-licensing); Implement Basic/Pro/Enterprise tiers with license variables * [Variables API Reference](/api/variables); Full endpoint documentation # Webhooks Source: https://docs.authforge.cc/features/webhooks Receive real-time HTTP notifications when license events occur in your AuthForge app. Webhooks send HTTP POST requests to your server when events happen on your licenses; creation, validation, revocation, and more. Use them to sync license state with your backend, trigger workflows, or update your database. ## How it works 1. You register a webhook URL in the dashboard or via the API. 2. When a matching event occurs, AuthForge sends an HTTP POST to your URL with a JSON payload. 3. Each request is signed with HMAC-SHA256 so you can verify it came from AuthForge. ```mermaid theme={null} sequenceDiagram participant User as End User participant SDK as AuthForge SDK participant AF as AuthForge API participant Your as Your Server User->>SDK: login(licenseKey) SDK->>AF: POST /auth/validate AF-->>SDK: Success AF->>Your: POST webhook (license.validated) Your-->>AF: 200 OK ``` ## Events | Event | Trigger | | -------------------- | ---------------------------------------- | | `license.validated` | Successful authentication via SDK | | `license.created` | License key generated (dashboard or API) | | `license.revoked` | License revoked | | `license.activated` | Revoked license re-activated | | `license.hwid_bound` | HWID bound to a license slot | | `license.hwid_reset` | HWID bindings cleared | | `license.deleted` | License permanently deleted | `license.validated` fires on every successful SDK login. For high-traffic apps, consider subscribing only to the events you need. ## Payload format Every webhook delivery sends a JSON body like this: ```json theme={null} { "event": "license.validated", "timestamp": "2026-04-10T15:30:00.000Z", "data": { "licenseKey": "A3K9-BFWX-7NP2-QHDT", "appId": "550e8400-e29b-41d4-a716-446655440000", "status": "active", "hwid": "a1b2c3d4e5f6..." } } ``` ### Headers | Header | Description | | ----------------------- | ------------------------------------------------------------------- | | `Content-Type` | `application/json` | | `X-AuthForge-Event` | The event name (e.g., `license.validated`) | | `X-AuthForge-Timestamp` | ISO 8601 timestamp of the event | | `X-AuthForge-Signature` | `sha256=` where `` is HMAC-SHA256 of the raw request body | ## Signature verification Every webhook is signed using the secret generated when you created the webhook. **Always verify the signature** before processing. The signature is computed as: ``` HMAC-SHA256(webhook_secret, raw_request_body) ``` ### Verification example (Node.js / Express) ```javascript theme={null} const crypto = require("crypto"); app.post("/webhooks/authforge", express.raw({ type: "application/json" }), (req, res) => { const signatureHeader = req.headers["x-authforge-signature"] || ""; const timestamp = req.headers["x-authforge-timestamp"]; const signature = signatureHeader.startsWith("sha256=") ? signatureHeader.slice("sha256=".length) : ""; const expected = crypto .createHmac("sha256", process.env.AUTHFORGE_WEBHOOK_SECRET) .update(req.body) .digest("hex"); const sigBuf = Buffer.from(signature, "hex"); const expectedBuf = Buffer.from(expected, "hex"); if (sigBuf.length !== expectedBuf.length || !crypto.timingSafeEqual(sigBuf, expectedBuf)) { return res.status(401).send("Invalid signature"); } if (!timestamp || Math.abs(Date.now() - Date.parse(timestamp)) > 5 * 60 * 1000) { return res.status(401).send("Stale timestamp"); } const event = JSON.parse(req.body); console.log(`Received ${event.event} for ${event.data.licenseKey}`); // Handle the event switch (event.event) { case "license.validated": // Update last-seen timestamp in your DB break; case "license.revoked": // Suspend user access in your system break; case "license.created": // Send welcome email break; } res.sendStatus(200); }); ``` ### Verification example (Python / Flask) ```python theme={null} import hmac import hashlib from datetime import datetime, timezone from flask import Flask, request, abort app = Flask(__name__) WEBHOOK_SECRET = os.environ["AUTHFORGE_WEBHOOK_SECRET"] @app.route("/webhooks/authforge", methods=["POST"]) def handle_webhook(): signature_header = request.headers.get("X-AuthForge-Signature", "") timestamp = request.headers.get("X-AuthForge-Timestamp") signature = signature_header[7:] if signature_header.startswith("sha256=") else "" expected = hmac.new( WEBHOOK_SECRET.encode(), request.data, hashlib.sha256 ).hexdigest() if not hmac.compare_digest(signature, expected): abort(401) if not timestamp: abort(401) if abs(datetime.now(timezone.utc).timestamp() - datetime.fromisoformat(timestamp.replace("Z", "+00:00")).timestamp()) > 300: abort(401) event = request.get_json() print(f"Received {event['event']} for {event['data']['licenseKey']}") # Handle the event... return "", 200 ``` ## Setup ### Via the dashboard 1. Go to your app's **Settings** → **Webhooks** 2. Click **Add Webhook** 3. Enter your HTTPS endpoint URL 4. Select which events to subscribe to (or select all) 5. Click **Create** 6. **Copy the webhook secret**: it's shown only once ### Via the Developer API ```bash theme={null} curl -X POST https://api.authforge.cc/v1/apps/YOUR_APP_ID/webhooks \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-server.com/webhooks/authforge", "events": ["license.created", "license.revoked"], "enabled": true }' ``` The response includes a `secret` field; store it securely for signature verification. ## Limits | Limit | Value | | -------------------- | ---------- | | Max webhooks per app | 5 | | Delivery timeout | 10 seconds | | Retries | None (v1) | ## Testing Use the test endpoint to send a sample payload to your webhook URL: ```bash theme={null} curl -X POST https://api.authforge.cc/v1/apps/YOUR_APP_ID/webhooks/WEBHOOK_ID/test \ -H "Authorization: Bearer af_live_your_key" ``` This sends a signed `test.ping` event to verify your endpoint is receiving and verifying payloads correctly. ## Next steps * [Webhooks API Reference](/api/webhooks); Full endpoint documentation * [Commerce](/features/commerce); Stripe checkout into licenses; subscribe to `license.created` for your own fulfilment # Developing with AI Source: https://docs.authforge.cc/guides/ai-development Use AI coding agents to integrate AuthForge licensing into your project faster. AuthForge ships an **`AGENTS.md`** file in the root of each official SDK repository. It is a single-file reference for correct licensing integration: constructor parameters, methods, server error codes, and copy-paste patterns (including what not to do). ## Quick integration Paste this prompt into your AI tool **together with** the `AGENTS.md` file from the SDK you use (from GitHub or your package checkout). If the agent should wire imports, include your dependency manifest (`package.json`, `go.mod`, `Cargo.toml`, etc.) after you add the official package. ``` Integrate AuthForge licensing into this project. My App ID is [YOUR_APP_ID] and App Secret is [YOUR_APP_SECRET]. The app should require a valid license to run. Show the license key input on startup, validate it, and exit gracefully if the license is revoked or expires. Refer to the AGENTS.md file for the SDK reference. ``` Replace the bracketed placeholders with your real credentials in a secure way (for example environment variables), not committed secrets. ## Where to find AGENTS.md Each file lives in the **repository root** of the matching SDK: * [Python](https://github.com/AuthForgeCC/authforge-python) * [C#](https://github.com/AuthForgeCC/authforge-csharp) * [C++](https://github.com/AuthForgeCC/authforge-cpp) * [Rust](https://github.com/AuthForgeCC/authforge-rust) * [Go](https://github.com/AuthForgeCC/authforge-go) * [Node.js](https://github.com/AuthForgeCC/authforge-node) Clone or browse the repo, open `AGENTS.md`, and attach it (or the raw file from the default branch) to your agent session. ## Supported AI tools This workflow works with Cursor, GitHub Copilot, Claude Code, Windsurf, and any assistant that can read project or pasted context files. ## Tips for better results * Add **`AGENTS.md`** from the matching GitHub repo so the agent matches real APIs and naming. Optionally include a small file that shows how you import the published package (for example `from authforge import …`, `using AuthForge;`, `import … from "@authforgecc/sdk"`). * State your **language and framework** explicitly (for example “.NET 8 WPF” or “Go CLI”). * Ask the agent to handle **failure paths** and server error codes, not only the happy path. * Compare generated code to the **Do NOT** section at the bottom of `AGENTS.md` before shipping. ## llms.txt For tools that support the [llms.txt](https://llmstxt.org/) convention, a site-wide summary is available at [https://docs.authforge.cc/llms.txt](https://docs.authforge.cc/llms.txt) for high-level AuthForge concepts and SDK links. # Build Your Own Integration Source: https://docs.authforge.cc/guides/custom-integration Use the AuthForge Developer API to build custom license management workflows from any backend. The Developer API lets you integrate AuthForge with any payment provider, CRM, or internal tool. This guide covers the patterns for building your own integration. ## Prerequisites * An AuthForge account with an app created * A [Developer API key](/api/overview#authentication) * A backend that can make HTTP requests ## Authentication All API requests require your API key in the `Authorization` header: ```bash theme={null} Authorization: Bearer af_live_your_key_here ``` ## Common patterns ### Generate a license after payment The most common integration: create a license when a customer pays, deliver the key. ```javascript theme={null} async function generateLicense({ appId, email, plan, orderId }) { // Create the license const response = await fetch("https://api.authforge.cc/v1/licenses", { method: "POST", headers: { Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ appId, quantity: 1, maxHwidSlots: plan === "enterprise" ? 5 : 1, label: orderId, }), }); if (!response.ok) { const error = await response.json(); throw new Error(`AuthForge API error: ${error.error}; ${error.message}`); } const { licenses } = await response.json(); const licenseKey = licenses[0].licenseKey; // Optionally set license variables for the plan tier if (plan !== "basic") { await setLicenseVariables(licenseKey, plan); } // Deliver to customer await sendEmail(email, licenseKey); return licenseKey; } ``` ### Bulk license generation Generate multiple keys at once (up to 100 per request): ```javascript theme={null} async function generateBulkLicenses(appId, quantity, options = {}) { const response = await fetch("https://api.authforge.cc/v1/licenses", { method: "POST", headers: { Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ appId, quantity, expiresAt: options.expiresAt || null, maxHwidSlots: options.maxHwidSlots || 1, label: options.label, }), }); const { licenses } = await response.json(); return licenses.map((l) => l.licenseKey); } // Generate 50 keys for a reseller const keys = await generateBulkLicenses("your-app-id", 50, { expiresAt: "2027-01-01T00:00:00Z", label: "Reseller batch #42", }); ``` ### License lookup and status check Check a license's status before taking action: ```javascript theme={null} async function checkLicense(licenseKey) { const response = await fetch( `https://api.authforge.cc/v1/licenses/${licenseKey}`, { headers: { Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`, }, } ); if (!response.ok) { if (response.status === 404) return null; throw new Error("API error"); } return await response.json(); } // Usage const license = await checkLicense("A3K9-BFWX-7NP2-QHDT"); if (license) { console.log(`Status: ${license.status}`); console.log(`Expires: ${license.expiresAt || "never"}`); console.log(`Devices: ${license.hwidList.length}/${license.maxHwidSlots}`); } ``` ### Customer support: HWID reset When a customer gets a new machine: ```javascript theme={null} async function resetHwid(licenseKey) { const response = await fetch( `https://api.authforge.cc/v1/licenses/${licenseKey}`, { method: "PUT", headers: { Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ action: "reset-hwid" }), } ); return response.ok; } ``` ### Pagination: iterate all licenses ```javascript theme={null} async function getAllLicenses(appId) { const allLicenses = []; let cursor = null; do { const url = new URL("https://api.authforge.cc/v1/licenses"); url.searchParams.set("appId", appId); url.searchParams.set("limit", "200"); if (cursor) url.searchParams.set("cursor", cursor); const response = await fetch(url.toString(), { headers: { Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`, }, }); const data = await response.json(); allLicenses.push(...data.licenses); cursor = data.cursor; } while (cursor); return allLicenses; } ``` ## Error handling Always handle API errors gracefully: ```javascript theme={null} async function safeApiCall(url, options) { const response = await fetch(url, options); if (!response.ok) { const body = await response.json().catch(() => ({})); switch (body.error) { case "no_credits": console.error("Out of credits; purchase more in the dashboard"); break; case "rate_limited": // Retry with exponential backoff await sleep(1000); return safeApiCall(url, options); case "forbidden": console.error("App doesn't belong to this account"); break; default: console.error(`API error: ${body.error}; ${body.message}`); } throw new Error(body.error || "unknown_error"); } return response.json(); } ``` ## Webhooks for event-driven workflows Instead of polling, use [webhooks](/features/webhooks) to react to license events: ```javascript theme={null} app.post("/webhooks/authforge", express.raw({ type: "application/json" }), (req, res) => { // Verify signature (see webhooks docs) const event = JSON.parse(req.body); switch (event.event) { case "license.validated": updateLastSeen(event.data.licenseKey); break; case "license.hwid_bound": logNewDevice(event.data.licenseKey, event.data.hwid); break; } res.sendStatus(200); }); ``` ## Language examples ### Python ```python theme={null} import requests API_KEY = os.environ["AUTHFORGE_API_KEY"] BASE_URL = "https://api.authforge.cc/v1" HEADERS = { "Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json", } def create_license(app_id, **kwargs): response = requests.post( f"{BASE_URL}/licenses", headers=HEADERS, json={"appId": app_id, **kwargs}, ) response.raise_for_status() return response.json()["licenses"] ``` ### Go ```go theme={null} func createLicense(appID string, quantity int) ([]License, error) { body, _ := json.Marshal(map[string]interface{}{ "appId": appID, "quantity": quantity, }) req, _ := http.NewRequest("POST", "https://api.authforge.cc/v1/licenses", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+os.Getenv("AUTHFORGE_API_KEY")) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req) if err != nil { return nil, err } defer resp.Body.Close() var result struct { Licenses []License `json:"licenses"` } json.NewDecoder(resp.Body).Decode(&result) return result.Licenses, nil } ``` ## Next steps * [API Reference](/api/overview); Full endpoint documentation * [Commerce](/features/commerce); Managed Stripe checkout → licenses * [Custom Stripe webhooks](/guides/stripe); Self-hosted Stripe webhook + Developer API * [Tiered Licensing](/guides/tiered-licensing); Feature gating with license variables # Discord Bot Source: https://docs.authforge.cc/guides/discord-bot Implement Discord user-based licensing with AuthForge using HWID override. This pattern binds licenses to Discord users by setting `hwidOverride = discord:`. ## Architecture 1. User runs a slash command (for example `/license activate`). 2. Bot asks for key via ephemeral response or DM. 3. Bot validates with AuthForge using override identity `discord:`. 4. On success, bot stores entitlement state and unlocks premium commands. ## Why this works * Discord user IDs are stable and immutable for the account. * AuthForge seat limits and reset behavior remain unchanged. * You get user-identity licensing without pretending a Discord bot has a real machine HWID. Use `user_id`, not usernames or display names. Use `heartbeatMode: "LOCAL"` for the bot process. **Server** heartbeat mode is for apps installed on the end user’s machine that periodically call AuthForge; a Discord bot only needs to validate keys and enforce session expiry on your server, not mirror an end-user “online” heartbeat model. For **cron jobs, per-message handlers, or any path that re-checks the license often**, prefer **`validateLicense`** (or the equivalent in your language: `validate_license`, `ValidateLicense`, `validate_license`, etc.). It performs the same `/auth/validate` request and signature verification as `login`, but **does not start heartbeat timers** and **does not require `logout()`** to clean up background work. One-time startup flows can still use `login` if you want a long-lived session plus LOCAL heartbeats. ## Node example (discord.js-style pseudocode) ```javascript theme={null} import { AuthForgeClient } from "@authforgecc/sdk"; async function validateDiscordKey(discordUserId, licenseKey) { const client = new AuthForgeClient({ appId: process.env.AUTHFORGE_APP_ID, appSecret: process.env.AUTHFORGE_APP_SECRET, publicKey: process.env.AUTHFORGE_PUBLIC_KEY, heartbeatMode: "LOCAL", hwidOverride: `discord:${discordUserId}`, }); const result = await client.validateLicense(licenseKey); if (!result.valid) { return { ok: false, code: result.code }; } return { ok: true, appVariables: result.appVariables, licenseVariables: result.licenseVariables }; } ``` ## Operational recommendations * Use ephemeral responses or DMs to avoid exposing keys in channels. * Rate limit invalid attempts to reduce brute force. * Use `maxHwidSlots=1` for strict per-user license behavior. * Document how users can request resets if they change accounts. ## Related * [HWID override](/features/hwid-override) * [HWID Override Guide](/guides/hwid-override) * [Security lists](/features/security) # Feature Flags with Variables Source: https://docs.authforge.cc/guides/feature-flags Use app variables as a simple remote feature flag system; toggle features without pushing an update. App variables let you control your application's behavior remotely. By setting boolean or string values in the dashboard, you can toggle features on or off for all users without deploying a new build. ## How it works 1. Set variables in the dashboard or via the API. 2. The SDK receives them during authentication. 3. Your app reads the variables and enables or disables features accordingly. ```mermaid theme={null} flowchart LR A["Set variable in Dashboard"] --> B["User authenticates"] B --> C["SDK receives appVariables"] C --> D["App checks variables"] D --> E["Feature enabled/disabled"] ``` ## Setup ### 1. Define your flags In the dashboard, go to your app's **Settings → Variables** and set your flags: ```json theme={null} { "newDashboard": false, "betaFeatures": true, "maxUploadSizeMb": 50, "maintenanceMode": false, "motd": "" } ``` Or via the API: ```bash theme={null} curl -X PUT https://api.authforge.cc/v1/apps/YOUR_APP_ID/variables \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "newDashboard": false, "betaFeatures": true, "maxUploadSizeMb": 50, "maintenanceMode": false, "motd": "" }' ``` ### 2. Read flags in your app ```python Python theme={null} if client.login(license_key): flags = client.app_variables # Kill switch if flags.get("maintenanceMode"): print("Application is under maintenance. Please try again later.") exit(0) # Message of the day motd = flags.get("motd", "") if motd: print(f"Notice: {motd}") # Feature toggle if flags.get("newDashboard"): show_new_dashboard() else: show_classic_dashboard() # Config value max_upload = int(flags.get("maxUploadSizeMb", 25)) configure_upload_limit(max_upload) ``` ```csharp C# theme={null} if (client.Login(licenseKey)) { var flags = client.AppVariables; // Kill switch if (flags.TryGetValue("maintenanceMode", out var maint) && maint is true) { Console.WriteLine("Application is under maintenance."); Environment.Exit(0); } // Feature toggle var useNewDashboard = flags.TryGetValue("newDashboard", out var nd) && nd is true; if (useNewDashboard) ShowNewDashboard(); else ShowClassicDashboard(); } ``` ### 3. Toggle remotely When you're ready to enable a feature, update the variable in the dashboard or via the API: ```bash theme={null} curl -X PUT https://api.authforge.cc/v1/apps/YOUR_APP_ID/variables \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "newDashboard": true, "betaFeatures": true, "maxUploadSizeMb": 50, "maintenanceMode": false, "motd": "New dashboard is live!" }' ``` Users pick up the change the next time they authenticate (restart the app or the SDK re-validates in LOCAL mode). ## Practical patterns ### Kill switch / maintenance mode Disable your app remotely without revoking licenses: ```python theme={null} if flags.get("maintenanceMode"): show_maintenance_screen() exit(0) ``` ### Minimum version enforcement Block outdated clients: ```python theme={null} import packaging.version min_ver = flags.get("minVersion") if min_ver and packaging.version.parse(APP_VERSION) < packaging.version.parse(min_ver): print(f"Please update to version {min_ver} or later.") show_update_dialog() exit(0) ``` ### Gradual rollout Use a percentage-based approach with the license key as a seed: ```python theme={null} import hashlib rollout_pct = int(flags.get("newFeatureRolloutPct", 0)) user_hash = int(hashlib.md5(license_key.encode()).hexdigest(), 16) % 100 if user_hash < rollout_pct: enable_new_feature() ``` ### A/B testing Assign users to groups based on their license key hash and configure behavior per group: ```python theme={null} group = "A" if int(hashlib.md5(license_key.encode()).hexdigest(), 16) % 2 == 0 else "B" theme = flags.get(f"theme_{group}", "default") ``` ## Limitations * Variables are delivered at authentication time, not real-time. Changes take effect on next login. * Max 50 keys, 4 KB total. For complex configuration, use a URL variable pointing to your own config endpoint. * Values are flat (string/number/boolean). No nested objects or arrays. ## Next steps * [App & License Variables](/features/variables); Full variable documentation * [Variables API](/api/variables); API reference # HWID Override Guide Source: https://docs.authforge.cc/guides/hwid-override Implement identity-based binding with AuthForge by overriding HWID in SDKs. This guide shows how to bind licenses to an external identity (Telegram/Discord user ID, account ID, etc.) by setting SDK HWID override fields. ## When to use this Use HWID override when device fingerprinting is the wrong abstraction: * bot users * web users * cloud workers * multi-tenant service identities For desktop apps, use default machine HWID behavior. ## Flow 1. User requests access in your app/bot. 2. Prompt for a license key. 3. Build a stable identity string (for example `tg:123456789`). 4. Initialize SDK with HWID override set to that identity. 5. Call `validateLicense()` / `validate_license` / `ValidateLicense` (or `login()` if you want a long-lived session and heartbeats). 6. Allow features only after success. Prefer **validate-license** APIs for **stateless or per-request** checks (API gateways, bots, cron): they run the same `/auth/validate` flow and signature verification as `login` without starting heartbeat threads or timers. ## Identity format and rules * Include a provider prefix (`tg:`, `discord:`, `user:`). * Use immutable IDs (platform numeric user IDs), not usernames. * Keep values under AuthForge limits (short strings are best). ## SDK examples ### Node ```javascript theme={null} const client = new AuthForgeClient({ appId: process.env.AUTHFORGE_APP_ID, appSecret: process.env.AUTHFORGE_APP_SECRET, publicKey: process.env.AUTHFORGE_PUBLIC_KEY, heartbeatMode: "SERVER", hwidOverride: `tg:${telegramUserId}`, }); ``` ### Python ```python theme={null} client = AuthForgeClient( app_id=APP_ID, app_secret=APP_SECRET, public_key=PUBLIC_KEY, heartbeat_mode="SERVER", hwid_override=f"discord:{discord_user_id}", ) ``` ### Go ```go theme={null} client, err := authforge.New(authforge.Config{ AppID: appID, AppSecret: appSecret, PublicKey: publicKey, HeartbeatMode: "server", HWIDOverride: fmt.Sprintf("tg:%d", telegramUserID), }) ``` ### C\# ```csharp theme={null} var client = new AuthForgeClient( appId: appId, appSecret: appSecret, publicKey: publicKey, heartbeatMode: "SERVER", hwidOverride: $"discord:{discordUserId}" ); ``` ### Rust ```rust theme={null} let client = AuthForgeClient::new(AuthForgeConfig { app_id: app_id.into(), app_secret: app_secret.into(), public_key: public_key.into(), heartbeat_mode: HeartbeatMode::Server, hwid_override: Some(format!("tg:{telegram_user_id}")), ..Default::default() }); ``` ### C++ ```cpp theme={null} authforge::AuthForgeClient client( appId, appSecret, publicKey, "SERVER", 900, authforge::AuthForgeClient::kDefaultApiBaseUrl, onFailure, 15, 0, "tg:" + std::to_string(telegramUserId) ); ``` ## Operational tips * Set sensible `maxHwidSlots` for your use case (often `1` for user-bound bots). * Provide a support flow to reset bindings when users migrate accounts. * If abuse is expected, combine with IP/HWID security lists and command rate limits. # Lemon Squeezy Source: https://docs.authforge.cc/guides/lemon Sell licenses through Lemon Squeezy with the managed Commerce pipeline. The supported way to sell licenses through Lemon Squeezy is **[Commerce](/features/commerce)**: paste your signing secret in the dashboard, map variants to license templates, and AuthForge verifies webhooks, deduplicates events, issues keys, and can email buyers for you. This guide walks through that setup. If you instead need to host your own webhook handler (custom checkout flows, bundles, or anything Commerce does not cover), use the [Custom integration guide](/guides/custom-integration) and call the Developer API directly when an order clears. ## Prerequisites * An AuthForge account with an app created * A Lemon Squeezy store with at least one product + variant ## How it maps to AuthForge Lemon Squeezy goes through the same Commerce pipeline as Stripe; ingress verifies signatures, inserts into the inbound event ledger, enqueues onto SQS, and a worker creates the license. The only differences are the signature header, the event names, and the identifier Lemon Squeezy assigns to each price. | Lemon Squeezy event | Commerce action | | ------------------------------- | ------------------------- | | `order_created` | Create license | | `subscription_payment_success` | Extend license on renewal | | `subscription_cancelled` | Revoke license | | `subscription_expired` | Revoke license | | `order_refunded` | Revoke license | | `subscription_payment_refunded` | Revoke license | AuthForge matches incoming events to a mapping by the Lemon Squeezy **variant ID** (the equivalent of a Stripe price). Every variant you sell that should issue a license needs one mapping row. ## Setup Commerce lives under **Dashboard → Commerce** and is account-global. You map Lemon Squeezy variants to specific apps inside the mappings table. ### 1. Create an AuthForge app Commerce delivers licenses *into* an app. If you don't have one yet, open **Dashboard → Applications → New application** first, then come back. ### 2. Connect your Lemon Squeezy store AuthForge stores both secrets encrypted in DynamoDB under a KMS key we never export. #### 2a. Register the webhook endpoint in Lemon Squeezy On the Commerce page, AuthForge shows a webhook URL of the form: ``` https://api.authforge.cc/billing/webhook/lemon/ ``` Copy it. In the **Lemon Squeezy dashboard**: 1. Go to **Settings → Webhooks → New webhook**. 2. Paste the URL from AuthForge into **Callback URL**. 3. Enter a **signing secret** (any random high-entropy string; you'll paste the same value into AuthForge in the next step). 4. Select the events AuthForge should receive: * `order_created`; required for purchases * `subscription_payment_success`; subscription renewals * `subscription_cancelled`; subscription cancellations * `subscription_expired`; subscription lapsing * `order_refunded`; refunds * `subscription_payment_refunded`; subscription refunds 5. Save. #### 2b. Paste the signing secret Back on the AuthForge Commerce page, paste the same signing secret into the **Lemon Squeezy signing secret** field. #### 2c. Paste a Lemon Squeezy API key (recommended) In the Lemon Squeezy dashboard, go to **Settings → API → Create API key**. Paste the token into the AuthForge **Lemon Squeezy API key** field. The API key is a fallback: Lemon Squeezy usually includes variant and product IDs on webhook payloads, but if an event ever lands without them (rare, but possible for some subscription lifecycle events), AuthForge uses this key to fetch the order/subscription and look them up. Without it, events that lack a variant are logged as `skipped · api_key_required_for_line_items` and no license is issued. Click **Connect Lemon Squeezy**. You should now see a green **Connected** chip next to the Lemon Squeezy section. ### 3. Map variants to license templates Still on the Commerce page, under **Product mappings**, click **New mapping**: 1. Pick **Lemon Squeezy** as the provider (only visible when you have more than one provider connected). 2. Pick the AuthForge app to deliver licenses into. 3. Paste the **Lemon Squeezy variant ID**. You can find it in Lemon Squeezy under **Products → open a product → Variants**: each variant has a numeric ID. 4. Choose **What kind of product is this?**: * **One-time purchase (lifetime or fixed length)**: leave **Access length** blank for a perpetual key, or enter days for a fixed-term license from a single payment. Renewal invoices do not extend the license—use a non-recurring variant for true one-time sales. * **Subscription; auto-renews and extends the license**: set **Billing period** to one billing cycle (`30` for monthly, `365` for yearly). The same mapping covers initial purchase, every renewal, and cancellations/refunds. * **Add-on; extends an existing customer's license**: for top-up SKUs only. Set days to the number of days to add. * **Revocation product**: almost never needed. Cancel/refund/dispute events already revoke automatically through the main mapping. 5. Set the **duration** in days (blank = lifetime) and **HWID slots** per key. 6. Optionally add **license variables** that get stamped onto every license this mapping creates. Your SDK exposes them as `licenseVariables` after the user logs in; use this for plan tier, feature flags, seat counts, etc. 7. Leave **Respect quantity** on unless you always want exactly one key per order regardless of what the buyer actually purchased. Save. Everything else; renewals, cancellations, refunds; flows through the same mapping automatically. **One mapping per variant ID is enough**: AuthForge stamps the Lemon Squeezy customer and subscription IDs on the license at create time so future renewal and cancellation events target the correct license, even when one customer holds multiple subscriptions. ## Testing the integration 1. In the Lemon Squeezy dashboard, open your webhook and click **Send test event** with `order_created`. 2. Refresh **Dashboard → Commerce → Recent events**. You should see the event with provider `lemon` and state `processed` (or `skipped` with a clear reason if something is misconfigured). 3. Check the relevant app's **Licenses** tab; the new key should be there, with the buyer's email attached and ready for the [self-service portal](/features/portal). Common failure states and what they mean: * `skipped · no_mapping_for_price`; the variant ID on the event does not match any mapping for this merchant. Add a mapping or correct the one you have. * `skipped · api_key_required_for_line_items`; the payload lacked a variant ID and no Lemon Squeezy API key is stored. Save an API key and Lemon Squeezy will redeliver the event on its next retry, or send a fresh test event. * `failed`; an unexpected error during license creation. The Commerce worker bumps `retryCount` and SQS will redeliver. Persistent failures end up in the DLQ (alarmed). ## Next steps * [Commerce](/features/commerce); Feature overview and dashboard walkthrough * [Self-service portal](/features/portal); Let buyers reset HWIDs and manage their own keys * [Subscription Lifecycle Management](/guides/subscriptions); Deeper patterns for subscription billing # Custom Stripe webhooks Source: https://docs.authforge.cc/guides/stripe Host your own Stripe webhook and call the Developer API when the managed Commerce pipeline is not enough. The supported way to sell licenses through Stripe is **[Commerce](/features/commerce)**: paste keys in the dashboard, map prices to license templates, and AuthForge verifies webhooks, deduplicates events, issues keys, and can email buyers for you. **This guide is the fallback:** integrate Stripe yourself by receiving webhooks on **your** server and calling `POST /v1/licenses` (and related endpoints). Reach for it when you need full control over Checkout sessions (custom line items, bundled purchases, bespoke metadata), or a flow Commerce does not cover yet. ## Prerequisites * An AuthForge account with an app created * An AuthForge [Developer API key](/api/overview#authentication) * A Stripe account with API keys * A Node.js backend (examples use Express.js) ## One-time purchases ### Flow overview ```mermaid theme={null} sequenceDiagram participant Customer participant YourSite as Your Website participant Stripe participant YourServer as Your Server participant AF as AuthForge API Customer->>YourSite: Click "Buy License" YourSite->>Stripe: Create Checkout Session Stripe-->>Customer: Redirect to Checkout Customer->>Stripe: Complete payment Stripe->>YourServer: Webhook: checkout.session.completed YourServer->>AF: POST /v1/licenses AF-->>YourServer: License key YourServer->>Customer: Deliver key (email / success page) ``` ### 1. Create a Stripe Checkout session ```javascript theme={null} const stripe = require("stripe")(process.env.STRIPE_SECRET_KEY); app.post("/api/create-checkout", async (req, res) => { const session = await stripe.checkout.sessions.create({ mode: "payment", payment_method_types: ["card"], line_items: [ { price: "price_your_product_price_id", quantity: 1, }, ], success_url: "https://yoursite.com/success?session_id={CHECKOUT_SESSION_ID}", cancel_url: "https://yoursite.com/pricing", metadata: { authforge_app_id: process.env.AUTHFORGE_APP_ID, license_duration: "365", // days, or omit for lifetime max_hwid_slots: "1", }, }); res.json({ url: session.url }); }); ``` ### 2. Handle the Stripe webhook ```javascript theme={null} const crypto = require("crypto"); app.post( "/webhooks/stripe", express.raw({ type: "application/json" }), async (req, res) => { const sig = req.headers["stripe-signature"]; let event; try { event = stripe.webhooks.constructEvent( req.body, sig, process.env.STRIPE_WEBHOOK_SECRET ); } catch (err) { console.error("Webhook signature verification failed:", err.message); return res.sendStatus(400); } if (event.type === "checkout.session.completed") { const session = event.data.object; await handleCompletedCheckout(session); } res.sendStatus(200); } ); ``` ### 3. Generate the license key ```javascript theme={null} async function handleCompletedCheckout(session) { const { authforge_app_id, license_duration, max_hwid_slots } = session.metadata; // Idempotency: check if we've already created a license for this session const existingLicense = await db.getLicenseByStripeSession(session.id); if (existingLicense) { console.log("License already created for session", session.id); return; } // Calculate expiration const expiresAt = license_duration ? new Date(Date.now() + parseInt(license_duration) * 86400000).toISOString() : null; // Create license via AuthForge API const response = await fetch("https://api.authforge.cc/v1/licenses", { method: "POST", headers: { Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ appId: authforge_app_id, quantity: 1, expiresAt, maxHwidSlots: parseInt(max_hwid_slots) || 1, label: `Stripe session ${session.id}`, }), }); const data = await response.json(); const licenseKey = data.licenses[0].licenseKey; // Store the mapping in your database await db.saveLicense({ stripeSessionId: session.id, customerEmail: session.customer_details.email, licenseKey, }); // Deliver the key to the customer await sendLicenseEmail(session.customer_details.email, licenseKey); } ``` ### 4. Deliver the key Options for delivering the license key to the customer: * **Email**: Send an automated email with the key after payment. * **Success page**: Redirect to a page that displays the key (retrieve it by session ID). * **Customer portal**: Store keys in your database and let users view them in their account. ```javascript theme={null} app.get("/success", async (req, res) => { const { session_id } = req.query; const license = await db.getLicenseByStripeSession(session_id); if (!license) { return res.status(404).send("License not found. Please check your email."); } res.render("success", { licenseKey: license.licenseKey }); }); ``` *** ## Subscriptions For recurring payments, you'll generate a license when the subscription starts and revoke it when the subscription ends. ### Events to handle | Stripe Event | AuthForge Action | | ------------------------------- | ------------------------- | | `customer.subscription.created` | Create a license key | | `customer.subscription.deleted` | Revoke the license key | | `customer.subscription.updated` | Extend expiry on renewal | | `invoice.payment_failed` | Optionally revoke or warn | ### Complete subscription handler ```javascript theme={null} app.post( "/webhooks/stripe", express.raw({ type: "application/json" }), async (req, res) => { const sig = req.headers["stripe-signature"]; let event; try { event = stripe.webhooks.constructEvent( req.body, sig, process.env.STRIPE_WEBHOOK_SECRET ); } catch (err) { return res.sendStatus(400); } switch (event.type) { case "customer.subscription.created": { const subscription = event.data.object; await handleSubscriptionCreated(subscription); break; } case "customer.subscription.deleted": { const subscription = event.data.object; await handleSubscriptionDeleted(subscription); break; } case "customer.subscription.updated": { const subscription = event.data.object; await handleSubscriptionUpdated(subscription); break; } case "invoice.payment_failed": { const invoice = event.data.object; await handlePaymentFailed(invoice); break; } } res.sendStatus(200); } ); ``` ### Subscription created; generate license ```javascript theme={null} async function handleSubscriptionCreated(subscription) { // Guard against duplicate processing (Stripe retries webhooks) const existing = await db.getLicenseBySubscription(subscription.id); if (existing) return; const response = await fetch("https://api.authforge.cc/v1/licenses", { method: "POST", headers: { Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ appId: process.env.AUTHFORGE_APP_ID, quantity: 1, expiresAt: new Date(subscription.current_period_end * 1000).toISOString(), maxHwidSlots: 1, label: `Stripe sub ${subscription.id}`, }), }); const data = await response.json(); const licenseKey = data.licenses[0].licenseKey; await db.saveSubscriptionLicense({ stripeSubscriptionId: subscription.id, customerId: subscription.customer, licenseKey, }); const customer = await stripe.customers.retrieve(subscription.customer); await sendLicenseEmail(customer.email, licenseKey); } ``` ### Subscription deleted; revoke license ```javascript theme={null} async function handleSubscriptionDeleted(subscription) { const record = await db.getLicenseBySubscription(subscription.id); if (!record) return; await fetch( `https://api.authforge.cc/v1/licenses/${record.licenseKey}`, { method: "PUT", headers: { Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ action: "revoke" }), } ); } ``` ### Subscription updated; extend expiry on renewal ```javascript theme={null} async function handleSubscriptionUpdated(subscription) { if (subscription.status !== "active") return; const record = await db.getLicenseBySubscription(subscription.id); if (!record) return; await fetch( `https://api.authforge.cc/v1/licenses/${record.licenseKey}`, { method: "PUT", headers: { Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ action: "extend", expiresAt: new Date(subscription.current_period_end * 1000).toISOString(), }), } ); } ``` ### Payment failed; optional revocation ```javascript theme={null} async function handlePaymentFailed(invoice) { const subscriptionId = invoice.subscription; if (!subscriptionId) return; const record = await db.getLicenseBySubscription(subscriptionId); if (!record) return; // Option 1: Revoke immediately // await revokeLicense(record.licenseKey); // Option 2: Send a warning email, revoke after grace period const customer = await stripe.customers.retrieve(invoice.customer); await sendPaymentFailedEmail(customer.email); } ``` ## Idempotency Stripe retries webhook deliveries on failure. Guard against duplicate license creation by: 1. Storing the Stripe session/subscription ID alongside each license in your database. 2. Checking for an existing record before creating a new license. 3. Returning early if a license already exists for that payment. ## Next steps * [Commerce](/features/commerce); Managed Stripe setup (start here unless you need this guide) * [Subscription Lifecycle Management](/guides/subscriptions); Detailed subscription patterns * [Custom Integration Guide](/guides/custom-integration); Build your own payment integration # Subscription Lifecycle Management Source: https://docs.authforge.cc/guides/subscriptions Manage the full lifecycle of subscription-based licenses: creation, renewal, cancellation, and edge cases. This guide covers patterns for managing licenses tied to recurring subscriptions, whether you use Stripe, Paddle, or any other payment provider. ## Lifecycle overview ```mermaid theme={null} stateDiagram-v2 [*] --> Created: Payment received Created --> Active: User authenticates Active --> Renewed: Subscription renews Renewed --> Active: Expiry extended Active --> GracePeriod: Payment fails GracePeriod --> Active: Payment retried GracePeriod --> Revoked: Grace period ends Active --> Cancelled: User cancels Cancelled --> Revoked: Period ends Revoked --> Active: User re-subscribes Revoked --> [*]: License deleted ``` ## Creating a subscription license When a subscription starts, create a license with an expiration matching the billing period: ```javascript theme={null} const response = await fetch("https://api.authforge.cc/v1/licenses", { method: "POST", headers: { Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ appId: process.env.AUTHFORGE_APP_ID, quantity: 1, expiresAt: periodEndDate.toISOString(), maxHwidSlots: 1, label: `sub_${subscriptionId}`, }), }); ``` Store the mapping between your subscription ID and the AuthForge license key in your database. ## Renewal When a subscription renews successfully, extend the license expiration: ```javascript theme={null} await fetch(`https://api.authforge.cc/v1/licenses/${licenseKey}`, { method: "PUT", headers: { Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ action: "extend", expiresAt: newPeriodEndDate.toISOString(), }), }); ``` The user's SDK session continues uninterrupted; the next heartbeat or re-authentication will pick up the new expiry. ## Cancellation When a user cancels, you have two options: ### Option A: Immediate revocation Revoke the license immediately on cancellation: ```javascript theme={null} await fetch(`https://api.authforge.cc/v1/licenses/${licenseKey}`, { method: "PUT", headers: { Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ action: "revoke" }), }); ``` ### Option B: Let it expire Don't revoke; let the license expire at the end of the already-paid period. This is the more user-friendly approach. The license `expiresAt` was set to the billing period end, so it will naturally stop working. Option B is the standard practice. Users have paid for the current period and should retain access until it ends. ## Payment failure When a payment fails, decide how to handle the grace period: ```javascript theme={null} async function handlePaymentFailed(subscriptionId, attemptCount) { const record = await db.getLicenseBySubscription(subscriptionId); if (!record) return; if (attemptCount >= 3) { // Final attempt failed; revoke await revokeLicense(record.licenseKey); await notifyCustomer(record.email, "subscription_cancelled"); } else { // Early attempt; warn the user but keep the license active await notifyCustomer(record.email, "payment_failed"); } } ``` ## Re-subscription When a user re-subscribes after cancellation or revocation: 1. Check if they have an existing (revoked) license key. 2. If yes, re-activate it and extend the expiry; this preserves their HWID bindings. 3. If no, create a new license. ```javascript theme={null} async function handleResubscription(customerId, newPeriodEnd) { const existing = await db.getLicenseByCustomer(customerId); if (existing) { // Re-activate the existing license await fetch(`https://api.authforge.cc/v1/licenses/${existing.licenseKey}`, { method: "PUT", headers: { Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ action: "activate" }), }); // Extend expiry await fetch(`https://api.authforge.cc/v1/licenses/${existing.licenseKey}`, { method: "PUT", headers: { Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ action: "extend", expiresAt: newPeriodEnd.toISOString(), }), }); } else { // Create a fresh license await createNewLicense(customerId, newPeriodEnd); } } ``` ## Plan upgrades and downgrades Use [license variables](/features/variables#license-variables) to control features per plan tier. When a user upgrades or downgrades: ```javascript theme={null} async function handlePlanChange(licenseKey, newPlan) { const planConfig = { basic: { plan: "basic", maxProjects: 3 }, pro: { plan: "pro", maxProjects: 10 }, enterprise: { plan: "enterprise", maxProjects: 50 }, }; await fetch( `https://api.authforge.cc/v1/licenses/${licenseKey}/variables`, { method: "PUT", headers: { Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(planConfig[newPlan]), } ); } ``` The user picks up the new plan features the next time they authenticate. ## Database schema suggestion Store the mapping between your payment provider and AuthForge: ```sql theme={null} CREATE TABLE subscription_licenses ( id SERIAL PRIMARY KEY, customer_id TEXT NOT NULL, customer_email TEXT NOT NULL, subscription_id TEXT UNIQUE NOT NULL, license_key TEXT UNIQUE NOT NULL, plan TEXT NOT NULL DEFAULT 'basic', status TEXT NOT NULL DEFAULT 'active', created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW() ); ``` ## Next steps * [Commerce](/features/commerce); Subscription renewals and cancellations via mapped Stripe prices * [Custom Stripe webhooks](/guides/stripe); Self-hosted webhook handler examples * [Tiered Licensing](/guides/tiered-licensing); Implement plan tiers with license variables # Telegram Bot Source: https://docs.authforge.cc/guides/telegram-bot Implement Telegram user-based licensing with AuthForge using HWID override. This pattern lets anyone message your bot, but only licensed users get full responses. ## Architecture 1. User sends a message to your bot. 2. If not licensed, bot asks for a license key. 3. User submits key in DM/private chat. 4. Bot validates key with AuthForge and sets `hwidOverride = tg:`. 5. If valid, store "licensed" status for that Telegram user. ## Why this works * AuthForge treats HWID as an identity string. * `tg:` is stable per Telegram account. * Seat controls (`maxHwidSlots`) and reset flows still work. Use `user_id`, not `username` (usernames can change). Use `heartbeatMode: "LOCAL"` (or `heartbeat_mode="LOCAL"` in Python) for the bot process. **Server** heartbeat mode targets software running on the licensee’s device; a Telegram bot only performs validation on your infrastructure, so local session checks after `login` are enough. For **every scheduled run or every message**, use **`validateLicense`** / **`validate_license`** (same `/auth/validate` + Ed25519 verification as `login`) so you **avoid heartbeat timers** and **never need `logout()`** just to stop background intervals. Keep `login` for processes that intentionally hold one long-lived session. ## Node example (Telegraf-style pseudocode) ```javascript theme={null} import { AuthForgeClient } from "@authforgecc/sdk"; async function validateTelegramKey(telegramUserId, licenseKey) { const client = new AuthForgeClient({ appId: process.env.AUTHFORGE_APP_ID, appSecret: process.env.AUTHFORGE_APP_SECRET, publicKey: process.env.AUTHFORGE_PUBLIC_KEY, heartbeatMode: "LOCAL", hwidOverride: `tg:${telegramUserId}`, }); const result = await client.validateLicense(licenseKey); return result.valid ? result : { valid: false, code: result.code }; } ``` ## Python example ```python theme={null} from authforge import AuthForgeClient def validate_telegram_key(user_id: int, license_key: str) -> dict: client = AuthForgeClient( app_id=APP_ID, app_secret=APP_SECRET, public_key=PUBLIC_KEY, heartbeat_mode="LOCAL", hwid_override=f"tg:{user_id}", ) return client.validate_license(license_key) ``` ## Recommended bot behavior * Keep key entry in private chat. * Rate limit invalid attempts per user and per IP. * Return clear error text for `invalid_key`, `expired`, `revoked`, `hwid_mismatch`. * Provide a support command to request reset help when users switch accounts. ## Related * [HWID override](/features/hwid-override) * [HWID Override Guide](/guides/hwid-override) * [License API](/api/licenses) # Tiered Licensing with License Variables Source: https://docs.authforge.cc/guides/tiered-licensing Implement Basic, Pro, and Enterprise plan tiers using license variables for per-user feature gating. License variables let you attach per-user metadata to each license key. By setting a `plan` variable (and feature-specific limits), you can gate functionality in your application based on the customer's subscription tier. ## Architecture ```mermaid theme={null} flowchart LR A["Stripe Payment"] --> B["Your Server"] B --> C["AuthForge API: Create License"] B --> D["AuthForge API: Set License Variables"] D --> E["SDK reads licenseVariables"] E --> F["App enables tier features"] ``` ## Step 1: Define your tiers Decide what each tier includes: | Feature | Basic | Pro | Enterprise | | ---------------- | ----- | --- | ---------- | | Max projects | 3 | 10 | 50 | | Export | No | Yes | Yes | | API access | No | Yes | Yes | | Priority support | No | No | Yes | | SSO | No | No | Yes | This maps to license variables: ```json theme={null} // Basic { "plan": "basic", "maxProjects": 3 } // Pro { "plan": "pro", "maxProjects": 10, "features": "export,api" } // Enterprise { "plan": "enterprise", "maxProjects": 50, "features": "export,api,priority-support,sso" } ``` ## Step 2: Create licenses with tier variables When a customer subscribes, create a license and set the variables: ```javascript theme={null} const PLAN_PRESETS = { basic: { plan: "basic", maxProjects: 3 }, pro: { plan: "pro", maxProjects: 10, features: "export,api" }, enterprise: { plan: "enterprise", maxProjects: 50, features: "export,api,priority-support,sso", }, }; async function createTieredLicense(appId, plan, label) { // Create the license const createRes = await fetch("https://api.authforge.cc/v1/licenses", { method: "POST", headers: { Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ appId, quantity: 1, maxHwidSlots: plan === "enterprise" ? 5 : 1, label, }), }); const { licenses } = await createRes.json(); const licenseKey = licenses[0].licenseKey; // Set tier variables await fetch( `https://api.authforge.cc/v1/licenses/${licenseKey}/variables`, { method: "PUT", headers: { Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(PLAN_PRESETS[plan]), } ); return licenseKey; } ``` ## Step 3: Read tier variables in the SDK ```python Python theme={null} if client.login(license_key): plan = client.license_variables.get("plan", "basic") max_projects = int(client.license_variables.get("maxProjects", 3)) features = client.license_variables.get("features", "").split(",") print(f"Plan: {plan} | Max projects: {max_projects}") if "export" in features: enable_export() if "api" in features: enable_api_access() if "sso" in features: enable_sso() ``` ```csharp C# theme={null} if (client.Login(licenseKey)) { var vars = client.LicenseVariables; var plan = vars.GetValueOrDefault("plan", "basic")?.ToString() ?? "basic"; var maxProjects = int.Parse(vars.GetValueOrDefault("maxProjects", "3")?.ToString() ?? "3"); var features = (vars.GetValueOrDefault("features", "")?.ToString() ?? "").Split(','); Console.WriteLine($"Plan: {plan} | Max projects: {maxProjects}"); if (features.Contains("export")) EnableExport(); if (features.Contains("api")) EnableApiAccess(); if (features.Contains("sso")) EnableSso(); } ``` ## Step 4: Handle plan upgrades When a customer upgrades (e.g., Basic → Pro), update their license variables: ```javascript theme={null} async function upgradePlan(licenseKey, newPlan) { await fetch( `https://api.authforge.cc/v1/licenses/${licenseKey}/variables`, { method: "PUT", headers: { Authorization: `Bearer ${process.env.AUTHFORGE_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify(PLAN_PRESETS[newPlan]), } ); } ``` The user picks up the new plan the next time they authenticate. ## Stripe integration Map Stripe price IDs to plan tiers: ```javascript theme={null} const PRICE_TO_PLAN = { price_basic_monthly: "basic", price_basic_yearly: "basic", price_pro_monthly: "pro", price_pro_yearly: "pro", price_enterprise_monthly: "enterprise", price_enterprise_yearly: "enterprise", }; async function handleCheckoutCompleted(session) { const lineItems = await stripe.checkout.sessions.listLineItems(session.id); const priceId = lineItems.data[0].price.id; const plan = PRICE_TO_PLAN[priceId] || "basic"; const licenseKey = await createTieredLicense( process.env.AUTHFORGE_APP_ID, plan, `Stripe ${session.id}` ); await db.saveSubscription({ customerId: session.customer, subscriptionId: session.subscription, licenseKey, plan, }); await sendLicenseEmail(session.customer_details.email, licenseKey); } ``` ## Displaying tier info in the app Show users what their plan includes and offer upgrade prompts: ```python theme={null} if client.login(license_key): plan = client.license_variables.get("plan", "basic") if plan == "basic": show_upgrade_banner( "Upgrade to Pro for export, API access, and 10 projects.", url="https://yoursite.com/upgrade" ) ``` ## Next steps * [App & License Variables](/features/variables); Variable documentation * [Commerce](/features/commerce); Sell tiers through Stripe with product mappings * [Custom Stripe webhooks](/guides/stripe); Self-hosted Stripe integration when you need it * [Subscription Lifecycle](/guides/subscriptions); Managing subscription state # Welcome to AuthForge Source: https://docs.authforge.cc/introduction Credit-based software licensing for developers. Protect your apps with hardware-bound license keys, automatic heartbeats, and a simple SDK. AuthForge is a credit-based software licensing platform. You create apps, generate license keys, and the AuthForge SDK handles authentication, HWID binding, and heartbeat verification automatically. ## What AuthForge does * **License key authentication**: Generate keys in the dashboard or via the Developer API. End users enter a key, and the SDK validates it against the AuthForge server. * **Hardware binding (HWID)**: Each license is bound to the user's machine fingerprint (CPU, MAC address, disk serial). Configure how many devices a single key can activate. * **Background heartbeats**: After login, the SDK periodically verifies the license is still valid. Revoke a key from the dashboard and it takes effect on the next heartbeat. * **Cryptographic verification**: Every `/auth/validate` and `/auth/heartbeat` success response is Ed25519-signed with your app's private signing key. SDKs verify the signature on every response; `/auth/validate` additionally enforces nonce matching to prevent replay and tampering. ## How licensing works ```mermaid theme={null} sequenceDiagram participant Dev as You (Developer) participant Dash as AuthForge Dashboard participant App as Your Application participant SDK as AuthForge SDK participant API as AuthForge API Dev->>Dash: Create app, get App ID + Secret Dev->>Dash: Generate license keys Dev->>App: Embed SDK with App ID + Secret App->>SDK: Initialize client Note over App: End user launches your app App->>SDK: client.login(licenseKey) SDK->>API: POST /auth/validate (appId, secret, key, HWID, nonce) API-->>SDK: Signed payload + session token SDK-->>App: Returns true (authenticated) loop Every 15 minutes SDK->>API: POST /auth/heartbeat (session token) API-->>SDK: Signed confirmation end ``` 1. **Create an app** in the AuthForge dashboard. You get an App ID and App Secret. 2. **Generate license keys**: one per customer, or in bulk via the Developer API. 3. **Integrate the SDK** into your application. Pass your App ID and App Secret to the client constructor. 4. **End users authenticate** by entering their license key. The SDK collects a hardware fingerprint, sends it to the API, and the server binds the key to that machine. 5. **Heartbeats run in the background** to keep the session alive and catch revocations. ## Credit model AuthForge uses a credit-based billing model: | Action | Credit cost | | ------------------------------------------------ | ----------- | | Successful license validation (`/auth/validate`) | 1 credit | | 10 successful heartbeats | 1 credit | Credits are purchased in the dashboard. [Set up auto-refill](/best-practices/credit-management) to avoid running out. ## Next steps Get your first app protected in 5 minutes. Official packages for Python, C#, C++, Rust, Go, and Node.js (CMake + GitHub for C++). Automate license management from your backend. Understand how AuthForge works under the hood. # Quick Start Source: https://docs.authforge.cc/quickstart Get your first app protected with AuthForge in under 5 minutes. ## 1. Create an account Sign up at [authforge.cc](https://app.authforge.cc/auth). You'll receive free credits to get started. ## 2. Create an app Go to the [Dashboard](https://app.authforge.cc/dashboard) and click **Create App**. Give it a name; this represents the software you're protecting. ## 3. Copy your credentials After creating the app, copy your **App ID**, **App Secret**, and **Public Key**. The secret is shown once; store it securely. Your App Secret authenticates validate requests. Your Public Key verifies signed server responses. Keep the secret private; the public key is safe to embed in shipped SDK code. ## 4. Install the SDK Add the official package for your stack (import the `authforge` module after `pip install authforge-sdk`). C++ ships as a CMake library from GitHub; see the C++ SDK page for `FetchContent` or `find_package`. Full options and versions are on each language page. ```bash theme={null} pip install authforge-sdk dotnet add package AuthForge cargo add authforge npm install @authforgecc/sdk go get github.com/AuthForgeCC/authforge-go@v1.0.1 ``` PyPI **`authforge-sdk`**: Python 3.9+ NuGet **`AuthForge`**: .NET 6+ CMake + GitHub; C++17 npm **`@authforgecc/sdk`**: Node.js 18+ TypeScript: the npm package ships `authforge.d.ts`; import from `@authforgecc/sdk` like JavaScript. ## 5. Add the SDK to your project ```python Python theme={null} from authforge import AuthForgeClient client = AuthForgeClient( app_id="YOUR_APP_ID", app_secret="YOUR_APP_SECRET", public_key="YOUR_PUBLIC_KEY", heartbeat_mode="SERVER", ) license_key = input("Enter license key: ") if client.login(license_key): print("Authenticated! Running app...") # Your app logic here; heartbeats run automatically in background else: print("Invalid license key.") exit(1) ``` ```csharp C# theme={null} using AuthForge; var client = new AuthForgeClient( appId: "YOUR_APP_ID", appSecret: "YOUR_APP_SECRET", publicKey: "YOUR_PUBLIC_KEY", heartbeatMode: "SERVER" ); Console.Write("Enter license key: "); var key = Console.ReadLine() ?? ""; if (client.Login(key)) { Console.WriteLine("Authenticated! Running app..."); // Your app logic here; heartbeats run automatically } else { Console.WriteLine("Invalid license key."); Environment.Exit(1); } ``` ```cpp C++ theme={null} #include "authforge_sdk.h" #include #include int main() { authforge::AuthForgeClient client( "YOUR_APP_ID", "YOUR_APP_SECRET", "YOUR_PUBLIC_KEY", "SERVER" ); std::string key; std::cout << "Enter license key: "; std::getline(std::cin, key); if (client.Login(key)) { std::cout << "Authenticated! Running app..." << std::endl; // Your app logic here; heartbeats run automatically } else { std::cout << "Invalid license key." << std::endl; return 1; } return 0; } ``` ```rust Rust theme={null} use authforge::{AuthForgeClient, AuthForgeConfig, HeartbeatMode}; fn main() { let client = AuthForgeClient::new(AuthForgeConfig { app_id: "YOUR_APP_ID".into(), app_secret: "YOUR_APP_SECRET".into(), public_key: "YOUR_PUBLIC_KEY".into(), heartbeat_mode: HeartbeatMode::Server, ..Default::default() }); print!("Enter license key: "); let mut key = String::new(); std::io::stdin().read_line(&mut key).unwrap(); let key = key.trim(); match client.login(key) { Ok(_) => { println!("Authenticated! Running app..."); // Your app logic here; heartbeats run automatically } Err(e) => { eprintln!("Login failed: {}", e); std::process::exit(1); } } } ``` ```go Go theme={null} package main import ( "bufio" "fmt" "os" "strings" "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", }) if err != nil { fmt.Fprintf(os.Stderr, "Init failed: %v\n", err) os.Exit(1) } reader := bufio.NewReader(os.Stdin) fmt.Print("Enter license key: ") key, _ := reader.ReadString('\n') key = strings.TrimSpace(key) if _, err := client.Login(key); err != nil { fmt.Fprintf(os.Stderr, "Login failed: %v\n", err) os.Exit(1) } fmt.Println("Authenticated! Running app...") // Your app logic here; heartbeats run automatically } ``` ```javascript Node.js theme={null} import { AuthForgeClient } from '@authforgecc/sdk'; import * as readline from 'node:readline/promises'; const client = new AuthForgeClient({ appId: 'YOUR_APP_ID', appSecret: 'YOUR_APP_SECRET', publicKey: 'YOUR_PUBLIC_KEY', heartbeatMode: 'SERVER', }); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const key = await rl.question('Enter license key: '); rl.close(); if (await client.login(key)) { console.log('Authenticated! Running app...'); // Your app logic here; heartbeats run automatically in the background } else { console.error('Invalid license key.'); process.exit(1); } ``` ## 6. Create a license key In the dashboard, open your app and click **Generate Licenses**. Set the quantity, expiration (or lifetime), and HWID slots (how many devices can use the same key). Click **Generate**. Copy one of the generated keys; the format is `XXXX-XXXX-XXXX-XXXX`. ## 7. Run your app Launch your application and enter the license key when prompted. You should see "Authenticated!"; the license is now active and bound to your machine. ## 8. What just happened? Here's what the SDK did behind the scenes: 1. **Collected HWID**: The SDK fingerprinted your machine by collecting stable hardware identifiers and hashing them into a SHA-256 string. The exact identifiers vary by SDK language (e.g., MAC address, CPU, disk serial, hostname). 2. **Generated a nonce**: A random string to prevent replay attacks. Every request uses a fresh nonce. 3. **Sent a validate request**: `POST /auth/validate` with your App ID, App Secret, license key, HWID, and nonce. 4. **Server validated**: The server checked the license exists, is active, hasn't expired, and the HWID is allowed (or bound it to a new slot). One credit was deducted from your account. 5. **Signed the response**: The server built a JSON payload (session token, app/license variables, etc.) and signed the base64 payload with the app's Ed25519 private key. 6. **SDK verified**: The SDK verified the signature with your app's public key (`public_key`). This proves the response came from AuthForge and wasn't tampered with in transit. 7. **Heartbeats started**: A background thread now sends `POST /auth/heartbeat` every 15 minutes (default). Each heartbeat response is also Ed25519-signed with the same app keypair, and SDK verification uses the same public key. ## Next steps * [SDK Best Practices](/sdk/best-practices); How to handle errors, offline mode, and graceful shutdown * [Core Concepts](/concepts); Understand HWIDs, heartbeat modes, credits, and more * [Commerce](/features/commerce); Connect Stripe and automate license delivery # SDK Integration Best Practices Source: https://docs.authforge.cc/sdk/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.

Enter License Key

XXXX-XXXX-XXXX-XXXX
Purchase Activate
### 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." | | Network error | No connectivity | "Could not connect to authentication server. Check your internet connection." | Never expose `no_credits` to end users; this is your billing issue, not theirs. Show a generic "temporarily unavailable" message. ### 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 **Don't kill the app immediately.** The user could lose unsaved work. A heartbeat failure might be a momentary network blip. ### 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: ```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 ); ``` *** ## 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 Don't log the app secret anywhere. Don't store it in plain text in the binary. Use environment variables or encrypted configuration. Call `login()` early in your app's startup, not lazily. Don't let the app run unprotected code paths before authentication. Don't expose internal auth failure details to the user. "The signature check failed" helps attackers; just say "Authentication failed." Assume the binary can be modified. Critical business logic that depends on license status should check variables, not just a boolean flag. LOCAL mode can be bypassed by freezing the system clock. SERVER mode requires a valid signed response from the API. # C++ SDK Source: https://docs.authforge.cc/sdk/cpp Integrate AuthForge into your C++ application with the official CMake library from GitHub. ## Requirements * C++17 or later * **libsodium**: Ed25519 signature verification * **OpenSSL**: SHA-256 and helpers * **libcurl**: HTTPS requests ## Installation There is no central C++ package registry. Consume the official SDK from [GitHub; AuthForgeCC/authforge-cpp](https://github.com/AuthForgeCC/authforge-cpp) (use a **release tag** under Releases). ### Option A: `FetchContent` (CMake) Pin a tag (for example `v1.0.1`) and link the `authforge_sdk` target: ```cmake theme={null} include(FetchContent) FetchContent_Declare( authforge_cpp GIT_REPOSITORY https://github.com/AuthForgeCC/authforge-cpp.git GIT_TAG v1.0.1 ) FetchContent_MakeAvailable(authforge_cpp) target_link_libraries(your_app PRIVATE authforge_sdk) ``` ### Option B: Install prefix + `find_package` Build and install the SDK, then point CMake at the prefix: ```bash theme={null} git clone https://github.com/AuthForgeCC/authforge-cpp.git cd authforge-cpp cmake -S . -B build -DCMAKE_INSTALL_PREFIX=../authforge-install cmake --build build cmake --install build ``` In your application: ```cmake theme={null} list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../authforge-install") find_package(AuthForge CONFIG REQUIRED) target_link_libraries(your_app PRIVATE AuthForge::authforge_sdk) ``` ### Dependencies Install development packages for **libsodium**, **OpenSSL**, and **libcurl** before configuring CMake. Examples: * **Linux (Debian/Ubuntu):** `sudo apt install libsodium-dev libssl-dev libcurl4-openssl-dev` * **macOS (Homebrew):** `brew install libsodium openssl curl`; set `CMAKE_PREFIX_PATH` if CMake does not find Homebrew prefixes. * **Windows:** Use [vcpkg](https://vcpkg.io/) for `libsodium`, `openssl`, and `curl`, then pass `-DCMAKE_TOOLCHAIN_FILE=.../vcpkg.cmake`. ## Quick start ```cpp theme={null} #include "authforge_sdk.h" #include #include int main() { authforge::AuthForgeClient client( "YOUR_APP_ID", "YOUR_APP_SECRET", "YOUR_PUBLIC_KEY", "SERVER" ); std::string key; std::cout << "Enter license key: "; std::getline(std::cin, key); if (client.Login(key)) { std::cout << "Authenticated!" << std::endl; // Your app logic here; heartbeats run in the background } else { std::cout << "Invalid license key." << std::endl; return 1; } return 0; } ``` ## Constructor parameters ```cpp theme={null} authforge::AuthForgeClient client( "app_id", // Required; from dashboard "app_secret", // Required; from dashboard "public_key", // Required; Ed25519 public key (base64) "SERVER", // Required; "SERVER" or "LOCAL" 900, // Optional; heartbeat interval in seconds (default: 900, any value ≥ 1) "https://auth.authforge.cc", // Optional; API base URL on_failure_callback, // Optional; void(const std::string& reason, const std::exception* ex) 15, // Optional; HTTP timeout in seconds 0, // Optional; ttlSeconds: requested session TTL. 0 = server default (24h). Clamped to [3600, 604800]. "" // Optional; hwidOverride: custom identity (for example "tg:123456789") ); ``` ### `ttlSeconds` Requested session token lifetime in seconds for `/auth/validate`. Pass `0` (or omit) to accept the server default of 24 hours. The server clamps to `[3600, 604800]` (1 hour to 7 days). The requested TTL is preserved across heartbeat refreshes so long-running apps in 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 heartbeat interval ≥ 1 is economically safe. * Revocations take effect on the **next** heartbeat regardless of interval. ## Login ```cpp theme={null} bool success = client.Login(license_key); ``` Returns `true` on success, `false` otherwise. Starts background heartbeats on success. ## Validate license (no heartbeat) ```cpp theme={null} authforge::ValidateLicenseResult r = client.ValidateLicense(license_key); if (r.valid) { // r.sessionToken, r.expiresIn, r.sessionDataJson, r.appVariablesJson, r.licenseVariablesJson } else { // r.errorCode; e.g. invalid_key, nonce_mismatch } ``` Same `/auth/validate` request and Ed25519 verification as `Login`, without persisting session fields on the client or starting the heartbeat thread. Does **not** invoke the failure callback or `std::exit` on error; inspect `valid` / `errorCode` instead. ## Failure callback If no callback is set (or the callback throws), the SDK calls `std::exit(1)`. ```cpp theme={null} #include "authforge_sdk.h" #include void on_failure(const std::string& reason, const std::exception* ex) { if (reason == "login_failed") { std::cerr << "Login failed." << std::endl; } else if (reason == "heartbeat_failed") { std::cerr << "Heartbeat failed; saving state." << std::endl; save_application_state(); } if (ex) { std::cerr << " Detail: " << ex->what() << std::endl; } } int main() { authforge::AuthForgeClient client( "YOUR_APP_ID", "YOUR_APP_SECRET", "YOUR_PUBLIC_KEY", "SERVER", 900, "https://auth.authforge.cc", on_failure ); // ... } ``` If you don't set an `onFailure` callback, the SDK terminates the process immediately via `std::exit(1)`. Always set a callback in production. ## Heartbeat modes ```cpp theme={null} // SERVER mode; pings the API every interval authforge::AuthForgeClient client("...", "...", "...", "SERVER"); // LOCAL mode; verifies locally, re-validates when the session token expires (default 24h; up to 7d via ttlSeconds) authforge::AuthForgeClient client("...", "...", "...", "LOCAL"); ``` See [Heartbeat Modes](/concepts#heartbeat-modes) for a detailed comparison. ## Full example (game) ```cpp theme={null} #include "authforge_sdk.h" #include #include #include std::atomic g_licensed{false}; void on_auth_failure(const std::string& reason, const std::exception* ex) { std::cerr << "[AuthForge] " << reason; if (ex) std::cerr << ": " << ex->what(); std::cerr << std::endl; g_licensed.store(false); // Don't exit here; let the game loop handle shutdown gracefully } int main() { authforge::AuthForgeClient client( "YOUR_APP_ID", "YOUR_APP_SECRET", "YOUR_PUBLIC_KEY", "SERVER", 900, "https://auth.authforge.cc", on_auth_failure ); std::string key; std::cout << "Enter license key: "; std::getline(std::cin, key); if (!client.Login(key)) { std::cerr << "Invalid license key." << std::endl; return 1; } g_licensed.store(true); std::cout << "Licensed. Starting game..." << std::endl; // Game loop while (g_licensed.load()) { // Update game state // Render frame // If g_licensed becomes false, show "license expired" dialog } std::cout << "License expired. Shutting down." << std::endl; return 0; } ``` ## Platform notes | Platform | HWID sources | Notes | | -------- | ---------------------------------------------------- | -------------------------------------------- | | Windows | `GetAdaptersAddresses`, WMI, `GetVolumeInformation` | Works out of the box with Windows SDK | | Linux | `/sys/class/net/*/address`, `/proc/cpuinfo`, `lsblk` | Standard filesystem access | | macOS | `getifaddrs`, `sysctl`, `diskutil` | May require Full Disk Access for disk serial | ## GitHub Full source, changelog, and issues: [AuthForgeCC/authforge-cpp](https://github.com/AuthForgeCC/authforge-cpp) # C# SDK Source: https://docs.authforge.cc/sdk/csharp Integrate AuthForge into your .NET application with the official NuGet package. ## Requirements * .NET 6.0 or later * `BouncyCastle.Cryptography` (pulled in transitively with the **AuthForge** package) ## Installation Add the **AuthForge** package from [NuGet](https://www.nuget.org/packages/AuthForge/): ```bash theme={null} dotnet add package AuthForge ``` Prefer a source-only layout? Copy `AuthForgeClient.cs` from [GitHub](https://github.com/AuthForgeCC/authforge-csharp) and reference `BouncyCastle.Cryptography` explicitly. The NuGet package is recommended for most apps. ## Quick start ```csharp theme={null} using AuthForge; var client = new AuthForgeClient( appId: "YOUR_APP_ID", appSecret: "YOUR_APP_SECRET", publicKey: "YOUR_PUBLIC_KEY", heartbeatMode: "SERVER" ); Console.Write("Enter license key: "); var key = Console.ReadLine() ?? ""; if (client.Login(key)) { Console.WriteLine("Authenticated!"); // Your app logic here; heartbeats run in the background } else { Console.WriteLine("Invalid license key."); Environment.Exit(1); } ``` ## Constructor parameters ```csharp theme={null} var client = new AuthForgeClient( appId: "...", // Required; from dashboard appSecret: "...", // Required; from dashboard publicKey: "YOUR_PUBLIC_KEY", // Required; Ed25519 public key from your AuthForge dashboard (base64) heartbeatMode: "SERVER", // Required; "SERVER" or "LOCAL" heartbeatInterval: 900, // Optional; seconds (default: 900, any value ≥ 1 is supported) apiBaseUrl: "https://auth.authforge.cc", // Optional onFailure: null, // Optional; Action requestTimeout: 15, // Optional; HTTP timeout in seconds ttlSeconds: null, // Optional; requested session TTL. null = server default (24h). Clamped to [3600, 604800]. hwidOverride: null // Optional; custom identity (for example "tg:123456789") ); ``` ### `ttlSeconds` Requested session token lifetime in seconds for `/auth/validate`. Pass `null` (or omit) to accept the server default of 24 hours. The server clamps to `[3600, 604800]` (1 hour to 7 days). The requested TTL is preserved across heartbeat refreshes so long-running apps in 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 ≥ 1` is economically safe. * Revocations take effect on the **next** heartbeat regardless of interval. ## Login ```csharp theme={null} bool success = client.Login(licenseKey); ``` Returns `true` if authentication succeeded, `false` otherwise. On success, the SDK starts a background heartbeat thread automatically. ## Validate license (no heartbeat) ```csharp theme={null} var result = client.ValidateLicense(licenseKey); if (result.Valid) { Console.WriteLine(result.SessionToken); } else { Console.WriteLine(result.ErrorCode); } ``` Same `/auth/validate` request and verification as `Login`, without updating the client’s session fields or starting the heartbeat thread. ## Failure callback If authentication or a heartbeat fails, the SDK calls your `OnFailure` callback. If no callback is set (or the callback throws), the SDK calls `Environment.Exit(1)`. ```csharp theme={null} void HandleFailure(string reason, Exception? exception) { if (reason == "login_failed") { Console.WriteLine("Login failed; check your license key."); } else if (reason == "heartbeat_failed") { Console.WriteLine("Heartbeat failed; saving state."); SaveApplicationState(); } } var client = new AuthForgeClient( appId: "YOUR_APP_ID", appSecret: "YOUR_APP_SECRET", publicKey: "YOUR_PUBLIC_KEY", heartbeatMode: "SERVER", onFailure: HandleFailure ); ``` If you don't set `onFailure`, the SDK terminates the process immediately on any failure. Always set a callback in production to handle graceful shutdown. ## Reading variables After a successful login, app variables and license variables are available: ```csharp theme={null} if (client.Login(licenseKey)) { // App-wide variables var appVars = client.AppVariables; if (appVars.TryGetValue("maintenanceMode", out var maintenance) && maintenance is true) { Console.WriteLine("Server is under maintenance."); Environment.Exit(0); } // Per-license variables var plan = client.LicenseVariables.GetValueOrDefault("plan", "basic"); if (plan?.ToString() == "pro") { EnableProFeatures(); } } ``` ## Heartbeat modes ```csharp theme={null} // SERVER mode; pings the API every interval var client = new AuthForgeClient( appId: "...", appSecret: "...", publicKey: "YOUR_PUBLIC_KEY", heartbeatMode: "SERVER", heartbeatInterval: 900 ); // LOCAL mode; verifies locally, re-validates when the session token expires (default 24h; up to 7d via ttlSeconds) var client = new AuthForgeClient( appId: "...", appSecret: "...", publicKey: "YOUR_PUBLIC_KEY", heartbeatMode: "LOCAL", heartbeatInterval: 900 ); ``` See [Heartbeat Modes](/concepts#heartbeat-modes) for a detailed comparison. ## Full example (WPF) ```csharp theme={null} using System.Windows; using AuthForge; public partial class App : Application { private AuthForgeClient? _authClient; protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); _authClient = new AuthForgeClient( appId: "YOUR_APP_ID", appSecret: "YOUR_APP_SECRET", publicKey: "YOUR_PUBLIC_KEY", heartbeatMode: "SERVER", onFailure: OnAuthFailure ); var dialog = new LicenseDialog(); if (dialog.ShowDialog() != true) { Shutdown(); return; } if (!_authClient.Login(dialog.LicenseKey)) { MessageBox.Show("Invalid license key.", "Authentication Failed", MessageBoxButton.OK, MessageBoxImage.Error); Shutdown(); return; } var mainWindow = new MainWindow(); mainWindow.Show(); } private void OnAuthFailure(string reason, Exception? ex) { Dispatcher.Invoke(() => { // Save user's work before shutting down (MainWindow as MainWindow)?.SaveState(); MessageBox.Show( "License verification failed. Your work has been saved.", "Authentication Error", MessageBoxButton.OK, MessageBoxImage.Warning ); Shutdown(); }); } } ``` ## GitHub Full source, changelog, and issues: [AuthForgeCC/authforge-csharp](https://github.com/AuthForgeCC/authforge-csharp) # Go SDK Source: https://docs.authforge.cc/sdk/go 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`. ```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`. ## 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). # Node.js SDK Source: https://docs.authforge.cc/sdk/node Integrate AuthForge into your Node.js or Electron application with the official npm package. ## Requirements * Node.js 18.0 or later * No runtime dependencies beyond Node.js built-ins ## Installation Install from [npm](https://www.npmjs.com/package/@authforgecc/sdk) as **`@authforgecc/sdk`**: ```bash theme={null} npm install @authforgecc/sdk ``` The package ships `authforge.mjs` and TypeScript declarations (`authforge.d.ts`). Prefer a single file in-repo? Copy `authforge.mjs` from [GitHub](https://github.com/AuthForgeCC/authforge-node). The npm package is recommended for versioning and updates. ## TypeScript Use the same import as JavaScript; types resolve from the package: ```ts theme={null} import { AuthForgeClient } from "@authforgecc/sdk"; ``` ## Quick start ```javascript theme={null} import { AuthForgeClient } from "@authforgecc/sdk"; import * as readline from "node:readline/promises"; const client = new AuthForgeClient({ appId: "YOUR_APP_ID", appSecret: "YOUR_APP_SECRET", publicKey: "YOUR_PUBLIC_KEY", heartbeatMode: "SERVER", }); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const key = await rl.question("Enter license key: "); rl.close(); if (await client.login(key)) { console.log("Authenticated!"); // Your app logic here - heartbeats run in the background } else { console.error("Invalid license key."); process.exit(1); } ``` ## Constructor parameters ```javascript theme={null} const client = new AuthForgeClient({ appId: "YOUR_APP_ID", // Required - from dashboard appSecret: "YOUR_APP_SECRET", // Required - from dashboard publicKey: "YOUR_PUBLIC_KEY", // Required - Ed25519 public key from your AuthForge dashboard (base64) heartbeatMode: "SERVER", // Required - "SERVER" or "LOCAL" heartbeatInterval: 900, // Optional - seconds (default: 900 = 15 min, any value ≥ 1 is supported) apiBaseUrl: "https://auth.authforge.cc", // Optional onFailure: null, // Optional - callback(reason, exception) requestTimeout: 15, // Optional - HTTP timeout in seconds ttlSeconds: null, // Optional - requested session TTL. null = server default (24h). Clamped to [3600, 604800]. hwidOverride: null, // Optional - custom identity (for example "tg:123456789") }); ``` ### `ttlSeconds` Requested session token lifetime in seconds for `/auth/validate`. Pass `null` (or omit) to accept the server default of 24 hours. The server clamps to `[3600, 604800]` (1 hour to 7 days). The requested TTL is preserved across heartbeat refreshes so long-running apps in LOCAL mode can stretch 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 ≥ 1` is economically safe. * Revocations take effect on the **next** heartbeat regardless of interval. ## Login ```javascript theme={null} const success = await client.login(licenseKey); ``` Returns `Promise`. It resolves to `true` if authentication succeeded, `false` otherwise. On success, the SDK starts background heartbeats automatically. ## Validate license (no heartbeat) ```javascript theme={null} const result = await client.validateLicense(licenseKey); if (result.valid) { console.log(result.sessionToken, result.expiresIn, result.appVariables, result.licenseVariables); } else { console.error(result.code, result.error); } ``` Same `/auth/validate` request and Ed25519 verification as `login`, without mutating the client’s stored session or starting the heartbeat timer. Use this for bots and repeated checks; use `login` when you want background heartbeats. Successful results may also include `sessionExpiresAt`, `licenseExpiresAt` (`null` for lifetime keys), `maxHwidSlots`, `hwidCount`, and `licenseLabel` when present in the signed payload; the decoded payload is always available as `sessionData`. ## Failure callback If authentication or a heartbeat fails, the SDK calls your `onFailure` callback. If no callback is set (or the callback throws), the SDK exits the process. ```javascript theme={null} function handleFailure(reason, exception) { if (reason === "login_failed") { console.error("Login failed - check your license key."); return; } if (reason === "heartbeat_failed") { console.error("Heartbeat failed - saving state and shutting down."); saveApplicationState(); } } const client = new AuthForgeClient({ appId: "YOUR_APP_ID", appSecret: "YOUR_APP_SECRET", publicKey: "YOUR_PUBLIC_KEY", heartbeatMode: "SERVER", onFailure: handleFailure, }); ``` If you don't set `onFailure`, the SDK terminates the process immediately on any failure. Set a callback in production for graceful shutdown. ## Reading variables After successful login, app variables and license variables are available on the client: ```javascript theme={null} if (await client.login(licenseKey)) { // App-wide variables (set in dashboard or API) const appVars = client.appVariables; if (appVars.maintenanceMode) { console.log("Server is under maintenance."); process.exit(0); } // Per-license variables const plan = client.licenseVariables.plan ?? "basic"; if (plan === "pro") { enableProFeatures(); } } ``` ## Heartbeat modes ```javascript theme={null} // SERVER mode - validates with the API every interval const serverClient = new AuthForgeClient({ appId: "YOUR_APP_ID", appSecret: "YOUR_APP_SECRET", publicKey: "YOUR_PUBLIC_KEY", heartbeatMode: "SERVER", heartbeatInterval: 900, }); // LOCAL mode - verifies locally, re-validates when the session token expires (default 24h; up to 7d via ttlSeconds) const localClient = new AuthForgeClient({ appId: "YOUR_APP_ID", appSecret: "YOUR_APP_SECRET", publicKey: "YOUR_PUBLIC_KEY", heartbeatMode: "LOCAL", heartbeatInterval: 900, }); ``` See [Heartbeat Modes](/concepts#heartbeat-modes) for a detailed comparison. ## Full example ```javascript theme={null} import { AuthForgeClient } from "@authforgecc/sdk"; import * as readline from "node:readline/promises"; function saveState() { console.log("Saving application state..."); // Your save logic here } const client = new AuthForgeClient({ appId: "YOUR_APP_ID", appSecret: "YOUR_APP_SECRET", publicKey: "YOUR_PUBLIC_KEY", heartbeatMode: "SERVER", heartbeatInterval: 900, onFailure: (reason, exception) => { console.error(`Auth failure: ${reason}`); if (exception) { console.error(`Detail: ${exception.message ?? exception}`); } saveState(); process.exit(1); }, }); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const key = await rl.question("Enter license key: "); rl.close(); if (!(await client.login(key))) { console.error("Invalid license key."); process.exit(1); } console.log("Licensed and running!"); const minVersion = client.appVariables.minVersion; if (minVersion && APP_VERSION < minVersion) { console.error(`Please update to version ${minVersion} or later.`); process.exit(1); } process.on("SIGINT", () => { saveState(); process.exit(0); }); setInterval(() => { // Your app logic here }, 1000); ``` ## GitHub Full source, changelog, and issues: [AuthForgeCC/authforge-node](https://github.com/AuthForgeCC/authforge-node) # SDK Overview Source: https://docs.authforge.cc/sdk/overview Install the official AuthForge SDK for Python, C#, C++, Rust, Go, or Node.js from package managers and registries. The AuthForge SDK handles license validation, HWID fingerprinting, cryptographic signature verification, and background heartbeats. ## Available SDKs **`authforge-sdk`** on PyPI Python 3.9+; `cryptography` pulled in automatically. **`AuthForge`** on NuGet .NET 6+ with `BouncyCastle.Cryptography`. **CMake library** from GitHub C++17, libsodium, OpenSSL, libcurl. **`authforge`** on crates.io Rust 1.70+, blocking HTTP via `ureq`. **`github.com/AuthForgeCC/authforge-go`** Go 1.21+, standard library only. **`@authforgecc/sdk`** on npm Node.js 18+, zero runtime dependencies. ## Official installs | SDK | Install command | Registry / source | | ------- | ------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | | Python | `pip install authforge-sdk` | [PyPI; authforge-sdk](https://pypi.org/project/authforge-sdk/) | | C# | `dotnet add package AuthForge` | [NuGet; AuthForge](https://www.nuget.org/packages/AuthForge/) | | Rust | `cargo add authforge` | [crates.io; authforge](https://crates.io/crates/authforge) | | Node.js | `npm install @authforgecc/sdk` | [npm; @authforgecc/sdk](https://www.npmjs.com/package/@authforgecc/sdk) | | Go | `go get github.com/AuthForgeCC/authforge-go@vX.Y.Z` | [GitHub; authforge-go](https://github.com/AuthForgeCC/authforge-go) (module path; pin a release tag) | | C++ | CMake: clone / release tarball, or `FetchContent` from GitHub | [GitHub; authforge-cpp](https://github.com/AuthForgeCC/authforge-cpp) (no C++ registry: build from source) | ## How it works All SDKs implement the same flow: 1. **Initialize**: Create a client with your App ID, App Secret, and heartbeat mode. 2. **Login or validate-only**: Call `login(licenseKey)` (or `Login`, etc.) for a long-lived session: the SDK collects the machine's HWID (or uses `hwidOverride`), generates a nonce, and sends `/auth/validate`. For **bots, cron, or per-request checks**, use the validate-only API (**`validateLicense`** in Node, **`validate_license`** in Python and Rust, **`ValidateLicense`** in C# / C++ / Go): same request and signature verification, **no heartbeat** and **no persisted session** on the client (C++ returns a result struct; others match their `login` result shape or a dedicated result type). 3. **Verify the validate response**: The SDK verifies the Ed25519 signature on `/auth/validate` using your app's configured `publicKey`. 4. **Heartbeat**: After `login`, a background thread runs at the configured interval (default 15 minutes). `/auth/heartbeat` success responses are also Ed25519-signed and verified with the same app public key. Validate-only calls do not start this loop. 5. **Failure**: If authentication or a heartbeat fails, the SDK calls your `onFailure` callback with a reason (`"login_failed"` or `"heartbeat_failed"`) and the exception. If no callback is set, the process exits. ## Common constructor parameters Every SDK accepts the same conceptual parameters, named according to each language's conventions. The table below shows the logical parameter names; see each SDK's page for exact syntax. | Parameter | Type | Default | Description | | ------------------- | -------- | ------------------------------------ | ---------------------------------------------------------------------------------------------------------- | | `appId` | string | required | Your application ID from the dashboard | | `appSecret` | string | required | Your application secret | | `publicKey` | string | required | Your app Ed25519 public key (base64) | | `heartbeatMode` | string | required | `"SERVER"` or `"LOCAL"` | | `heartbeatInterval` | integer | `900` | Seconds between heartbeat checks (any value ≥ 1 is supported) | | `apiBaseUrl` | string | `https://auth.authforge.cc` | Override the API base URL | | `onFailure` | callback | `null` | Called on auth/heartbeat failure with `(reason, exception)` | | `requestTimeout` | integer | `15` | HTTP request timeout in seconds | | `ttlSeconds` | integer | `null` / `0` (server default: 86400) | Requested session token lifetime. Server clamps to `[3600, 604800]`. Preserved across heartbeat refreshes. | ## Billing model All SDKs follow the same billing rules: * Each successful `login()` / `Login()` or **`validateLicense`**-style call costs **1 credit** (one `/auth/validate` debit). * Heartbeats cost **1 credit per 10 successful calls** (debited on every 10th heartbeat). Any `heartbeatInterval ≥ 1s` is economically safe. * Revocations take effect on the **next** heartbeat regardless of the configured interval. See [Managing Credits](/best-practices/credit-management) for usage estimates and cost-saving patterns. ## Next steps * Choose your SDK: [Python](/sdk/python), [C#](/sdk/csharp), [C++](/sdk/cpp), [Rust](/sdk/rust), [Go](/sdk/go), or [Node.js](/sdk/node) * Read the [SDK Best Practices](/sdk/best-practices) for error handling, offline mode, and UX guidance # Python SDK Source: https://docs.authforge.cc/sdk/python Integrate AuthForge into your Python application with the official PyPI package. ## Requirements * Python 3.9 or later * Dependencies: `cryptography` and `typing_extensions` (installed automatically with the PyPI package). ## Installation Install from [PyPI](https://pypi.org/project/authforge-sdk/) as **`authforge-sdk`**. In code, import the **`authforge`** module: ```bash theme={null} pip install authforge-sdk ``` Need a vendored single file? Copy `authforge.py` from the [GitHub repository](https://github.com/AuthForgeCC/authforge-python) and ensure `cryptography` is declared in your environment. The published wheel is recommended for most projects. ## Quick start ```python theme={null} from authforge import AuthForgeClient client = AuthForgeClient( app_id="YOUR_APP_ID", app_secret="YOUR_APP_SECRET", public_key="YOUR_PUBLIC_KEY", heartbeat_mode="SERVER", ) license_key = input("Enter license key: ") if client.login(license_key): print("Authenticated!") # Your app logic here; heartbeats run in the background else: print("Invalid license key.") exit(1) ``` ## Constructor parameters ```python theme={null} client = AuthForgeClient( app_id="...", # Required; from dashboard app_secret="...", # Required; from dashboard public_key="YOUR_PUBLIC_KEY", # Required; Ed25519 public key from your AuthForge dashboard (base64) heartbeat_mode="SERVER", # Required; "SERVER" or "LOCAL" heartbeat_interval=900, # Optional; seconds (default: 900, any value ≥ 1 is supported) api_base_url="https://auth.authforge.cc", # Optional on_failure=None, # Optional; callable(reason: str, exc: BaseException | None) request_timeout=15, # Optional; HTTP timeout in seconds ttl_seconds=None, # Optional; requested session TTL. None = server default (24h). Clamped to [3600, 604800]. hwid_override=None # Optional; custom identity (for example "tg:123456789") ) ``` ### `ttl_seconds` Requested session token lifetime in seconds for `/auth/validate`. Pass `None` (or omit) to accept the server default of 24 hours. The server clamps to `[3600, 604800]` (1 hour to 7 days). The requested TTL is preserved across heartbeat refreshes so long-running apps in LOCAL mode can extend their offline window up to 7 days. ## Billing * Each successful `login()` or `validate_license()` costs **1 credit** (one `/auth/validate` debit). * Heartbeats cost **1 credit per 10 successful calls** (billed on every 10th heartbeat). Any `heartbeat_interval ≥ 1` is economically safe. * Revocations take effect on the **next** heartbeat regardless of interval. ## Login ```python theme={null} success = client.login(license_key) ``` Returns `True` if authentication succeeded, `False` otherwise. On success, the SDK starts a background heartbeat thread automatically. ## Validate license (no heartbeat) ```python theme={null} result = client.validate_license(license_key) if result["valid"]: print(result["session_token"], result["app_variables"], result["license_variables"]) else: print(result["code"]) ``` Same `/auth/validate` flow and signatures as `login`, without storing session state on the client or starting the heartbeat thread. On success, the result includes optional entitlement convenience fields when the server sends them: `session_expires_at`, `license_expires_at` (`None` for lifetime keys after a JSON null), `max_hwid_slots`, `hwid_count`, and `license_label`. The full signed payload remains in `session_data`. ## Failure callback If authentication or a heartbeat fails, the SDK calls your `on_failure` callback. If no callback is set (or the callback raises an exception), the SDK calls `os._exit(1)`. ```python theme={null} def handle_failure(reason: str, exception: Exception | None): if reason == "login_failed": print("Login failed; check your license key.") elif reason == "heartbeat_failed": print("Heartbeat failed; saving state and shutting down.") save_application_state() # The SDK will exit after this callback returns if you don't handle it client = AuthForgeClient( app_id="YOUR_APP_ID", app_secret="YOUR_APP_SECRET", public_key="YOUR_PUBLIC_KEY", heartbeat_mode="SERVER", on_failure=handle_failure, ) ``` If you don't set `on_failure`, the SDK terminates the process immediately on any failure. Always set a callback in production to handle graceful shutdown. ## Reading variables After a successful login, app variables and license variables are available on the client: ```python theme={null} if client.login(license_key): # App-wide variables (set in dashboard or API) app_vars = client.get_app_variables() or {} if app_vars.get("maintenanceMode"): print("Server is under maintenance.") exit(0) # Per-license variables license_vars = client.get_license_variables() or {} plan = license_vars.get("plan", "basic") if plan == "pro": enable_pro_features() ``` ## Heartbeat modes ```python theme={null} # SERVER mode; checks with the API every interval client = AuthForgeClient( app_id="...", app_secret="...", public_key="YOUR_PUBLIC_KEY", heartbeat_mode="SERVER", heartbeat_interval=900, # 15 minutes ) # LOCAL mode; verifies locally, re-validates when the session token expires (default 24h; up to 7d via ttl_seconds) client = AuthForgeClient( app_id="...", app_secret="...", public_key="YOUR_PUBLIC_KEY", heartbeat_mode="LOCAL", heartbeat_interval=900, ) ``` See [Heartbeat Modes](/concepts#heartbeat-modes) for a detailed comparison. ## Full example ```python theme={null} import signal import sys from authforge import AuthForgeClient def on_auth_failure(reason, exception): print(f"Auth failure: {reason}") if exception: print(f" Detail: {exception}") save_state() sys.exit(1) def save_state(): print("Saving application state...") # Your save logic here client = AuthForgeClient( app_id="YOUR_APP_ID", app_secret="YOUR_APP_SECRET", public_key="YOUR_PUBLIC_KEY", heartbeat_mode="SERVER", heartbeat_interval=900, on_failure=on_auth_failure, ) license_key = input("Enter license key: ") if not client.login(license_key): print("Invalid license key.") sys.exit(1) print("Licensed and running!") # Check variables app_vars = client.get_app_variables() or {} min_version = app_vars.get("minVersion") if min_version and __version__ < min_version: print(f"Please update to version {min_version} or later.") sys.exit(1) # Your application main loop try: while True: # App logic here pass except KeyboardInterrupt: save_state() ``` ## GitHub Full source, changelog, and issues: [AuthForgeCC/authforge-python](https://github.com/AuthForgeCC/authforge-python) # Rust SDK Source: https://docs.authforge.cc/sdk/rust Integrate AuthForge into your Rust application with a lightweight Cargo crate. ## Requirements * Rust 1.70 or later * Crates used by the SDK: * `hmac` * `sha2` * `serde` * `serde_json` * `ureq` * `base64` * `mac_address` * `hostname` ## Installation The crate is published on [crates.io/crates/authforge](https://crates.io/crates/authforge) as **`authforge`**. ```bash theme={null} cargo add authforge ``` Or set a semver range in `Cargo.toml` (for example `1.0` for compatible `1.x` releases): ```toml theme={null} [dependencies] authforge = "1.0" ``` ```toml theme={null} [dependencies] authforge = { git = "https://github.com/AuthForgeCC/authforge-rust" } ``` Optional: pin `branch`, `tag`, or `rev`. ```toml theme={null} [dependencies] authforge = { path = "../authforge-rust" } ``` Adjust the path to your checkout (for example `vendor/authforge-rust`). ## Quick start ```rust theme={null} use authforge::{AuthForgeClient, AuthForgeConfig, HeartbeatMode}; fn main() { let client = AuthForgeClient::new(AuthForgeConfig { app_id: "your-app-id".into(), app_secret: "your-app-secret".into(), public_key: "YOUR_PUBLIC_KEY".into(), heartbeat_mode: HeartbeatMode::Server, on_failure: Some(Box::new(|err| { eprintln!("Auth failed: {}", err); std::process::exit(1); })), ..Default::default() }); match client.login("XXXX-XXXX-XXXX-XXXX") { Ok(result) => { println!("Authenticated!"); println!("Session expires at unix time: {}", result.expires_in); } Err(err) => { eprintln!("Login failed: {:?}", err); std::process::exit(1); } } } ``` ## Config struct reference ```rust theme={null} AuthForgeConfig { app_id: "your-app-id".into(), // Required app_secret: "your-app-secret".into(), // Required public_key: "YOUR_PUBLIC_KEY".into(), // Required - Ed25519 public key from your AuthForge dashboard (base64) heartbeat_mode: HeartbeatMode::Server, // Local or Server heartbeat_interval: 900, // Optional, seconds (any value ≥ 1) api_base_url: "https://auth.authforge.cc".into(), // Optional on_failure: None, // Optional callback request_timeout: 15, // Optional, seconds session_ttl_seconds: None, // Optional - requested session TTL. None = server default (24h). Clamped to [3600, 604800]. hwid_override: None, // Optional - custom identity (for example "tg:123456789") } ``` | Field | Type | Default | Description | | --------------------- | ------------------------------------- | ------------------------------ | ---------------------------------------------------------------------------------------------------------- | | `app_id` | `String` | required | Application ID from your AuthForge dashboard | | `app_secret` | `String` | required | Application secret from your AuthForge dashboard | | `public_key` | `String` | required | Ed25519 public key from your AuthForge dashboard (base64) | | `heartbeat_mode` | `HeartbeatMode` | `Local` | `Local` or `Server` heartbeat mode | | `heartbeat_interval` | `u64` | `900` | Seconds between heartbeat checks (any value ≥ 1) | | `api_base_url` | `String` | `https://auth.authforge.cc` | AuthForge API base URL | | `on_failure` | `Option>` | `None` | Invoked when heartbeat fails | | `request_timeout` | `u64` | `15` | Timeout for API requests | | `session_ttl_seconds` | `Option` | `None` (server default: 86400) | Requested session token lifetime. Server clamps to `[3600, 604800]`; preserved across heartbeat refreshes. | | `hwid_override` | `Option` | `None` | Optional custom identity string sent as HWID (for example `tg:123456789`). | ## Validate license (no heartbeat) `validate_license` runs the same `/auth/validate` request and signature checks as `login`, but does **not** store a session on the client or start the heartbeat thread. It returns the same `LoginResult` on success. ```rust theme={null} match client.validate_license("XXXX-XXXX-XXXX-XXXX") { Ok(result) => { /* use result.session_token, result.app_variables, … */ } Err(err) => { /* same AuthForgeError variants as login */ } } ``` ## Billing * Each successful `login()` or `validate_license()` costs **1 credit** (one `/auth/validate` debit). * Heartbeats cost **1 credit per 10 successful calls** (billed on every 10th heartbeat). Any `heartbeat_interval ≥ 1` is economically safe. * Revocations take effect on the **next** heartbeat regardless of interval. ## Methods reference ```rust theme={null} pub fn new(config: AuthForgeConfig) -> Self; pub fn login(&self, license_key: &str) -> Result; pub fn validate_license(&self, license_key: &str) -> Result; pub fn logout(&self); pub fn is_authenticated(&self) -> bool; pub fn get_session_data(&self) -> Option; pub fn get_app_variables(&self) -> Option>; pub fn get_license_variables(&self) -> Option>; ``` ## Error enum reference ```rust theme={null} AuthForgeError::InvalidApp AuthForgeError::InvalidKey AuthForgeError::Expired AuthForgeError::Revoked AuthForgeError::HwidMismatch AuthForgeError::NoCredits AuthForgeError::Blocked AuthForgeError::RateLimited // validate only; never returned from heartbeat AuthForgeError::ReplayDetected // validate only; never returned from heartbeat AuthForgeError::SignatureMismatch AuthForgeError::NetworkError(String) AuthForgeError::Other(String) ``` ## Heartbeat modes ```rust theme={null} // SERVER mode: network heartbeats via /auth/heartbeat let server_client = AuthForgeClient::new(AuthForgeConfig { app_id: "...".into(), app_secret: "...".into(), public_key: "YOUR_PUBLIC_KEY".into(), heartbeat_mode: HeartbeatMode::Server, ..Default::default() }); // LOCAL mode: local expiry checks with no heartbeat network calls let local_client = AuthForgeClient::new(AuthForgeConfig { app_id: "...".into(), app_secret: "...".into(), public_key: "YOUR_PUBLIC_KEY".into(), heartbeat_mode: HeartbeatMode::Local, ..Default::default() }); ``` See [Heartbeat Modes](/concepts#heartbeat-modes) for a detailed comparison.