Skip to main content
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. 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).
This is a separate mode from the default. Most apps should keep using online activation + the grace period. Reach for offline files only when a machine genuinely cannot phone home (factory floors, classified networks, ships, embedded appliances), or when you sell a one-time-purchase app that should run offline forever after 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

  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. 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 silent hwid_mismatch. You can still paste the getter (getHwid() / get_hwid() / HWID() / hwid() / GetHwid()). Any machine requires an explicit acknowledgement.
    Collect the HWID from a 2.0+ SDK build of the same app (same app id) that will call loginFromFile. HWIDs include the app id, so another app on the same machine reports a different value, and pre-2.0 SDKs used a different fingerprint. A file bound to the wrong one fails with hwid_mismatch. On 2.0+ every SDK reports the same HWID for the same app and machine.
  4. Click Mint & download. The browser downloads <licenseKey>.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

Returns 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 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).

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.
A refund or chargeback revokes the license online but cannot reach files already issued. For a lifetime file, that means the buyer keeps the app. Revoking stops new activations only.

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 with offline_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.
Every refusal is free. The seat bind, the machine count and the 2-credit debit commit together or not at all. Rate limits: 10 requests per minute per IP and 3 per minute per license key.

Machine cap

A license can activate at most maxHwidSlots 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.
To let a buyer move to new hardware once the cap is reached, you can:
  • 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 as license.offline_machine_released. GET /v1/licenses/{licenseKey}/offline-machines lists the machines with used and limit.
  • 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 example hwid_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 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.

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