# 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`, `malformed_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`: * `30/min` per IP * `5/min` per license key * Application-level per-minute limit on `/auth/heartbeat`: * `6/min` per license key `/auth/heartbeat` is **not** IP rate-limited at the application layer, only per-license. The default online check-in interval of 15 minutes is far below the limit; the per-license cap exists to stop runaway loops. 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 JSON body parsed successfully but failed schema validation (missing fields, invalid types). Includes a `details` array. | This is a developer integration error. | | `malformed_request` | The request body is missing or is not valid JSON. | 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 JSON body parsed successfully but failed schema validation (missing fields, invalid types). | This is a developer integration error. | | `malformed_request` | The request body is missing or is not valid JSON. | 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` | The per-license heartbeat limit (6/min) was exceeded. | This is a developer integration error; keep the check-in interval well above 10 seconds. | `replay_detected` is **never** returned from `/auth/heartbeat`. Heartbeats do not enforce nonce replay detection; replay protection is provided by the signed, short-lived session token. Heartbeat rate limiting is per license only (6/min), never per IP. ### 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. ### Offline license file errors (`loginFromFile` / `verifyLicenseFile`) Verifying a `.authforge` [offline license file](/features/offline-license-files) is entirely local. Every SDK runs the same checks in this order and reports the first failure through `onFailure("offline_login_failed", error)` (Go and Rust return typed errors instead): | Code | Meaning | Recommended handling | |------|---------|---------------------| | `bad_armor` | Not well-formed `.authforge` armor (truncated, wrong markers, not base64). | Show "The license file is damaged. Re-download it." | | `bad_signature` | Signature does not verify against any configured public key: tampered file, or minted for a different app's key. | Show "This file is not a valid license for this product." | | `unsupported_version` | Payload `v` is newer than the SDK understands. | Show "Update the app to use this license file." | | `malformed_payload` | Signed payload is missing required fields. | Treat like `bad_armor`. | | `wrong_app` | Valid signature, but minted for a different app id. | Show "This file is not a valid license for this product." | | `expired` | Past the file's `expiresAt`. | Show "Your offline license expired on {date}. Request a new license file." | | `hwid_mismatch` | File is HWID-bound and this machine's HWID is not listed. | Show "This license file is for a different machine. Your machine ID is {hwid}." and print `getHwid()`. | `loginFromFile` never terminates the process on its own; display the message and exit from your code. --- ## 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). Ignored when `unlimitedHwidSlots` is `true`. | | `unlimitedHwidSlots` | boolean | No | When `true`, mark these licenses as shared "unlimited-seat" keys. The server skips seat enforcement and never returns `hwid_mismatch` for them. See [Unlimited-seat licenses](/features/unlimited-seats). | | `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 fail on their next activation or online check-in; apps running through the grace period without check-ins stop at session expiry. Revoking also blocks new [offline license file](#offline-license-files) mints, but files already minted stay valid until their own expiry. ```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 | --- ## Offline license files Cloud-minted, Ed25519-signed `.authforge` files for machines that never connect to the internet. The SDK verifies a file locally with your app public key and the machine HWID; it never contacts AuthForge. This is a separate mode from the grace period. Read [Offline license files](/features/offline-license-files) first, especially the revocation semantics: **issued files stay valid until their own expiry**; revoking the license only blocks new mints. ### Mint an offline license file Mint a `.authforge` file for an active license. Scope `write:licenses`. **1 credit per successful mint** (same debit path as `/auth/validate`, so your app's burn caps apply); rejected mints are free. #### Request body | Field | Type | Required | Description | |-------|------|----------|-------------| | `expiresAt` | string or `null` | Yes | ISO 8601 file expiry. Must be in the future and on or before the license expiry. `null` mints a **lifetime** file and is only accepted for licenses without an expiry. | | `hwidMode` | string | Yes | `bound` (verify only on listed HWIDs, recommended) or `any` (verify on any machine). | | `hwids` | string[] | For `bound` | 1 to 16 HWIDs. Collect them with the SDK's `getHwid()` on the target machine, or reuse the license's `hwidList`. | | `allowUnbound` | boolean | For `any` | Must be `true` to mint an unbound file. Anyone who copies an unbound file has a working license. | #### Headers | Header | Required | Description | |--------|----------|-------------| | `Idempotency-Key` | Recommended | Any unique string (a UUID is ideal), same header as `POST /v1/licenses`. A retry with the same key, license and app within **24 hours** returns the **identical file** with HTTP `200` and `meta.idempotentReplay: true`, and is **not charged again**. A duplicate that arrives while the first request is still running gets `409 idempotency_in_flight`; retry shortly. Without the header every request is a new, charged mint. | The server never stores the file body: on a replay it re-signs the stored payload with your app key (Ed25519 is deterministic), so the bytes match the first response exactly. #### Example: bound, 90 days ```bash theme={null} curl -X POST https://api.authforge.cc/v1/licenses/A3K9-BFWX-7NP2-QHDT/offline-files \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 5c1d5f4e-9b62-4d1a-8f7e-2b0d6a7c3e11" \ -d '{ "expiresAt": "2026-12-10T00:00:00Z", "hwidMode": "bound", "hwids": ["3f1c9a0e...e9a2"] }' ``` #### Example: unbound (explicit acknowledgement) ```bash theme={null} curl -X POST https://api.authforge.cc/v1/licenses/A3K9-BFWX-7NP2-QHDT/offline-files \ -H "Authorization: Bearer af_live_your_key" \ -H "Content-Type: application/json" \ -d '{ "expiresAt": "2026-10-11T00:00:00Z", "hwidMode": "any", "allowUnbound": true }' ``` #### Response (201) ```json theme={null} { "file": "-----BEGIN AUTHFORGE LICENSE-----\nVersion: 1\nApp-Id: YOUR_APP_ID\nLicense: A3K9-BFWX-7NP2-QHDT\nKey-Id: 8c9e...\nExpires-At: 2026-12-10T00:00:00.000Z\n\neyJ2IjoxLCJ0eXAiOiJhdXRoZm9yZ2UtbGljZW5zZSIs...\n-----END AUTHFORGE LICENSE-----\n-----BEGIN AUTHFORGE SIGNATURE-----\nOUi4frE6...\n-----END AUTHFORGE SIGNATURE-----\n", "fileName": "A3K9-BFWX-7NP2-QHDT.authforge", "meta": { "jti": "6b1f0c2a-...", "licenseKey": "A3K9-BFWX-7NP2-QHDT", "appId": "YOUR_APP_ID", "issuedAt": "2026-09-11T17:00:00.000Z", "expiresAt": "2026-12-10T00:00:00.000Z", "hwidPolicy": { "mode": "bound", "hwids": ["3f1c9a0e...e9a2"] }, "kid": "8c9e...", "fileSha256": "…", "source": "developer-api", "actorId": "…", "apiKeyId": "…", "version": 1, "creditsCharged": 1, "idempotentReplay": false, "historyRecorded": true } } ``` Write `file` to disk as `fileName`. The server stores only `fileSha256`, never the file body, so there is no download endpoint; if the response was lost, retry with the same `Idempotency-Key` within 24h to get the identical file (HTTP `200`, `meta.idempotentReplay: true`, no extra charge). | Field | Description | |-------|-------------| | `meta.idempotentReplay` | `false` on a fresh mint (`201`), `true` when the response replays an earlier mint with the same `Idempotency-Key` (`200`). | | `meta.historyRecorded` | `false` when the credit was charged and the file signed but the mint-history row could not be written. The file is still returned and the audit log still records the mint. Keep the file; retry only with the same `Idempotency-Key`. | | `warnings` | Optional array, present only on partial failures. Currently `["history_not_recorded"]`, paired with `meta.historyRecorded: false`. | #### Errors | HTTP | Code | Cause | |------|------|-------| | 400 | `bad_request` | Body failed schema validation (for example `hwidMode: "bound"` without `hwids`) | | 400 | `expiry_in_past` | `expiresAt` is not a valid future timestamp | | 400 | `expiry_after_license` | `expiresAt` is later than the license expiry | | 400 | `lifetime_requires_perpetual_license` | `expiresAt: null` on a license that has an expiry | | 400 | `unbound_not_allowed` | `hwidMode: "any"` without `allowUnbound: true` | | 400 | `invalid_hwids` | `hwidMode: "bound"` but every HWID is empty or whitespace after trimming | | 400 | `too_many_hwids` | More than 16 distinct HWIDs | | 402 | `no_credits` | Insufficient credits; nothing was signed | | 403 | `forbidden` / `insufficient_scope` | Not your license, or the key lacks `write:licenses` | | 404 | `not_found` | License key doesn't exist | | 409 | `license_not_active` | License is revoked | | 409 | `license_expired` | License has expired | | 409 | `app_disabled` | App is paused | | 409 | `idempotency_in_flight` | A request with the same `Idempotency-Key` is still running; retry shortly | | 409 | `idempotency_replay_unavailable` | The app signing key changed since the first mint, so the original file cannot be reproduced; mint again with a new key | | 429 | `rate_limited` | More than 30 mints/minute per account or 10/minute per license | | 429 | `burn_cap` | App hourly/daily credit burn cap reached | ### List offline file mint history Newest-first mint records for the license, capped at the 100 most recent (`limit`); `truncated: true` means older mints were omitted and the full trail is in the account audit log. Scope `read:licenses`. Returns metadata and hashes only; file bodies are never stored. ```bash theme={null} curl https://api.authforge.cc/v1/licenses/A3K9-BFWX-7NP2-QHDT/offline-files \ -H "Authorization: Bearer af_live_your_key" ``` #### Response (200) ```json theme={null} { "files": [ { "jti": "6b1f0c2a-...", "licenseKey": "A3K9-BFWX-7NP2-QHDT", "appId": "YOUR_APP_ID", "issuedAt": "2026-09-11T17:00:00.000Z", "expiresAt": "2026-12-10T00:00:00.000Z", "hwidPolicy": { "mode": "bound", "hwids": ["3f1c9a0e...e9a2"] }, "kid": "8c9e...", "fileSha256": "…", "source": "developer-api", "actorId": "…", "apiKeyId": "…", "version": 1, "creditsCharged": 1 } ], "truncated": false, "limit": 100 } ``` Use this to track which files are outstanding for a license and when each one lapses; it is the operator-side half of the re-issue workflow described in [Offline licensing best practices](/best-practices/offline-licensing). # 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 ``` theme={null} 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` has a per-license limit (6/min) but no per-IP limit. See [Error Codes Reference](/api/errors#public-auth-errors-sdk). ## Credits License management calls (create, list, update, delete, variables, webhooks, security) do not consume credits. Two things do: SDK authentication (1 credit per successful validate, 1 per 10 heartbeats) and **minting an [offline license file](/api/licenses#offline-license-files)** via `POST /v1/licenses/{licenseKey}/offline-files` (1 credit per successful mint, debited from the app owner's balance; rejected mints are free). 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 (online check-ins) | 1 credit | | Minting an [offline license file](/features/offline-license-files) (`.authforge`) | 1 credit | Failed validations (invalid key, expired, revoked) do **not** consume credits. Only successful authentications are billed. Heartbeats happen only if you enable [online check-ins](/concepts#online-check-ins-optional); credits are debited on every 10th successful call. The default grace-period behavior makes no heartbeat calls at all. Offline license files are billed once, at mint time, to the operator's account (through the same debit path as validate, so your app's burn caps apply). Verifying a file on the air-gapped machine costs nothing, and rejected mints are free. An air-gapped fleet that re-issues 90-day files costs roughly 4 credits per machine per year. ## Estimating monthly usage ### Formula ``` theme={null} 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 (only if online check-ins are enabled; otherwise 0) ``` ### Example calculation Suppose you have: * 500 active licenses * Users log in once per day on average * Users run the app 8 hours per day * Online check-ins enabled at the default 15-minute interval ``` theme={null} 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 check-in interval to 30 minutes roughly halves the heartbeat portion. ### Quick reference (online check-ins enabled, 15-min interval) | 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 check-in frequency. Doubling the interval halves the heartbeat column; relying on the grace period alone (the default, 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 ### Rely on the grace period (the default) If you don't need fast revocation enforcement, don't enable online check-ins. The default behavior eliminates heartbeat credit consumption entirely; the SDK re-verifies the signed session locally and only makes a network call when the session expires (24 hours by default, up to 7 days via the SDK's `ttl` option). ```python theme={null} client = AuthForgeClient( app_id="...", app_secret="...", public_key="YOUR_PUBLIC_KEY", # No online_heartbeat: grace period, no check-ins, minimal credit usage ) ``` **Credit savings:** For a user running the app 8 hours/day, online check-ins at a 15-minute interval consume ~32 heartbeats ≈ 3.2 credits/day. The grace period needs at most 1 re-activation per session-TTL window (~1 credit every 24h by default). That's roughly 3x cheaper at a 15-minute interval, and more if you check in more often. ### Increase the check-in interval If you want online check-ins but don't need 15-minute checks, increase the interval: ```python theme={null} client = AuthForgeClient( app_id="...", app_secret="...", public_key="YOUR_PUBLIC_KEY", online_heartbeat=True, 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 check-in, if online check-ins are enabled) 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. # Offline licensing best practices Source: https://docs.authforge.cc/best-practices/offline-licensing How to store, deliver, refresh and enforce cloud-minted .authforge license files on air-gapped machines, and how to write software that is protected by them. This page assumes you have read [Offline license files](/features/offline-license-files) and decided that a machine genuinely cannot reach AuthForge. If it can reach the internet even occasionally, the default online activation + [grace period](/concepts#activation-and-the-grace-period) is simpler, cheaper and revocable; use that instead. ## Decide the policy before the first mint Every `.authforge` file is a promise you cannot take back. Settle these three questions up front: | Question | Recommended default | Why | | --- | --- | --- | | How long should a file live? | 30 to 90 days | Re-issuing is your only revocation lever. Shorter files bound the damage of a lost laptop or a churned customer. | | Bound or unbound? | **Bound** to the machine's HWID | An unbound (`any`) file is a working license for anyone who copies it. Accept `any` only for appliances you control end to end. | | Lifetime files? | Only for perpetual licenses you are prepared to honour forever | A lifetime file has no expiry and cannot be revoked. | Write the answers down as your offline SKU's terms so support, sales and engineering all say the same thing to customers. ## Collecting the HWID A bound file needs the target machine's HWID before you mint. Every SDK exposes it without any network access: | SDK | Call | | --- | --- | | Python | `client.get_hwid()` | | Node.js | `client.getHwid()` | | Go | `client.HWID()` | | Rust | `client.hwid()` | | C# | `client.GetHwid()` | | C++ | `client.GetHwid()` | Ship a small "Show machine ID" affordance (a menu item, a `--hwid` flag, a first-run screen) that prints the value and lets the user copy it. Because HWIDs are [SDK-specific](/features/hwid-locking), collect it from the same SDK build that will later verify the file. If the customer already activated online once, the dashboard prefills the HWIDs bound to the license and you can skip this step. For appliances you image yourself, pass `hwidOverride` with a serial number you control (for example `unit:SN-00042`) so the HWID survives disk swaps and NIC replacements. ## Delivering the file The file is plain text with PEM-style armor, so it survives every channel that carries text: email, a support portal download, a USB stick through a data diode, a QR code for very short payloads, or a printed sheet in a pinch. The parser tolerates CRLF conversion, a UTF-8 BOM, re-wrapped lines and surrounding text (an email preamble), so copy/paste damage is rare. Keep the extension `.authforge`. Use `.authforge.lic` only where a platform refuses unknown extensions. Treat the file like a bearer credential for bound files and like cash for unbound files. Send it over the same channel you would use for a password reset link. ## Storing the file on the machine Store the file in a per-user or per-machine application data directory, not next to the executable: * Windows: `%ProgramData%\\\license.authforge` (machine-wide) or `%LocalAppData%\...` (per user) * macOS: `/Library/Application Support//license.authforge` or `~/Library/Application Support/...` * Linux: `/etc//license.authforge` or `$XDG_CONFIG_HOME//license.authforge` Recommendations: * Accept the file through an explicit **Import license file...** action that copies it into place, so users are not editing config folders by hand. * Do not encrypt or obfuscate the file. It is already signed; encrypting it buys nothing against a user who controls the machine and complicates support. * Keep the previous file around when you import a new one (`license.authforge.bak`) so a bad import is recoverable. * The SDKs accept either a filesystem path or the armored text, so you can also embed the file contents in your own settings store if that is easier. ## Refreshing before expiry Because revocation is impossible, expiry is the lifecycle. Build the refresh into the product rather than leaving it to a support ticket: 1. **Surface the expiry.** After `loginFromFile`, read `expiresAt` from `getOfflineLicense()` and show it in the About/License screen. Start warning at 14 days and again at 3 days. 2. **Make re-issue a routine operator task.** Mint the replacement from the license page (or script it against `POST /v1/licenses/{licenseKey}/offline-files`) a few weeks before the old file lapses. Mint history on the license page tells you what is outstanding. 3. **Track the fleet.** For every offline machine keep the license key, HWID, file `jti`, and `expiresAt` in your CRM or asset inventory. The Developer API `GET /v1/licenses/{licenseKey}/offline-files` returns the same fields if you prefer to sync from AuthForge. 4. **Let a new file replace an old one without downtime.** Import the new file, verify it with `verifyLicenseFile` before overwriting the old one, then swap. A file with a later `expiresAt` for the same license is always safe to install early. If a customer churns, stop re-issuing and revoke the license online so no new files can be minted. The outstanding file runs out on schedule. ## Writing software protected by an offline file Call `loginFromFile` at launch and gate the app on the result. Do not re-verify on every action; the file does not change while the app runs. `loginFromFile` never terminates the process on its own. Map the code you receive in `onFailure("offline_login_failed", error)` to a human message: | Code | Message to show | | --- | --- | | `expired` | "Your offline license expired on {date}. Ask {vendor} for a new license file." | | `hwid_mismatch` | "This license file is for a different machine. Your machine ID is {hwid}." | | `bad_signature`, `wrong_app` | "This file is not a valid license for {product}." | | `bad_armor`, `malformed_payload` | "The license file is damaged. Re-download it." | | `unsupported_version` | "Please update {product} to use this license file." | Always print the machine's HWID on the failure screen: it is the one piece of information support needs to fix a mismatch. `getLicenseVariables()` and `getAppVariables()` return the snapshots frozen at mint time. Use them for tiering and feature flags exactly as you would online. Remember they only change when a new file is imported. `loginFromFile` never starts the grace-period timer or online check-ins, and you should not start them either. If your product supports both connected and air-gapped customers, branch on the presence of a license file: `loginFromFile` when one is installed, otherwise the normal online `login()`. Decide what happens when the file lapses while the app is running: most products keep running until restart and refuse to start the next time. Whatever you choose, do not silently corrupt or lock user data. ### Supporting both modes in one binary ```text theme={null} if license.authforge exists in the app data directory: ok = client.loginFromFile(path) # offline mode, no network else: ok = client.login(licenseKey) # online activation + grace period ``` Never fall back from a *rejected* offline file to an online login automatically; a rejected file means something is wrong that the user should see. ## Threat model, honestly * **Clock roll-back.** The expiry check uses the machine's clock. A user can extend a file by winding the clock back. Short expiries and HWID binding are the mitigations; there is no server to consult. * **Copying a bound file.** Useless on another machine unless the HWID collides, which is why bound is the default. * **Copying an unbound file.** Works everywhere. Only mint `any` files when you would be comfortable posting them publicly. * **Tampering.** Any edit to the payload fails `bad_signature`. Headers are not signed and are ignored by the SDKs. * **Key rotation.** Rotating your app signing key means new files carry a new `kid`. SDKs accept a list of public keys, so ship the old and new key together until every old file has expired. * **Your own mistakes.** Mint history and the audit log record every file (actor, time, expiry, binding, file id, hash). Review them when a customer reports something unexpected. ## Checklist * [ ] Offline SKU terms written: expiry length, binding policy, lifetime policy * [ ] HWID visible in the product without network access * [ ] Import flow that validates before overwriting the previous file * [ ] Expiry shown in the UI, warnings at 14 and 3 days * [ ] Fleet inventory of license key / HWID / `jti` / `expiresAt` * [ ] Re-issue runbook (dashboard or scripted Developer API call) owned by a named team * [ ] Failure screen shows the code, a plain-language message, and the machine HWID * [ ] Online customers still use `login()` + grace period; offline files only where required # 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**). Running clients fail on their next server call (activation or online check-in), 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. ## Enable online check-ins for high-value software The default grace period relies on local re-verification of the signed session, which can be bypassed by: * Freezing or rolling back the system clock * Patching the SDK to skip the local signature check Online check-ins require a valid signed response from the AuthForge API on every interval and catch revocations within minutes. For software where piracy has significant business impact, enable online check-ins (`online_heartbeat` / `onlineHeartbeat` / `OnlineHeartbeat`). ## 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 rate-limited **per license** (up to **6** requests per minute) but not per IP at the application layer; a flood of heartbeats beyond that only burns the victim app's credits (which the server already handles via `no_credits` / `app_burn_cap_reached`). 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 to activate. After that, it keeps working without a connection for up to 24 hours before it needs to check in again.

