.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.
There are two ways to get a file onto a machine: you mint it and deliver it, or the app gets its own lifetime file with one online call (self-serve activation).
Offline file vs grace period
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
- Open the license (Applications -> app -> Licenses -> license page) and click Mint .authforge file.
- 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.
-
Pick a machine binding. Bound to specific HWIDs (recommended) prefills the HWIDs already bound to the license. Prefer an activation request (
.authforge-request) over a pasted HWID: the dashboard checksums the file so email damage becomes a clear error instead of a silenthwid_mismatch. You can still paste the getter (getHwid()/get_hwid()/HWID()/hwid()/GetHwid()). Any machine requires an explicit acknowledgement. -
Click Mint & download. The browser downloads
<licenseKey>.authforgeand 1 credit is charged. The file body is never stored server-side, only its SHA-256, so download it now.
Developer API
201 with { file, fileName, meta }. file is the armored text; write it to fileName. Scope write:licenses. See Licenses API -> Offline license files for the full reference and error codes.
Mint rules
- License must be
activeand not expired; the app must not be paused. expiresAtmust 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"needsallowUnbound: 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 withno_creditsbefore anything is signed. - Every mint is audited (actor, time, license, expiry, HWID policy, file id and hash).
Self-serve activation
Self-serve activation lets the app get its own file. The buyer enters their license key once, the app makes one online call, and AuthForge returns a lifetime file bound to that machine’s HWID. After that the app runs from the file with no network, exactly as with a file you minted. It suits one-time-purchase apps that should keep working offline forever. For a complete walkthrough (static page, Stripe, trial, activation), see Sell offline apps.Requirements
- The app opts in. Self-serve activation is off by default. Turn on Self-serve offline activation in the app’s General settings in the dashboard, or send
PUT /v1/apps/{appId}with{"offlineSelfServe": {"enabled": true}}(reference). Otherwise the call fails withoffline_activation_disabled. - The license is perpetual (no expiry). Otherwise
offline_activation_requires_perpetual. - The license is seat-limited. Unlimited-seat (shared) keys are refused with
offline_activation_requires_seats: a shared key binds no HWIDs, so nothing would limit the number of lifetime files. - The usual validate checks pass: the key exists for this app and is active, the app is not paused, the HWID and IP pass your access lists, there is a free seat, and you have credits.
Machine cap
A license can activate at mostmaxHwidSlots distinct machines, ever.
- Activation binds a seat, like
login(), and records the machine. - Same machine again (reinstall, deleted file, lost response): the identical file comes back for free. It never counts twice.
- HWID resets free seats but never machine slots. A reset in the portal or dashboard cannot take back the old machine’s file, so it does not give the buyer another activation.
- Cap reached: a new machine gets
offline_activation_limit_reached. The app should tell the buyer to contact you.
- Release the machine in the Offline machines section of the license page, or call
DELETE /v1/licenses/{licenseKey}/offline-machines/{hwidHash}. This frees one machine slot and unbinds that machine’s seat. The released machine’s file keeps working, so release only when you trust the request (for example “my old PC died”). Releases are audited aslicense.offline_machine_released.GET /v1/licenses/{licenseKey}/offline-machineslists the machines withusedandlimit. - Raise the license’s HWID slots in the dashboard (up to 16).
- Mint a file by hand as described above. Operator mints never count against the machine cap.
In the app
The pattern is the same in every SDK: on launch, try the file. If there is no file, or it fails (for examplehwid_mismatch after a hardware change), prompt for the key and activate with a path.
The SDK sends the same HWID
login() would send, verifies the returned file (signature, app id, this machine’s HWID) before saving it, writes it atomically, and leaves the client in an offline session exactly as after loginFromFile. A file that fails local verification is refused with offline_file_rejected and nothing is written. If the verified file cannot be written to the path, the code is file_write_failed, and calling again returns the same file for free. The method never calls your failure callback, never exits the process, and leaves any existing session alone on failure. Code for all six SDKs and the error messages to show: Sell offline apps.
Activation fires the license.hwid_bound webhook when it binds a new seat, and license.offline_file_minted with source: "self-serve" for each new file. The license page’s mint history lists self-serve files next to the ones you minted.
Not supported
- Time-limited licenses. There is no self-serve file that expires with the license, and no re-activation on renewal.
- Unbound files emailed at checkout.
- Remote kill of issued files.
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 soisAuthenticated(),getLicenseVariables()andgetAppVariables()work exactly as after an onlinelogin(). It never starts the grace-period timer or online check-ins.logout()clears it.
Rejection codes
Checks run in this fixed order in every SDK; the first failure wins.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:- 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 ornull),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/validateresponses, 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
vother than 1 withunsupported_version. Future versions will be additive and announced with an SDK release.
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 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.
- One-time-purchase apps that should run offline forever: self-serve activation. One online call per machine, then a lifetime file. Refunds cannot reach issued files.