Retry
After initial activation, the app runs through the grace period with no network access needed until the signed session expires (24 hours by default; up to 7 days if the SDK was configured with a longer `ttlSeconds`). If you enable online check-ins, the app needs connectivity while it runs. Do not describe the grace period to users as "works offline": it is a temporary window, not offline licensing. Customers whose machines never connect need an [offline license file](/features/offline-license-files) instead, and that flow has its own UX (import a file, show its expiry, show the machine ID on failure). # Core Concepts Source: https://docs.authforge.cc/concepts Key terms and concepts in AuthForge: apps, licenses, HWIDs, credits, the grace period, online check-ins, 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 (online check-ins) | 1 credit | | Minting an [offline license file](#offline-license-files-authforge) | 1 credit | Heartbeats only occur if you enable [online check-ins](#online-check-ins-optional). 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. ## Activation and the Grace Period Authentication in AuthForge is one online call followed by a window of local operation: 1. **Activate (validate)**: On login, the SDK sends `POST /auth/validate`. The server binds the HWID, checks revocation, expiry, and credits, and returns an Ed25519-signed session. 2. **Grace period**: The app then keeps running on that signed session without contacting AuthForge. The SDK periodically re-verifies the stored session signature locally (no network calls) and stops when the session expires. The grace period equals the **session TTL**: 24 hours by default, clamped by the server to a minimum of 1 hour and a maximum of 7 days, and configurable via the SDK's `ttl` option (`ttl_seconds` / `ttlSeconds` / `SessionTTL` / `session_ttl_seconds` depending on language). When the session expires, the SDK triggers your `onFailure` callback (or terminates the process if no callback is set), and the user must activate again. This is the default experience: activate online once, then run through the grace period with zero further network traffic and zero heartbeat credit consumption. ## Online Check-ins (Optional) If you need faster revocation or concurrent-use detection, opt in to **online check-ins**: periodic `POST /auth/heartbeat` calls while the app runs. Enable them with the SDK's `online_heartbeat` / `onlineHeartbeat` / `OnlineHeartbeat` option. On each interval (default **15 minutes**, 900 seconds), the SDK sends the session token to `/auth/heartbeat`. The server verifies the token + signature chain, checks the license status, and returns a new signed response that refreshes the session. SDKs verify every heartbeat signature with your app's Ed25519 public key. If a check-in 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). **Trade-offs:** * Revocations take effect on the next check-in instead of at session expiry: real-time enforcement for high-value software. * Each successful check-in refreshes the session, so long-running apps never hit the grace-period expiry. * Requires network connectivity while the app runs. * Billing: 1 credit per 10 heartbeats. The server rate-limits heartbeats to 6 per minute per license, so keep the interval at 10 seconds or more; the 15-minute default is right for most apps. **Migrating from `heartbeat_mode`:** older SDK versions exposed `heartbeat_mode: "SERVER" | "LOCAL"`. The default grace-period behavior is what `"LOCAL"` used to do (just remove the option), and online check-ins are what `"SERVER"` used to do (enable `online_heartbeat`). The old option still works but is deprecated. ## Offline License Files (`.authforge`) The grace period is **not** offline licensing: it continues a session after an online activation and lapses within at most 7 days. For machines that can **never** reach AuthForge, there is a separate mode: **offline license files**. An operator mints a `.authforge` file in the dashboard (or via `POST /v1/licenses/{licenseKey}/offline-files`). The file is a standalone document signed with your app's existing Ed25519 key. On the air-gapped machine the SDK verifies it with only your app public key and the machine's HWID (`loginFromFile`), and never contacts AuthForge. | | Grace period | Offline license file | |---|---|---| | Network on the end machine | Once, at activation | Never | | Created by | The SDK, from a `/auth/validate` response | You, deliberately, per license | | Lifetime | Session TTL (1h to 7d) | Your chosen expiry or lifetime (perpetual licenses only) | | Revocation | Next online validate or check-in | Not reachable: valid until the file's own expiry | | Cost | 1 credit per activation | 1 credit per mint; verifying is free | Because a distributed file cannot be revoked, mint short-lived, HWID-bound files and re-issue on a schedule. Full details, format and SDK examples: [Offline license files](/features/offline-license-files). Operational guidance: [Offline licensing best practices](/best-practices/offline-licensing). ## Session Tokens A successful `/auth/validate` response includes a signed **session token** that the SDK stores, re-verifies locally during the grace period, and replays on every online check-in (if enabled). 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 (online check-in) mints a fresh token, preserving the originally requested TTL | | Revocation | Revoking a license invalidates all of its tokens; running apps detect this on their next online check-in, or at session expiry if check-ins are disabled | SDKs expose a `ttlSeconds` / `session_ttl_seconds` / `SessionTTL` configuration option so you can tune the grace period: how long the SDK can keep running without reaching the server before forcing a re-login. The server silently clamps out-of-range values to the 1 hour to 7 day range. ### 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`. Apps with online check-ins enabled fail on the next check-in. Apps running through the grace period (the default) continue until the signed session 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`. #### Does AuthForge support offline or air-gapped machines? Yes, in two different ways, and it matters which one you mean. * The **grace period** (default) lets an app keep running for up to 7 days after an *online* activation. It is session continuation, not offline licensing: the machine must reach AuthForge at least once, and again when the session lapses. * **Offline license files** (`.authforge`) are for machines that *never* connect. You mint a signed file in the dashboard or Developer API (1 credit), deliver it out-of-band, and the SDK verifies it locally with your public key and the machine HWID via `loginFromFile`. No network, ever. Issued files cannot be revoked remotely; they stay valid until their own expiry. Mint short-lived, HWID-bound files and re-issue on a schedule. See [Offline license files](/features/offline-license-files) and [Offline licensing best practices](/best-practices/offline-licensing). #### I revoked a license but the offline machine still works Expected. Revoking online blocks new mints and online activations, but a `.authforge` file already on a machine is a standalone signed document with no server to ask. It runs until its `expiresAt`. The license page's mint history shows every outstanding file and when it lapses; stop re-issuing and the entitlement ends on schedule. ### 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 only happen if you enabled online check-ins, and they're cheap: **10 successful heartbeats cost 1 credit** (billed on every 10th call). At the default 15-minute interval, an always-on app costs about 10 credits/day. If that's still more than you want, increase the check-in interval (the credit cost scales directly with the number of heartbeats sent), or disable online check-ins entirely and rely on the grace period: the SDK re-verifies the signed session locally, with zero heartbeat credits, until it expires (24h by default). With online check-ins, revocations apply on the **next** check-in, regardless of interval. Without them, revocations apply when the session expires. #### 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 `/auth/validate` is rate-limited to 5 requests per license per minute and 30 per IP per minute. `/auth/heartbeat` is rate-limited to 6 requests per license per minute (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), successful heartbeats (1 credit per 10) and successful offline file mints (1 credit each) consume credits. Verifying an offline file on the end machine is free. #### 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: ``` theme={null} 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). ## Disabling seat limits Some keys are not paid licenses - internal team tools, beta cohorts, public demos. For those, you usually want one shared key that any device can activate, with no `hwid_mismatch` errors and no exclusive binding. AuthForge supports this as a per-license toggle: see [**Unlimited-seat licenses**](/features/unlimited-seats). Activations are still logged so you keep distinct-device analytics; only the seat enforcement is removed. ## Related features * **[Unlimited-seat licenses](/features/unlimited-seats)**: Opt out of seat enforcement on a per-license basis - shared keys with no exclusive HWID binding. * **[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 background 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. ## Background checks are separate After login, the SDK re-verifies the signed session locally through the [grace period](/concepts#activation-and-the-grace-period), and sends [online check-ins](/concepts#online-check-ins-optional) only if you enabled them. Neither is part of the initial validation latency your user waits on at startup (unless you block your UI until a check-in 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, grace-period behavior, and online check-ins. * **[Core Concepts](/concepts)**: Credits, sessions, and HWID binding. # Offline license files (.authforge) Source: https://docs.authforge.cc/features/offline-license-files Cloud-minted, Ed25519-signed license files for air-gapped machines that never contact AuthForge. How they differ from the grace period, how to mint one, and how every SDK verifies it. An **offline license file** is a signed document (`.authforge`) that you mint in the AuthForge cloud and hand to a customer whose machine never connects to the internet. The SDK on that machine verifies the file with **only your app public key, app id, and its own HWID**. It never calls `/auth/validate`, never checks in, and never needs AuthForge network access for the life of the file. **Do not embed the App Secret** in air-gapped binaries: `loginFromFile` does not use it. Pass an empty secret (or omit it in languages that allow that) when constructing an offline-only client. This is a **separate mode** from the default. Most apps should keep using online activation + the [grace period](/concepts#activation-and-the-grace-period). Reach for offline files only when a machine genuinely cannot phone home (factory floors, classified networks, ships, embedded appliances). ## Offline file vs grace period | | Grace period (default) | Offline license file | | --- | --- | --- | | Network on the end machine | Once, at `login()` | **Never** | | What is verified | Signed *session* returned by `/auth/validate` | Signed *document* minted by an operator | | Who creates it | The SDK, automatically | You, in the dashboard or Developer API | | Lifetime | Session TTL: 1h to 7d | Your chosen expiry, or lifetime (perpetual licenses only) | | HWID binding | Server binds seats at activation | Baked into the file: bound to specific HWIDs, or (explicitly) any machine | | Revocation | Picked up at the next online validate or check-in | **Not reachable.** The file stays valid until its own expiry | | Cost | 1 credit per `login()` | 1 credit per mint. Verifying is free | | SDK call | `login(licenseKey)` | `loginFromFile(pathOrText)` | The grace period is *session continuation*: it keeps an app running for a while after an online activation. An offline file is *persistent air-gap licensing*: a standalone credential the machine can verify forever without you. ## Revocation: read this before you mint Revoking a license online blocks **new** mints and stops online activations. It does **not** reach files that are already on customer machines. Every issued `.authforge` file stays valid until its own `expiresAt` (or forever, for lifetime files). There is no remote kill switch, and we do not pretend otherwise. Practical consequences: * Prefer **short expiries** (30 to 90 days) and re-issue on a schedule. Treat re-issuing as your revocation lever. * Prefer **HWID-bound** files. An unbound (`any`) file works on every machine that has a copy. * Mint **lifetime** files only for perpetual licenses where you accept that the entitlement is permanent. * Keep mint history (the dashboard shows it per license) so you know exactly which files exist and when they lapse. ## Minting a file ### Dashboard 1. Open the license (Applications -> app -> Licenses -> license page) and click **Mint .authforge file**. 2. Pick an expiry. Presets are 30 days, 90 days, 1 year, the license expiry, or Lifetime (perpetual licenses only). The file can never outlive the license. 3. Pick a machine binding. **Bound to specific HWIDs** (recommended) prefills the HWIDs already bound to the license; paste the customer's HWID if they have not activated online before (every SDK exposes it: `getHwid()` / `get_hwid()` / `HWID()` / `hwid()`). **Any machine** requires an explicit acknowledgement. **Collect the HWID from the same SDK (and language) that will call `loginFromFile`.** HWID fingerprints are [not portable across SDKs](/features/hwid-locking): a machine reports a different value from the Node SDK than from the C# SDK. A file bound to the wrong one fails with `hwid_mismatch`. If your product uses several SDKs on the same box, pass `hwidOverride` with an identifier you control instead. 4. Click **Mint & download**. The browser downloads `.authforge` and 1 credit is charged. The file body is never stored server-side, only its SHA-256, so download it now. The row menu on the licenses table has the same action, and the license page lists every mint (issued, expires, binding, source, file id). ### Developer API ```bash theme={null} curl -X POST https://api.authforge.cc/v1/licenses/XXXX-XXXX-XXXX-XXXX/offline-files \ -H "Authorization: Bearer af_live_..." \ -H "Content-Type: application/json" \ -d '{ "expiresAt": "2027-01-01T00:00:00Z", "hwidMode": "bound", "hwids": ["3f1c...e9a2"] }' ``` Returns `201` with `{ file, fileName, meta }`. `file` is the armored text; write it to `fileName`. Scope `write:licenses`. See [Licenses API -> Offline license files](/api/licenses#offline-license-files) for the full reference and error codes. ### Mint rules * License must be `active` and not expired; the app must not be paused. * `expiresAt` must be in the future and on or before the license expiry. `null` (lifetime) is only accepted for licenses without an expiry. * `hwidMode: "bound"` needs 1 to 16 HWIDs. `hwidMode: "any"` needs `allowUnbound: true`. * Rate limited: 30 mints per minute per account, 10 per minute per license. * **Billing:** 1 credit per successful mint, debited through the same path as `/auth/validate` (so your app's hourly/daily burn caps apply). Rejected mints are free. A mint fails with `no_credits` before anything is signed. * Every mint is audited (actor, time, license, expiry, HWID policy, file id and hash). ## Verifying on the air-gapped machine Every official SDK (1.2.0+) ships two things: * `verifyLicenseFile(...)`: a pure function that checks a file and returns the decoded license or an error code. No client state, no network. * `client.loginFromFile(pathOrText)`: verifies with the client's configured app id, public key(s) and HWID, then marks the client authenticated so `isAuthenticated()`, `getLicenseVariables()` and `getAppVariables()` work exactly as after an online `login()`. It **never** starts the grace-period timer or online check-ins. `logout()` clears it. ```python Python theme={null} from authforge import AuthForgeClient client = AuthForgeClient(app_id="APP_ID", app_secret=None, public_key="PUBLIC_KEY", on_failure=lambda reason, exc: print(reason, exc)) print("HWID:", client.get_hwid()) # customer sends this to you before you mint if client.login_from_file("license.authforge"): info = client.get_offline_license() print("valid until", info["expires_at"] or "forever", client.get_license_variables()) ``` ```js Node.js theme={null} import { AuthForgeClient } from "@authforgecc/sdk"; const client = new AuthForgeClient({ appId: "APP_ID", publicKey: "PUBLIC_KEY", onFailure: (reason, error) => console.error(reason, error?.message) }); console.log("HWID:", client.getHwid()); if (client.loginFromFile("./license.authforge")) { console.log("valid until", client.getOfflineLicense().expiresAt ?? "forever", client.getLicenseVariables()); } ``` ```go Go theme={null} client, _ := authforge.New(authforge.Config{AppID: "APP_ID", PublicKey: "PUBLIC_KEY"}) fmt.Println("HWID:", client.HWID()) lic, err := client.LoginFromFile("license.authforge") if errors.Is(err, authforge.ErrOfflineExpired) { log.Fatal("offline license expired - request a new file") } else if err != nil { log.Fatal(err) } fmt.Println("valid until", lic.ExpiresAt) // nil = lifetime ``` ```rust Rust theme={null} let client = AuthForgeClient::new(AuthForgeConfig { app_id: "APP_ID".into(), public_key: "PUBLIC_KEY".into(), ..Default::default() }); println!("HWID: {}", client.hwid()); match client.login_from_file("license.authforge") { Ok(lic) => println!("valid until {:?}", lic.expires_at), // None = lifetime Err(OfflineLicenseError::HwidMismatch) => eprintln!("this file is bound to another machine"), Err(err) => eprintln!("rejected: {err}"), } ``` ```csharp C# theme={null} var client = new AuthForgeClient(appId: "APP_ID", appSecret: "", publicKey: "PUBLIC_KEY", onFailure: (reason, ex) => Console.Error.WriteLine($"{reason}: {ex?.Message}")); Console.WriteLine($"HWID: {client.GetHwid()}"); if (client.LoginFromFile("license.authforge")) { Console.WriteLine($"valid until {client.GetOfflineLicense()!.ExpiresAt ?? "forever"}"); } ``` ```cpp C++ theme={null} authforge::AuthForgeClient client("APP_ID", "", "PUBLIC_KEY"); std::cout << "HWID: " << client.GetHwid() << "\n"; if (client.LoginFromFile("license.authforge")) { const auto info = client.GetOfflineLicense(); std::cout << "valid until " << (info->expiresAt ? *info->expiresAt : "forever") << "\n"; } ``` ### Rejection codes Checks run in this fixed order in every SDK; the first failure wins. | Code | Meaning | | --- | --- | | `bad_armor` | Not a well-formed `.authforge` file (truncated, wrong markers, not base64) | | `bad_signature` | Signature does not verify against any configured public key: tampered file, or a file for a different app's key | | `unsupported_version` | The payload `v` is newer than this SDK understands: upgrade the SDK | | `malformed_payload` | Signed payload is missing required fields | | `wrong_app` | Valid signature, but the file was minted for a different app id | | `expired` | Past the file's `expiresAt` | | `hwid_mismatch` | File is HWID-bound and this machine's HWID is not in the list | `loginFromFile` reports these through `onFailure("offline_login_failed", error)` and returns false. Unlike `login()`, it never exits the process on its own: an air-gapped user deserves a message before the app closes. Offline verification depends on the machine's clock for `expired`. A user can extend a file by winding the clock back, exactly like the grace period. Short expiries and HWID binding limit the blast radius; there is no server to consult. ## File format (version 1) The file is text, safe to email or copy through a data diode: ```text theme={null} -----BEGIN AUTHFORGE LICENSE----- Version: 1 App-Id: 3d3a... License: XXXX-XXXX-XXXX-XXXX Key-Id: 8c9e... Expires-At: 2027-01-01T00:00:00.000Z eyJ2IjoxLCJ0eXAiOiJhdXRoZm9yZ2UtbGljZW5zZSIsImFwcElkIjoiM2QzYS4u ... -----END AUTHFORGE LICENSE----- -----BEGIN AUTHFORGE SIGNATURE----- OUi4frE6lPUXqvZDs4nHvbHGsR4ZKBis6lrJjnZfPbFinPtbVBrE/pwW9F6PHJsx G+RbBBOIN+JN70CcDsOrAg== -----END AUTHFORGE SIGNATURE----- ``` * **Headers are informational.** SDKs read every decision-relevant field from the signed payload, never from the headers. * **Payload** (base64 JSON): `v` (1), `typ` (`authforge-license`), `appId`, `licenseKey`, `jti` (unique file id), `kid` (the app signing key id), `issuedAt`, `expiresAt` (ISO 8601 or `null`), `hwid` (`{ "mode": "bound", "hwids": [...] }` or `{ "mode": "any" }`), and optional snapshots taken at mint time: `label`, `licenseExpiresAt`, `licenseVariables`, `appVariables`. * **Signed bytes:** the Ed25519 signature covers the UTF-8 bytes of the base64 payload *string* (body lines joined, all whitespace removed). This is the same contract as `/auth/validate` responses, which is why every SDK reuses its existing verify routine and why there is no JSON canonicalisation step. * **Keys:** the file is signed with your app's existing per-app Ed25519 key (KMS-protected in AuthForge) and verified with the same public key you already embed in your app. Rotating the app key means new files carry the new `kid`; SDKs accept a list of public keys, so keep the old one in the list until every old file has expired. * **Versioning:** SDKs reject any `v` other than 1 with `unsupported_version`. Future versions will be additive and announced with an SDK release. Variable snapshots are frozen at mint time. If you change a license variable later, machines running on an existing file keep the old value until you mint and deliver a new file. ## Where this fits * **Default:** online `login()` + grace period. Cheapest, revocable at the next activation, zero operator work. * **High-value software with connectivity:** online `login()` + [online check-ins](/concepts#online-check-ins-optional) for fast revocation. * **Machines that can never connect:** offline license files, short-lived and HWID-bound, re-issued on a schedule. See [Offline licensing best practices](/best-practices/offline-licensing). Out of scope, by design: bring-your-own signing keys, client-side minting, floating seats, and any claim of remote revocation for files already distributed. # 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. # Unlimited-seat licenses Source: https://docs.authforge.cc/features/unlimited-seats Issue a single license key that any number of devices can activate, with no exclusive HWID binding. A standard AuthForge license is bound to a fixed number of HWIDs (1-16, configurable per key via `maxHwidSlots`). Once those slots fill up, new devices are rejected with `hwid_mismatch`. That's the right default for paid software. For the rest of the time - internal tools, beta cohorts, freeware that just wants license-gated features, demo and sandbox keys - you want the opposite: **one shared key, any device, any time, no limits**. AuthForge supports this as a per-license toggle called **unlimited seats**. ## What it is When a license is marked as unlimited-seat: * The server accepts any HWID that authenticates against this key. * The first activator does **not** get exclusive ownership. There is no "binding" to undo or reset. * The `hwid_mismatch` error is structurally impossible for this license. * `maxHwidSlots` is still stored on the license but is ignored at validation time. (You can flip the toggle off later to re-enforce the cap.) Everything else is unchanged: app secret verification, nonce replay protection, the signed Ed25519 response payload, expiration, revocation, rate limiting, and webhooks all behave exactly the same as for finite-seat licenses. ## When to use it Reasonable fits: * **Internal tools.** A single shared key for the whole team, no HWID juggling when laptops get reimaged. * **Beta programs.** You want the testers to be the same set of people you control via key issuance, not via per-device caps. * **Freeware with license-gated extras.** The license is a feature flag, not a payment receipt - capping per device adds friction without revenue protection. * **Demo or sandbox keys.** You're publishing the credentials in docs or a README. Any cap you set will be hit by the first user. ## When *not* to use it * **Per-seat pricing.** If your business model bills per device, finite `maxHwidSlots` is the seat enforcement. Unlimited would silently waive it. * **Anti-piracy hardening.** Unlimited-seat licenses can be shared like a Netflix password. If pirates getting the key matters more to you than legitimate user friction, keep seats finite. * **Trial keys with multi-device limits.** If you want each trial confined to one machine, leave seats finite (probably `maxHwidSlots: 1`). ## You still get device analytics Even though no HWIDs are bound to the license, every activation is logged. The auth log records `appId`, `licenseKey`, `hwid`, `ip`, `result`, and timestamp - exactly as for seat-enforced licenses. You can: * Look at the license's **Activity** panel in the dashboard for distinct devices over time. * Query the same data via the `GET /v1/licenses/:licenseKey/logs` developer-API endpoint. * Subscribe to `license.validated` webhooks if you need a real-time stream. So you do not lose visibility into "how many distinct devices are using this key" - you just don't reject new ones when the count grows. ## It's a per-license setting, not a per-app setting Unlimited seats is a flag on the license row, not on the application. One application can have: * 100 paid customer licenses with `maxHwidSlots: 1`, * 5 employee licenses with `maxHwidSlots: 3`, * 1 unlimited-seat key for your public sandbox / docs, all coexisting on the same `appId`, all using the same SDK build. ## How to set it ### Dashboard When generating licenses, toggle **Unlimited seats (shared key)** in the create form. The "Max devices per key" input is greyed out while the toggle is on (the value is still stored, so flipping the toggle off restores the previous cap). ### Developer API Pass `unlimitedHwidSlots: true` in the `POST /v1/licenses` body: ```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, "expiresAt": null, "unlimitedHwidSlots": true, "label": "team-shared" }' ``` The signed validate payload for an unlimited-seat license contains `unlimitedHwidSlots: true`; SDKs that don't know about the field simply ignore it, so old SDK versions keep working. ## Related * [Hardware locking (HWID)](/features/hwid-locking) - the default, finite-seat behaviour. * [Try it without signing up](/try-it) - the public sandbox is itself an unlimited-seat license, so you can hit `/auth/validate` from any device with the same key. * [Licenses API reference](/api/licenses) - full request/response schema for the create endpoint. # 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 activation time, not on every online check-in. 2. To pick up variable changes, the user must re-authenticate (restart the app, or wait for the next activation after the grace period ends). 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: ``` theme={null} 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 publishes a single, machine-readable documentation bundle at **[https://docs.authforge.cc/llms-full.txt](https://docs.authforge.cc/llms-full.txt)**. It contains the full docs site — SDK reference pages, public auth error codes, best practices, and integration guides — in one file. Point your AI agent at this URL (or paste the prompt below) and it can load everything it needs without you attaching files from GitHub. For a shorter index, see [llms.txt](https://docs.authforge.cc/llms.txt). Cursor and other MCP-capable tools can also use the docs MCP server at `https://docs.authforge.cc/mcp` to search and fetch pages on demand. ## Integration prompt Paste this into your AI tool with your project open. The agent should fetch the docs itself in Step 1 — you do not need to attach `AGENTS.md` or other reference files manually. ``` theme={null} You are going to integrate AuthForge license validation into my project. Do not write any code yet. Follow this protocol exactly. STEP 1 - LOAD REFERENCES Fetch https://docs.authforge.cc/llms-full.txt and read it end-to-end. This file is authoritative. If anything below conflicts with it, follow the docs, not your prior knowledge. STEP 2 - LEARN MY PROJECT Look at the project I have open. Identify: - Language and framework (e.g. .NET 8 WPF, Python CLI, Electron, Go service) - Where startup happens (main, App.xaml.cs, __main__, etc.) - How the app currently handles configuration and secrets - Whether there is already a user-facing settings / login UI STEP 3 - IMPLEMENT For every file you touch: - Match my project's existing style (indentation, naming, error handling patterns - look at neighbouring files) - Use the official AuthForge SDK for my language. If no official SDK exists for my stack, say so and stop - do not roll your own client unless explicitly requested. - Verify the Ed25519 signature on every response. The SDK does this automatically; if you are writing raw HTTP for some reason, do not skip it. - Handle every error code listed in the docs' "Public auth error codes" section, not just the happy path. - Default: activate online once, then rely on the grace period (signed session TTL; 24h default, up to 7 days). Do not enable online check-ins unless I ask. - Before enabling online check-ins (periodic /auth/heartbeat), explain pros and cons (faster revocation and concurrent-use detection vs credit cost and needing connectivity) and wait for my go-ahead. - Do NOT use offline license files (loginFromFile / .authforge) unless I explicitly say the target machines are air-gapped; if I do, follow the docs' "Offline license files" page and keep the online path for everyone else. ``` After the agent completes Step 2, provide your **App ID** and **App Secret** through your project's existing config mechanism (environment variables, user secrets, etc.) — never commit credentials to source control. ## Supported AI tools This workflow works with Cursor, GitHub Copilot, Claude Code, Windsurf, and any assistant that can fetch URLs or read attached context files. If your tool cannot fetch URLs at runtime, download [llms-full.txt](https://docs.authforge.cc/llms-full.txt) once and attach it to the session instead of relying on Step 1's fetch. ## Tips for better results * **Open the right project** before pasting the prompt so Step 2 can inspect your actual entry point, config, and UI patterns. * **Name your stack explicitly** in a follow-up if it is ambiguous (for example ".NET 8 WPF" or "Electron + React"). * **Ask for a plan first** — the prompt already defers implementation until after Steps 1–2; keep that order if you iterate. * **Review error handling** against the "Public auth errors" section in llms-full.txt before shipping; generated code often skips `hwid_mismatch`, `session_expired`, and rate-limit paths. * **Say "air-gapped" only if you mean it.** Agents that see the words "offline" or "no internet" sometimes reach for offline license files; the grace period already covers ordinary connectivity gaps. Ask for `.authforge` support only when machines truly never connect. * Each official SDK repo also ships an **`AGENTS.md`** with copy-paste patterns and a "Do NOT" checklist — useful as a second pass after the main integration. ## Official SDKs llms-full.txt includes per-language SDK pages. Repositories: * [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) # 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 the default configuration (no online check-ins) for the bot process. Online check-ins are 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. 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`, etc.). It performs the same `/auth/validate` request and signature verification as `login`, but **does not start background 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 running through the grace period. ## 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, 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-activates after the grace period ends). ## 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). 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 background 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, hwidOverride: `tg:${telegramUserId}`, }); ``` ### Python ```python theme={null} client = AuthForgeClient( app_id=APP_ID, app_secret=APP_SECRET, public_key=PUBLIC_KEY, hwid_override=f"discord:{discord_user_id}", ) ``` ### Go ```go theme={null} client, err := authforge.New(authforge.Config{ AppID: appID, AppSecret: appSecret, PublicKey: publicKey, HWIDOverride: fmt.Sprintf("tg:%d", telegramUserID), }) ``` ### C# ```csharp theme={null} var client = new AuthForgeClient( appId: appId, appSecret: appSecret, publicKey: publicKey, 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(), hwid_override: Some(format!("tg:{telegram_user_id}")), ..Default::default() }); ``` ### C++ ```cpp theme={null} authforge::AuthForgeClient client( appId, appSecret, publicKey, authforge::OnlineHeartbeat::Off, 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: ``` theme={null} 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 --> BillingGrace: Payment fails BillingGrace --> Active: Payment retried BillingGrace --> Revoked: Billing grace window 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 online check-in or re-activation 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 long the billing grace window should be (this is your dunning policy, unrelated to the SDK session 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 the default configuration (no online check-ins) for the bot process. Online check-ins target software running on the licensee’s device; a Telegram bot only performs validation on your infrastructure, so the grace period's 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 background 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, 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, 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, signed sessions, 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 session 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. * **Grace period on a signed session**: After activation, the app runs on an Ed25519-signed session with no further network calls (24 hours by default, configurable up to 7 days). Optional **online check-ins** verify the license periodically for fast revocation: revoke a key from the dashboard and it takes effect on the next check-in. * **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. * **Offline license files (`.authforge`)**: For machines that never connect to the internet, mint a signed license file in the cloud (1 credit) and let the SDK verify it locally with only your public key. A separate mode from the grace period; issued files stay valid until their own expiry. See [Offline license files](/features/offline-license-files). ## 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) Note over SDK: Grace period: session re-verified locally, no network calls opt Online check-ins enabled (every 15 min) SDK->>API: POST /auth/heartbeat (session token) API-->>SDK: Signed confirmation, session refreshed 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. **The app runs through the grace period** on the signed session, with no further network calls. Enable optional online check-ins to catch revocations quickly and keep the session refreshed. ## Credit model AuthForge uses a credit-based billing model: | Action | Credit cost | |--------|-------------| | Successful license validation (`/auth/validate`) | 1 credit | | 10 successful heartbeats (optional online check-ins) | 1 credit | | Minting an offline license file (`.authforge`) | 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@latest ``` 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", ) license_key = input("Enter license key: ") if client.login(license_key): print("Authenticated! Running app...") # Your app logic here; the signed session covers the grace period 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" ); Console.Write("Enter license key: "); var key = Console.ReadLine() ?? ""; if (client.Login(key)) { Console.WriteLine("Authenticated! Running app..."); // Your app logic here; the signed session covers the grace period } 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" ); 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; the signed session covers the grace period } else { std::cout << "Invalid license key." << std::endl; return 1; } return 0; } ``` ```rust Rust theme={null} use authforge::{AuthForgeClient, AuthForgeConfig}; 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(), ..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; the signed session covers the grace period } 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", }) 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; the signed session covers the grace period } ``` ```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', }); 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; the signed session covers the grace period } 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. **Grace period started**: Your app now runs on the signed session with no further network calls. The SDK re-verifies the session signature locally in the background and stops when the session expires (24 hours by default, configurable via the SDK's `ttl` option, clamped to 1 hour minimum and 7 days maximum). If you need faster revocation, enable [online check-ins](/concepts#online-check-ins-optional): periodic signed `POST /auth/heartbeat` calls, every 15 minutes by default. This online activate + grace period flow is the default and the right choice for almost every app. If a customer's machine can **never** reach the internet, that is a different product mode: [offline license files](/features/offline-license-files) (`.authforge`), which you mint in the dashboard and the SDK verifies with `loginFromFile`. Do not reach for it unless you need it. ## Next steps * [SDK Best Practices](/sdk/best-practices); How to handle errors, the grace period, and graceful shutdown * [Core Concepts](/concepts); Understand HWIDs, the grace period, online check-ins, 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, check-in failures, the grace period, 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." | | `malformed_request` | Request body is missing or not valid JSON | This is a developer integration error. Fix request serialization. | | `system_error` | Unexpected server-side failure | "Authentication service temporarily unavailable. Please try again." Retry with backoff. | | Network error | No connectivity | "Could not connect to authentication server. Check your internet connection." | 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) ``` --- ## Check-in and session failure handling The `onFailure` callback fires with `"heartbeat_failed"` when an online check-in fails (if you enabled them) or when the signed session expires at the end of the grace period. **Don't kill the app immediately.** The user could lose unsaved work. A failed online check-in might be a momentary network blip. ### The retry window pattern When a check-in fails, start a countdown. Show a non-intrusive warning. If the next check-in succeeds, cancel the countdown. ```python theme={null} import threading retry_timer = None RETRY_WINDOW = 300 # 5 minutes def on_failure(reason, exception): global retry_timer if reason == "heartbeat_failed": if retry_timer is None: retry_timer = threading.Timer(RETRY_WINDOW, force_shutdown) retry_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 retry_timer if retry_timer: retry_timer.cancel() retry_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 check-in failure or session expiry | |----------|---------------------| | **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="...", public_key="YOUR_PUBLIC_KEY", 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: "...", publicKey: "YOUR_PUBLIC_KEY", 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( "...", "...", "YOUR_PUBLIC_KEY", authforge::OnlineHeartbeat::Off, 900, "https://auth.authforge.cc", on_failure ); ``` --- ## Poor connectivity and the grace period The default behavior is built for applications where users may not always have internet access: * The initial `login()` (activation) call **always** requires network; make this clear in your app's system requirements. * After activation, the SDK re-verifies the stored session signature locally during the grace period and makes no network calls unless online check-ins are enabled. * The grace period ends when the session TTL elapses (default 24 hours; configurable via the SDK's `ttlSeconds` / `session_ttl_seconds` / `SessionTTL` option, clamped to 1 hour minimum and 7 days maximum). Revocations don't take effect until the SDK makes a new server call. * With online check-ins enabled, each successful check-in refreshes the session automatically; long-running apps stay authenticated indefinitely as long as check-ins succeed, but network is required while the app runs. ### Check-in failure tolerance With online check-ins enabled, individual check-in 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. ### Machines that can never connect If a customer's machine has no internet access at all, the grace period cannot help: it needs one online activation and lapses within 7 days. For those customers use [offline license files](/features/offline-license-files): you mint a signed `.authforge` file in the cloud, deliver it out-of-band, and the SDK verifies it with `loginFromFile` using only your public key and the machine HWID. Keep the default online `login()` for everyone else, and read [Offline licensing best practices](/best-practices/offline-licensing) before shipping the offline path. --- ## Multi-instance and multi-window If your app can be opened multiple times on the same machine: * **Only one instance should authenticate.** Use a lockfile or IPC mechanism to coordinate. * All instances share the same HWID, so extra instances won't consume HWID slots. * Each `login()` call consumes a credit. If your app opens 10 windows, don't call login 10 times. ```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 session re-verification during the grace period can be bypassed by freezing the system clock. Online check-ins require a valid signed response from the API on every interval. # 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" ); 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; the signed session covers the grace period } 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) authforge::OnlineHeartbeat::Off,// Optional; ::On enables periodic online check-ins (/auth/heartbeat) 900, // Optional; background timer cadence (check-ins when online, local session checks otherwise) "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: grace period (session TTL). 0 = server default (24h). Clamped to [3600, 604800]. "" // Optional; hwidOverride: custom identity (for example "tg:123456789") ); ``` ### `ttlSeconds` (grace period) Requested session token lifetime in seconds for `/auth/validate`, passed as the 9th constructor parameter. This is the grace period: how long the app keeps running on the signed session without contacting AuthForge. 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, and apps relying on the grace period alone can extend their window up to 7 days. ## Billing * Each successful `Login()` or `ValidateLicense()` costs **1 credit** (one `/auth/validate` debit). * The grace period is free: local session re-verification makes no network calls and consumes no credits. * Online check-ins (if enabled) cost **1 credit per 10 successful heartbeats** (billed on every 10th call). * With online check-ins, revocations take effect on the **next** check-in regardless of interval; without them, at session expiry. ## Login ```cpp theme={null} bool success = client.Login(license_key); ``` Returns `true` on success, `false` otherwise. On success, a background thread re-verifies the signed session locally during the grace period (and sends online check-ins if `OnlineHeartbeat::On` was passed). ## Validate license (no session) ```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 background 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 << "Session check 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", authforge::OnlineHeartbeat::Off, 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. ## Grace period and online check-ins By default, the app runs through the grace period after activation: the SDK re-verifies the signed session locally, makes no network calls, and stops when the session expires. Opt in to online check-ins for fast revocation and concurrent-use detection by passing `authforge::OnlineHeartbeat::On` as the 4th constructor argument: ```cpp theme={null} // Default: activate online, then run through the grace period (24h by default) authforge::AuthForgeClient client("...", "...", "..."); // Strict policy: periodic online check-ins for fast revocation authforge::AuthForgeClient strict_client("...", "...", "...", authforge::OnlineHeartbeat::On); ``` See [Online Check-ins](/concepts#online-check-ins-optional) for a detailed comparison. ## Offline license files (`.authforge`) For machines that never connect to the internet, the operator mints a signed [offline license file](/features/offline-license-files) in the dashboard or Developer API (1 credit). The SDK verifies it locally with your public key and the machine HWID; no network, no check-ins, and online `Login()` is untouched. ```cpp theme={null} // Step 1 (customer machine): the customer sends you this so you can bind the file to it. std::cout << "Machine ID: " << client.GetHwid() << "\n"; // Step 2 (customer machine, after you deliver the file): path or armored text. if (client.LoginFromFile("license.authforge")) { const auto info = client.GetOfflineLicense(); std::cout << "Offline license valid until " << (info->expiresAt ? *info->expiresAt : "forever") << "\n"; } ``` Failures call `onFailure("offline_login_failed", &exc)` with `exc.what()` set to the code and return `false` (never `std::exit`). Codes, in check order: `bad_armor`, `bad_signature`, `unsupported_version`, `malformed_payload`, `wrong_app`, `expired`, `hwid_mismatch`. `client.VerifyLicenseFile(pathOrText)` and the free function `authforge::VerifyLicenseFile(text, appId, publicKeys, hwid)` run the same checks without touching state and return a `VerifyLicenseFileResult`. Issued files cannot be revoked remotely; they stay valid until their own expiry. ### Migrating from `"SERVER"` / `"LOCAL"` The legacy constructors taking a `"SERVER"` or `"LOCAL"` string as the 4th argument are deprecated but still work: * `"LOCAL"`: drop the argument; the grace period is now the default. * `"SERVER"`: pass `authforge::OnlineHeartbeat::On` instead. ## 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", authforge::OnlineHeartbeat::On, // Strict policy: check in every 15 minutes 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" ); Console.Write("Enter license key: "); var key = Console.ReadLine() ?? ""; if (client.Login(key)) { Console.WriteLine("Authenticated!"); // Your app logic here; the signed session covers the grace period } 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) onlineHeartbeat: false, // Optional; true enables periodic online check-ins (/auth/heartbeat) heartbeatInterval: 900, // Optional; background timer cadence (check-ins when online, local session checks otherwise) apiBaseUrl: "https://auth.authforge.cc", // Optional onFailure: null, // Optional; Action requestTimeout: 15, // Optional; HTTP timeout in seconds ttlSeconds: null, // Optional; grace period (session TTL). null = server default (24h). Clamped to [3600, 604800]. hwidOverride: null // Optional; custom identity (for example "tg:123456789") ); ``` ### `ttlSeconds` (grace period) Requested session token lifetime in seconds for `/auth/validate`. This is the grace period: how long the app keeps running on the signed session without contacting AuthForge. 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, and apps relying on the grace period alone can extend their window up to 7 days. ## Billing * Each successful `Login()` or `ValidateLicense()` costs **1 credit** (one `/auth/validate` debit). * The grace period is free: local session re-verification makes no network calls and consumes no credits. * Online check-ins (if enabled) cost **1 credit per 10 successful heartbeats** (billed on every 10th call). * With online check-ins, revocations take effect on the **next** check-in regardless of interval; without them, at session expiry. ## Login ```csharp theme={null} bool success = client.Login(licenseKey); ``` Returns `true` if authentication succeeded, `false` otherwise. On success, the SDK starts a background thread that re-verifies the signed session locally during the grace period (and sends online check-ins if `onlineHeartbeat: true`). ## Validate license (no session) ```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 background thread. ## Failure callback If authentication fails, an online check-in fails, or the session expires, 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("Session check failed; saving state."); SaveApplicationState(); } } var client = new AuthForgeClient( appId: "YOUR_APP_ID", appSecret: "YOUR_APP_SECRET", publicKey: "YOUR_PUBLIC_KEY", 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(); } } ``` ## Grace period and online check-ins By default, the app runs through the grace period after activation: the SDK re-verifies the signed session locally, makes no network calls, and stops when the session expires. Opt in to online check-ins for fast revocation and concurrent-use detection: ```csharp theme={null} // Default: activate online, then run through the grace period (24h by default) var client = new AuthForgeClient( appId: "...", appSecret: "...", publicKey: "YOUR_PUBLIC_KEY", ttlSeconds: 259200 // Optional: extend the grace period to 3 days (clamped to 1h-7d) ); // Strict policy: periodic online check-ins for fast revocation var strictClient = new AuthForgeClient( appId: "...", appSecret: "...", publicKey: "YOUR_PUBLIC_KEY", onlineHeartbeat: true, heartbeatInterval: 900 // 15 minutes (default) ); ``` See [Online Check-ins](/concepts#online-check-ins-optional) for a detailed comparison. ## Offline license files (`.authforge`) For machines that never connect to the internet, the operator mints a signed [offline license file](/features/offline-license-files) in the dashboard or Developer API (1 credit). The SDK verifies it locally with your public key and the machine HWID; no network, no check-ins, and online `Login()` is untouched. ```csharp theme={null} // Step 1 (customer machine): the customer sends you this so you can bind the file to it. Console.WriteLine($"Machine ID: {client.GetHwid()}"); // Step 2 (customer machine, after you deliver the file): path or armored text. if (client.LoginFromFile("license.authforge")) { var info = client.GetOfflineLicense()!; Console.WriteLine($"Offline license valid until {info.ExpiresAt ?? "forever"}"); } ``` Failures call `onFailure("offline_login_failed", new ArgumentException(code))` and return `false` (never `Environment.Exit`). Codes, in check order: `bad_armor`, `bad_signature`, `unsupported_version`, `malformed_payload`, `wrong_app`, `expired`, `hwid_mismatch`. `client.VerifyLicenseFile(pathOrText)` and the static `AuthForgeClient.VerifyLicenseFile(text, appId, publicKeys, hwid)` run the same checks without touching state. Issued files cannot be revoked remotely; they stay valid until their own expiry. ### Migrating from `heartbeatMode` The legacy string `heartbeatMode` constructor is marked obsolete but still works: * `heartbeatMode: "LOCAL"`: remove the argument; the grace period is now the default. * `heartbeatMode: "SERVER"`: use `onlineHeartbeat: true` instead. ## 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", onlineHeartbeat: true, // Optional strict policy: check in every 15 minutes 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@latest ``` 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", 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) OnlineHeartbeat: false, // Optional: true enables periodic online check-ins (/auth/heartbeat) HeartbeatInterval: 15 * time.Minute, // Optional: background timer cadence (check-ins when online, local session checks otherwise) APIBaseURL: "https://auth.authforge.cc",// Optional OnFailure: nil, // Optional: func(error string) RequestTimeout: 15 * time.Second, // Optional SessionTTL: 0, // Optional: time.Duration. Grace period. 0 = server default (24h). Clamped to [1h, 7d]. HWIDOverride: "", // Optional: custom identity (for example "tg:123456789") }) ``` ### `SessionTTL` (grace period) Requested lifetime of the session token returned by `/auth/validate`. This is the grace period: how long the app keeps running on the signed session without contacting AuthForge. 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, and apps relying on the grace period alone can extend their window up to 7 days: ```go theme={null} client, _ := authforge.New(authforge.Config{ AppID: "...", AppSecret: "...", PublicKey: "YOUR_PUBLIC_KEY", SessionTTL: 72 * time.Hour, // 3-day grace period }) ``` ## Billing * Each successful `Login()` or `ValidateLicense()` costs **1 credit** (one `/auth/validate` debit). * The grace period is free: local session re-verification makes no network calls and consumes no credits. * Online check-ins (if enabled) cost **1 credit per 10 successful heartbeats** (billed on every 10th call). * With online check-ins, revocations take effect on the **next** check-in regardless of interval; without them, at session expiry. ## Validate license (no session) `ValidateLicense` performs the same `/auth/validate` request and signature verification as `Login`, but does **not** persist session fields on the client, start the background 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 background loop / 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: per IP / per license; heartbeat: 6/min per license) case errors.Is(err, authforge.ErrReplayDetected): // replay detected (validate only) case errors.Is(err, authforge.ErrSignatureMismatch): // signature verification failed default: // transport or unexpected error } } ``` ## Grace period and online check-ins By default, the app runs through the grace period after activation: the SDK re-verifies the signed session locally, makes no network calls, and stops when the session expires. Opt in to online check-ins for fast revocation and concurrent-use detection: ```go theme={null} // Default: activate online, then run through the grace period (24h by default) client, _ := authforge.New(authforge.Config{ AppID: "...", AppSecret: "...", PublicKey: "YOUR_PUBLIC_KEY", }) // Strict policy: periodic online check-ins for fast revocation client, _ := authforge.New(authforge.Config{ AppID: "...", AppSecret: "...", PublicKey: "YOUR_PUBLIC_KEY", OnlineHeartbeat: true, }) ``` See [Online Check-ins](/concepts#online-check-ins-optional) for a detailed comparison. ## Offline license files (`.authforge`) For machines that never connect to the internet, the operator mints a signed [offline license file](/features/offline-license-files) in the dashboard or Developer API (1 credit). The SDK verifies it locally with your public key and the machine HWID; no network, no check-ins, and online `Login` is untouched. ```go theme={null} // Step 1 (customer machine): the customer sends you this so you can bind the file to it. fmt.Println("Machine ID:", client.HWID()) // Step 2 (customer machine, after you deliver the file): path or armored text. lic, err := client.LoginFromFile("license.authforge") switch { case errors.Is(err, authforge.ErrOfflineExpired): log.Fatal("offline license expired - request a new file") case errors.Is(err, authforge.ErrOfflineHwidMismatch): log.Fatal("this file is bound to a different machine") case err != nil: log.Fatal(err) } fmt.Println("Offline license valid until", lic.ExpiresAt) // nil pointer = lifetime ``` Errors are sentinels for `errors.Is`, in check order: `ErrOfflineBadArmor`, `ErrOfflineBadSignature`, `ErrOfflineUnsupportedVersion`, `ErrOfflineMalformedPayload`, `ErrOfflineWrongApp`, `ErrOfflineExpired`, `ErrOfflineHwidMismatch`; `authforge.OfflineErrorCode(err)` returns the cross-SDK string and `OnFailure` receives `offline_login_failed: `. `client.VerifyLicenseFile(pathOrText)` and the package function `authforge.VerifyLicenseFile(text, opts)` run the same checks without touching state. Issued files cannot be revoked remotely; they stay valid until their own expiry. ### Migrating from `HeartbeatMode` `HeartbeatMode` (`"server"` / `"local"`) is deprecated and may now be left empty: * `HeartbeatMode: "local"`: remove the field; the grace period is now the default. * `HeartbeatMode: "server"`: use `OnlineHeartbeat: true` instead. The old field still works but is deprecated. ## 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", }); 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 - the signed session covers the grace period } 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) onlineHeartbeat: false, // Optional - true enables periodic online check-ins (/auth/heartbeat) heartbeatInterval: 900, // Optional - background timer cadence (check-ins when online, local session checks otherwise) apiBaseUrl: "https://auth.authforge.cc", // Optional onFailure: null, // Optional - callback(reason, exception) requestTimeout: 15, // Optional - HTTP timeout in seconds ttlSeconds: null, // Optional - grace period (session TTL). null = server default (24h). Clamped to [3600, 604800]. hwidOverride: null, // Optional - custom identity (for example "tg:123456789") }); ``` ### `ttlSeconds` (grace period) Requested session token lifetime in seconds for `/auth/validate`. This is the grace period: how long the app keeps running on the signed session without contacting AuthForge. 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, and apps relying on the grace period alone can stretch their window up to 7 days. ## Billing * Each successful `login()` or `validateLicense()` costs **1 credit** (one `/auth/validate` debit). * The grace period is free: local session re-verification makes no network calls and consumes no credits. * Online check-ins (if enabled) cost **1 credit per 10 successful heartbeats** (billed on every 10th call). * With online check-ins, revocations take effect on the **next** check-in regardless of interval; without them, at session expiry. ## 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 a background timer that re-verifies the signed session locally during the grace period (and sends online check-ins if `onlineHeartbeat: true`). ## Validate license (no session) ```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 background timer. Use this for bots and repeated checks; use `login` when you want a long-lived session. 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 fails, an online check-in fails, or the session expires, 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("Session check failed - saving state and shutting down."); saveApplicationState(); } } const client = new AuthForgeClient({ appId: "YOUR_APP_ID", appSecret: "YOUR_APP_SECRET", publicKey: "YOUR_PUBLIC_KEY", 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(); } } ``` ## Grace period and online check-ins By default, the app runs through the grace period after activation: the SDK re-verifies the signed session locally, makes no network calls, and stops when the session expires. Opt in to online check-ins for fast revocation and concurrent-use detection: ```javascript theme={null} // Default: activate online, then run through the grace period (24h by default) const client = new AuthForgeClient({ appId: "YOUR_APP_ID", appSecret: "YOUR_APP_SECRET", publicKey: "YOUR_PUBLIC_KEY", ttlSeconds: 259200, // Optional: extend the grace period to 3 days (clamped to 1h-7d) }); // Strict policy: periodic online check-ins for fast revocation const strictClient = new AuthForgeClient({ appId: "YOUR_APP_ID", appSecret: "YOUR_APP_SECRET", publicKey: "YOUR_PUBLIC_KEY", onlineHeartbeat: true, heartbeatInterval: 900, // 15 minutes (default) }); ``` See [Online Check-ins](/concepts#online-check-ins-optional) for a detailed comparison. ## Offline license files (`.authforge`) For machines that never connect to the internet, the operator mints a signed [offline license file](/features/offline-license-files) in the dashboard or Developer API (1 credit). The SDK verifies it locally with your public key and the machine HWID; no network, no check-ins, and online `login()` is untouched. ```js theme={null} // Step 1 (customer machine): the customer sends you this so you can bind the file to it. console.log("Machine ID:", client.getHwid()); // Step 2 (customer machine, after you deliver the file): path or armored text. if (client.loginFromFile("./license.authforge")) { console.log("Offline license valid until", client.getOfflineLicense().expiresAt ?? "forever"); console.log(client.getLicenseVariables()); } ``` Failures call `onFailure("offline_login_failed", new Error(code))` and return `false` (never `process.exit`). Codes, in check order: `bad_armor`, `bad_signature`, `unsupported_version`, `malformed_payload`, `wrong_app`, `expired`, `hwid_mismatch`. `client.verifyLicenseFile(pathOrText)` and the exported `verifyLicenseFile({ file, appId, publicKey, hwid })` run the same checks without touching state. Issued files cannot be revoked remotely; they stay valid until their own expiry. ### Migrating from `heartbeatMode` `heartbeatMode: "LOCAL"` / `"SERVER"` is deprecated but still accepted: * `heartbeatMode: "LOCAL"`: remove the option; the grace period is now the default. * `heartbeatMode: "SERVER"`: use `onlineHeartbeat: true` instead. ## 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", onlineHeartbeat: true, // Optional strict policy: check in every 15 minutes 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, the grace period, and optional online check-ins. ## 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 and public key. Include the App Secret for online `login()` / `validateLicense`. For air-gapped `loginFromFile` only, omit the secret (or pass empty / `None`); do not ship it in offline binaries. 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 background loop** 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. **Grace period**: After `login`, the app runs on the signed session with no further network calls. A background thread re-verifies the session signature locally and stops when the session expires (session TTL: default 24 hours, configurable via `ttlSeconds`, clamped to 1 hour minimum and 7 days maximum). 5. **Online check-ins (optional)**: Enable `onlineHeartbeat` to send `POST /auth/heartbeat` at the configured interval (default 15 minutes) for fast revocation and concurrent-use detection. Success responses are Ed25519-signed and verified with the same app public key, and each check-in refreshes the session. Validate-only calls never start this loop. 6. **Failure**: If authentication fails, a check-in fails, or the session expires, 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. ### Offline license files (separate mode) For machines that can **never** reach AuthForge, every SDK (1.2.0+) also verifies cloud-minted [offline license files](/features/offline-license-files) (`.authforge`) with zero network access. This is not a variant of the grace period; it is a different credential: | | Python | Node.js | Go | Rust | C# | C++ | |---|---|---|---|---|---|---| | Authorize from file | `login_from_file(path_or_text)` | `loginFromFile(pathOrText)` | `LoginFromFile(pathOrText)` | `login_from_file(path_or_text)` | `LoginFromFile(pathOrText)` | `LoginFromFile(pathOrText)` | | Pure verify | `verify_license_file(...)` | `verifyLicenseFile(...)` | `VerifyLicenseFile(...)` | `verify_license_file(...)` | `VerifyLicenseFile(...)` | `VerifyLicenseFile(...)` | | Machine ID for binding | `get_hwid()` | `getHwid()` | `HWID()` | `hwid()` | `GetHwid()` | `GetHwid()` | | File in use | `get_offline_license()` | `getOfflineLicense()` | `OfflineLicense()` | `get_offline_license()` | `GetOfflineLicense()` | `GetOfflineLicense()` | `loginFromFile` verifies the file against the client's configured app id, public key(s) and HWID, then populates the same session accessors as `login()` (`isAuthenticated`, variables). It never starts the grace-period timer or online check-ins, and it never exits the process; rejections arrive as `onFailure("offline_login_failed", error)` with one of `bad_armor`, `bad_signature`, `unsupported_version`, `malformed_payload`, `wrong_app`, `expired`, `hwid_mismatch` (Go returns `ErrOffline*` sentinels, Rust an `OfflineLicenseError` enum). Online `login()` and the grace period are unchanged. ## 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 for online APIs; omit / empty for `loginFromFile` only | Your application secret. Authenticates `/auth/validate` and other online calls. **Do not ship it in air-gapped binaries** — offline file verification uses only the app id and public key. | | `publicKey` | string | required | Your app Ed25519 public key (base64) | | `onlineHeartbeat` | boolean | `false` | Enable periodic online check-ins (`/auth/heartbeat`) for fast revocation. Off by default: the app runs through the grace period on the signed session. | | `heartbeatInterval` | integer | `900` | Seconds between background timer ticks. With `onlineHeartbeat` enabled each tick calls `/auth/heartbeat` (server rate limit: 6 per minute per license); otherwise each tick re-verifies the signed session locally and enforces grace period expiry, with no network. | | `apiBaseUrl` | string | `https://auth.authforge.cc` | Override the API base URL | | `onFailure` | callback | `null` | Called on auth failure, check-in failure, or session expiry with `(reason, exception)` | | `requestTimeout` | integer | `15` | HTTP request timeout in seconds | | `ttlSeconds` | integer | `null` / `0` (server default: 86400) | Requested session token lifetime, which is the grace period. Server clamps to `[3600, 604800]` (1 hour to 7 days). Preserved across heartbeat refreshes. | | `heartbeatMode` | string | deprecated | Legacy option: `"LOCAL"` maps to the default (grace period), `"SERVER"` maps to `onlineHeartbeat: true`. Still accepted, but new code should not use it. | ## Billing model All SDKs follow the same billing rules: * Each successful `login()` / `Login()` or **`validateLicense`**-style call costs **1 credit** (one `/auth/validate` debit). * The grace period is free: local session re-verification consumes no credits. * Online check-ins (if enabled) cost **1 credit per 10 successful heartbeats** (debited on every 10th heartbeat). * With online check-ins, revocations take effect on the **next** check-in regardless of the configured interval; without them, at session expiry. * Minting an offline license file costs **1 credit** (charged to the operator at mint time). `loginFromFile` / `verifyLicenseFile` cost nothing. See [Managing Credits](/best-practices/credit-management) for usage estimates and cost-saving patterns. ## Migrating from `heartbeat_mode` Older SDK versions required a `heartbeatMode` / `heartbeat_mode` of `"SERVER"` or `"LOCAL"`. The option is deprecated across all SDKs: * **`"LOCAL"`**: remove the option; the grace period is now the default behavior. * **`"SERVER"`**: enable online check-ins instead (`onlineHeartbeat: true` or the language equivalent). The old option still works but emits a deprecation warning. See each SDK page for the exact migration syntax. ## 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, grace-period behavior, 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", ) license_key = input("Enter license key: ") if client.login(license_key): print("Authenticated!") # Your app logic here; the signed session covers the grace period 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) online_heartbeat=False, # Optional; True enables periodic online check-ins (/auth/heartbeat) heartbeat_interval=900, # Optional; background timer cadence (check-ins when online, local session checks otherwise) 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; grace period (session TTL). None = server default (24h). Clamped to [3600, 604800]. hwid_override=None # Optional; custom identity (for example "tg:123456789") ) ``` ### `ttl_seconds` (grace period) Requested session token lifetime in seconds for `/auth/validate`. This is the grace period: how long the app keeps running on the signed session without contacting AuthForge. 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, and apps relying on the grace period alone can extend their window up to 7 days. ## Billing * Each successful `login()` or `validate_license()` costs **1 credit** (one `/auth/validate` debit). * The grace period is free: local session re-verification makes no network calls and consumes no credits. * Online check-ins (if enabled) cost **1 credit per 10 successful heartbeats** (billed on every 10th call). * With online check-ins, revocations take effect on the **next** check-in regardless of interval; without them, at session expiry. ## Login ```python theme={null} success = client.login(license_key) ``` Returns `True` if authentication succeeded, `False` otherwise. On success, the SDK starts a background thread that re-verifies the signed session locally during the grace period (and sends online check-ins if `online_heartbeat=True`). ## Validate license (no session) ```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 background 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 fails, an online check-in fails, or the session expires, 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("Session check 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", 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() ``` ## Grace period and online check-ins By default, the app runs through the grace period after activation: the SDK re-verifies the signed session locally, makes no network calls, and stops when the session expires. Opt in to online check-ins for fast revocation and concurrent-use detection: ```python theme={null} # Default: activate online, then run through the grace period (24h by default) client = AuthForgeClient( app_id="...", app_secret="...", public_key="YOUR_PUBLIC_KEY", ttl_seconds=259200, # Optional: extend the grace period to 3 days (clamped to 1h-7d) ) # Strict policy: periodic online check-ins for fast revocation client = AuthForgeClient( app_id="...", app_secret="...", public_key="YOUR_PUBLIC_KEY", online_heartbeat=True, heartbeat_interval=900, # 15 minutes (default) ) ``` See [Online Check-ins](/concepts#online-check-ins-optional) for a detailed comparison. ## Offline license files (`.authforge`) For machines that never connect to the internet, the operator mints a signed [offline license file](/features/offline-license-files) in the dashboard or Developer API (1 credit). The SDK verifies it locally with your public key and the machine HWID; no network, no check-ins, and online `login()` is untouched. ```python theme={null} # Step 1 (customer machine): the customer sends you this so you can bind the file to it. print("Machine ID:", client.get_hwid()) # Step 2 (customer machine, after you deliver the file): path or armored text. if client.login_from_file("license.authforge"): info = client.get_offline_license() print("Offline license valid until", info["expires_at"] or "forever") print(client.get_license_variables()) ``` Failures call `on_failure("offline_login_failed", ValueError(code))` and return `False` (never `os._exit`). Codes, in check order: `bad_armor`, `bad_signature`, `unsupported_version`, `malformed_payload`, `wrong_app`, `expired`, `hwid_mismatch`. `client.verify_license_file(path_or_text)` and the module-level `authforge.verify_license_file(text, app_id, public_key, hwid)` run the same checks without touching state. Issued files cannot be revoked remotely; they stay valid until their own expiry. ### Migrating from `heartbeat_mode` `heartbeat_mode="LOCAL"` / `"SERVER"` is deprecated (it emits a `DeprecationWarning`) but still accepted: * `heartbeat_mode="LOCAL"`: remove the option; the grace period is now the default. * `heartbeat_mode="SERVER"`: use `online_heartbeat=True` instead. ## 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", online_heartbeat=True, # Optional strict policy: check in every 15 minutes 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}; 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(), 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) online_heartbeat: false, // Optional - true enables periodic online check-ins (/auth/heartbeat) heartbeat_interval: 900, // Optional, background timer cadence (check-ins when online, local session checks otherwise) api_base_url: "https://auth.authforge.cc".into(), // Optional on_failure: None, // Optional callback request_timeout: 15, // Optional, seconds session_ttl_seconds: None, // Optional - grace period (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) | | `online_heartbeat` | `bool` | `false` | Enable periodic online check-ins (`/auth/heartbeat`) for fast revocation | | `heartbeat_interval` | `u64` | `900` | Seconds between background timer ticks: check-ins when `online_heartbeat` is `true`, local session re-verification otherwise | | `api_base_url` | `String` | `https://auth.authforge.cc` | AuthForge API base URL | | `on_failure` | `Option>` | `None` | Invoked on check-in failure or session expiry | | `request_timeout` | `u64` | `15` | Timeout for API requests | | `session_ttl_seconds` | `Option` | `None` (server default: 86400) | Grace period: requested session token lifetime. Server clamps to `[3600, 604800]` (1 hour to 7 days); preserved across heartbeat refreshes. | | `hwid_override` | `Option` | `None` | Optional custom identity string sent as HWID (for example `tg:123456789`). | | `heartbeat_mode` | `HeartbeatMode` | deprecated | Legacy: `Local` maps to the default (grace period), `Server` maps to `online_heartbeat: true`. Do not use in new code. | ## Validate license (no session) `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 background 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). * The grace period is free: local session re-verification makes no network calls and consumes no credits. * Online check-ins (if enabled) cost **1 credit per 10 successful heartbeats** (billed on every 10th call). * With online check-ins, revocations take effect on the **next** check-in regardless of interval; without them, at session expiry. ## 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: per IP / per license; heartbeat: 6/min per license AuthForgeError::ReplayDetected // validate only; never returned from heartbeat AuthForgeError::SignatureMismatch AuthForgeError::NetworkError(String) AuthForgeError::Other(String) ``` ## Grace period and online check-ins By default, the app runs through the grace period after activation: the SDK re-verifies the signed session locally, makes no network calls, and stops when the session expires. Opt in to online check-ins for fast revocation and concurrent-use detection: ```rust theme={null} // Default: activate online, then run through the grace period (24h by default) let client = AuthForgeClient::new(AuthForgeConfig { app_id: "...".into(), app_secret: "...".into(), public_key: "YOUR_PUBLIC_KEY".into(), session_ttl_seconds: Some(259200), // Optional: extend the grace period to 3 days (clamped to 1h-7d) ..Default::default() }); // Strict policy: periodic online check-ins for fast revocation let strict_client = AuthForgeClient::new(AuthForgeConfig { app_id: "...".into(), app_secret: "...".into(), public_key: "YOUR_PUBLIC_KEY".into(), online_heartbeat: true, ..Default::default() }); ``` See [Online Check-ins](/concepts#online-check-ins-optional) for a detailed comparison. ## Offline license files (`.authforge`) For machines that never connect to the internet, the operator mints a signed [offline license file](/features/offline-license-files) in the dashboard or Developer API (1 credit). The SDK verifies it locally with your public key and the machine HWID; no network, no check-ins, and online `login` is untouched. ```rust theme={null} use authforge::OfflineLicenseError; // Step 1 (customer machine): the customer sends you this so you can bind the file to it. println!("Machine ID: {}", client.hwid()); // Step 2 (customer machine, after you deliver the file): path or armored text. match client.login_from_file("license.authforge") { Ok(lic) => println!("Offline license valid until {:?}", lic.expires_at), // None = lifetime Err(OfflineLicenseError::Expired) => eprintln!("offline license expired - request a new file"), Err(OfflineLicenseError::HwidMismatch) => eprintln!("this file is bound to a different machine"), Err(err) => eprintln!("offline license rejected: {err}"), } ``` Errors are the `OfflineLicenseError` enum (separate from `AuthForgeError`, which is unchanged), in check order: `BadArmor`, `BadSignature`, `UnsupportedVersion`, `MalformedPayload`, `WrongApp`, `Expired`, `HwidMismatch`, plus `ReadError(String)` for an unreadable path; `.code()` gives the cross-SDK string and `on_failure` receives `offline_login_failed: `. `client.verify_license_file(path_or_text)` and the crate function `authforge::verify_license_file(text, &opts)` run the same checks without touching state. Issued files cannot be revoked remotely; they stay valid until their own expiry. ### Migrating from `heartbeat_mode` The `heartbeat_mode` field and the `HeartbeatMode` enum are deprecated but still accepted: * `heartbeat_mode: HeartbeatMode::Local`: remove the field; the grace period is now the default. * `heartbeat_mode: HeartbeatMode::Server`: use `online_heartbeat: true` instead. Do not import `HeartbeatMode` in new code. # Try it without signing up Source: https://docs.authforge.cc/try-it Hit the live AuthForge auth endpoints from your terminal in 30 seconds. Real signed responses, no account required. If you've ever wondered "is this signed-response stuff actually real or marketing?", the curl examples below are your answer. The Node verification snippet at the bottom proves a real Ed25519 signature against the public key on this very page - no account, no SDK, no trust. The credentials hit a public sandbox app on the live `auth.authforge.cc` endpoint. Every request goes through the production code path - real Ed25519 signing, real nonce replay protection, real rate limits - and returns a payload that's byte-for-byte indistinguishable from one a paying customer would get. The only difference is that this app's traffic doesn't get billed. Public credentials, deliberately. Do not use them for anything important - rate limits are tighter, the daily request ceiling is shared with every other reviewer, and the license can be reset or revoked at any time. When you're ready to ship, [sign up](https://app.authforge.cc/auth) for free starter credits and your own keys. ## Demo credentials | Field | Value | |-------|-------| | Application ID | `789ffbc6-555c-4a3c-8e9b-89be6f865195` | | Application secret | `demo-key-1a73-471e-ad08-8bd159b7e69c` | | Application Ed25519 public key | `XRvq8r6PnKqJuNNBrNezhwPmZs887l1Fu4U/WGPz8Wc=` | | Demo license key | `HE6H-S2V5-PKMW-WDVD` | The license is **unlimited-seat** ([what does that mean?](/features/unlimited-seats)) - any HWID can authenticate against it, so it doesn't matter whether you've already used it on another machine or which fingerprint your shell sends. ## 1. Validate This is the first call every SDK makes. It binds your HWID (or in this case doesn't, since the key is unlimited-seat), debits one credit (skipped on the demo), and returns a signed session token plus the entitlements you'd want to render in your UI. ```bash theme={null} curl -sS -X POST https://auth.authforge.cc/auth/validate \ -H "Content-Type: application/json" \ -d '{ "appId": "789ffbc6-555c-4a3c-8e9b-89be6f865195", "appSecret": "demo-key-1a73-471e-ad08-8bd159b7e69c", "licenseKey": "HE6H-S2V5-PKMW-WDVD", "hwid": "demo-machine-001", "nonce": "your-random-nonce-12345678" }' ``` A successful response has this shape: | Field | Type | Notes | |-------|------|-------| | `status` | string | `"success"` on the happy path; `"failed"` with an `error` code otherwise. | | `payload` | string | Base64-encoded JSON. Decode it to read the entitlements (see fields below). | | `signature` | string | Base64 Ed25519 signature, always 88 chars. Verify it against the app's public key before trusting `payload`. | | `keyId` | string | Identifier for the signing key. Useful when rotating keys: clients can pin multiple trusted keys and use `keyId` to pick the right verifier. | After base64-decoding `payload` you'll see fields like `sessionToken`, `expiresIn`, `licenseExpiresAt`, `unlimitedHwidSlots: true`, and any application/license `variables` set on the app. The full field list is in [Concepts → Signed payload fields](/concepts#signed-payload-fields). We deliberately don't print a fake `payload`/`signature` example here - hand-crafted strings can't actually be verified, so they'd just be lying decoration. Run the curl above and you'll have a real one to feed into the Node snippet below. ## 2. Heartbeat With optional online check-ins enabled, your SDK keeps the session refreshed by calling `/auth/heartbeat` periodically with the `sessionToken` from the previous response. Heartbeats refresh the token and are how the server delivers fast revocations to running clients. (By default, SDKs skip this and run through the grace period on the signed session instead.) ```bash theme={null} curl -sS -X POST https://auth.authforge.cc/auth/heartbeat \ -H "Content-Type: application/json" \ -d '{ "appId": "789ffbc6-555c-4a3c-8e9b-89be6f865195", "sessionToken": "PASTE_SESSION_TOKEN_FROM_VALIDATE_RESPONSE", "hwid": "demo-machine-001" }' ``` Response is the same shape as validate: `{ status, payload, signature, keyId }`. The new `payload` carries a fresh `sessionToken` and `expiresIn`. ## 3. Self-ban Lets a client self-report cheating / tampering / EULA violations and ask the server to revoke or blacklist itself. Useful for anti-tamper integrations - the server is the source of truth and other SDKs on this license stop working immediately. **The demo app neutralizes selfban.** This call below is intentionally a no-op: it exists so you can see the request and response shape, the auth + rate-limit pipeline, and confirm that the endpoint accepts your payload. It does not revoke the demo license or blacklist anything, no matter what flags you send. If it did, the first reviewer would lock everyone else out of the demo. In your own app, the same handler with the same flags **does** revoke the license and persist the blacklist - that's the whole point of the endpoint. ```bash theme={null} curl -sS -X POST https://auth.authforge.cc/auth/selfban \ -H "Content-Type: application/json" \ -d '{ "appId": "789ffbc6-555c-4a3c-8e9b-89be6f865195", "appSecret": "demo-key-1a73-471e-ad08-8bd159b7e69c", "licenseKey": "HE6H-S2V5-PKMW-WDVD", "hwid": "demo-machine-001", "nonce": "another-random-nonce-87654321", "blacklistHwid": false, "blacklistIp": false }' ``` Response: ```json theme={null} { "status": "success", "mode": "pre_session", "performed": { "revokeLicense": false, "blacklistHwid": false, "blacklistIp": false } } ``` The `performed` object is the server's honest report of what it actually did - on the demo app, it's always all-false. In production it reflects the request flags (defaulting to `true` post-session, `false` for `revokeLicense` pre-session). ## Verify the signed payload (Node) This is the part that matters. The whole point of "signed validation" is that you can verify the response server-side without trusting the network or any TLS-terminating intermediary. Drop this into a `.mjs` file and run it after the validate call above: ```js theme={null} import { verify, createPublicKey } from "node:crypto"; // The Ed25519 public key from this docs page (raw 32-byte key, base64). const PUBLIC_KEY_B64 = "XRvq8r6PnKqJuNNBrNezhwPmZs887l1Fu4U/WGPz8Wc="; const publicKey = createPublicKey({ // Wrap raw Ed25519 bytes in a SPKI DER prefix so Node accepts them. key: Buffer.concat([ Buffer.from("302a300506032b6570032100", "hex"), Buffer.from(PUBLIC_KEY_B64, "base64") ]), format: "der", type: "spki" }); // Paste the validate response here (or fetch() it inline). const response = { status: "success", payload: "PASTE_PAYLOAD_FROM_VALIDATE_RESPONSE", signature: "PASTE_SIGNATURE_FROM_VALIDATE_RESPONSE", keyId: "PASTE_KEY_ID" }; // IMPORTANT: `response.payload` is used in two different ways below, and // that's deliberate - not a bug. The same base64 string serves two purposes: // // 1. As the input to `verify()`, treated as UTF-8 text. The server signs // the base64-encoded string itself (its UTF-8 bytes), NOT the raw JSON // bytes underneath. This avoids any canonicalization ambiguity over the // JSON representation. // 2. As the encoded entitlement JSON. Decode the same string from base64 // to recover the JSON bytes, then `JSON.parse` them. // // All six AuthForge SDKs (Node, Python, Go, Rust, C#, C++) verify exactly // this way. If you change either line to "match" the other, verification // will silently fail. const ok = verify( null, Buffer.from(response.payload, "utf8"), // [1] verify the base64 string as UTF-8 bytes publicKey, Buffer.from(response.signature, "base64") ); if (!ok) throw new Error("signature verification FAILED"); const payload = JSON.parse( Buffer.from(response.payload, "base64").toString("utf8") // [2] decode same string from base64 → JSON ); console.log("verified payload:", payload); ``` If `verify(...)` returns `true`, you've confirmed end-to-end that: 1. The `payload` you received was signed by the private key paired with the public key in this docs page. 2. Nobody between AuthForge and your machine modified a single byte of the JSON. 3. The entitlement fields inside `payload` (license expiry, variables, `unlimitedHwidSlots`, etc.) can be trusted as offline-verifiable claims. That's the property all six AuthForge SDKs rely on internally. You've just reproduced it in 20 lines of Node. You just verified a real Ed25519 signature against a real production endpoint. The exact same code, with your own `appId` / `appSecret` / public key, is your entire integration. Sign up for free, get starter credits, and ship - **no credit card, takes about a minute**. ## Limits The demo has a hard guardrail at every layer. Reviewers running the curl examples in parallel during a traffic spike will sometimes hit one - that's the design, not a bug. | Limit | Value | Error returned | |-------|-------|----------------| | Per-IP rate (validate) | 20 req/min | `rate_limited` | | Per-IP rate (heartbeat) | 20 req/min | `rate_limited` | | Per-IP rate (selfban) | 20 req/min | `rate_limited` | | Per-license rate (validate) | 60 req/min | `rate_limited` | | Per-license rate (selfban) | 30 req/min | `rate_limited` | | Per-app daily ceiling | 50,000 req/day | `demo_quota_exceeded` | `rate_limited` resets continuously (token-bucket refill); `demo_quota_exceeded` resets at 00:00 UTC. Both come back with HTTP 429. Your own apps have looser per-IP / per-license limits and no daily ceiling - they're billed per request instead. ## Ready for the real thing? [Create an account](https://app.authforge.cc/auth) - it's free, comes with starter credits, and gets you your own `appId`/`appSecret` plus unlimited license generation. The exact same code you wrote against the demo will work against your own credentials with a one-line swap.