- A static page has a Stripe Checkout button. The product is a $2.99 lifetime license.
- Stripe tells AuthForge Commerce about the payment. Commerce creates a perpetual license with 5 seats and emails the key to the buyer.
- The app runs as a 1-hour free trial until the buyer enters a key.
- The app calls
activate_offlineonce. AuthForge returns a lifetime.authforgefile bound to that machine. - From then on the app starts from the file with no network calls, forever.
Before you start
You need:- An AuthForge app (App ID, App Secret, public key).
- A Stripe account.
- One of the six SDKs in your app. The method is
activate_offlinein Python and Rust,activateOfflinein Node.js, andActivateOfflinein Go, C# and C++. See Self-serve activation.
- Perpetual licenses. Licenses with an expiry are refused with
offline_activation_requires_perpetual. Subscriptions and time-limited licenses are not supported. - Seat-limited licenses. Unlimited-seat (shared) keys are refused with
offline_activation_requires_seats. - Apps that opted in. It is off by default. Otherwise the call fails with
offline_activation_disabled.
What it costs
A license with 5 seats can cost at most 10 credits over its whole life: 2 credits per machine, once. Most buyers use one or two machines. At the smallest credit tier (0.002. See Managing credits.
Activations happen when buyers install, which can be months after the sale. Keep a credit balance or turn on auto-refill. With no credits, new activations fail with
no_credits. Machines that already have a file keep working.
1. Turn on self-serve activation
In the dashboard, open the app’s settings and turn on Self-serve offline activation. Or use the Developer API:write:apps scope. See Licenses API -> Self-serve offline activation.
2. Sell the license with Stripe
Create the product
In Stripe, create a product with a one-time price of $2.99. Copy the price ID (price_...).
Connect Commerce and map the price
Follow Commerce setup to connect Stripe. Subscribe the Stripe webhook tocheckout.session.completed, and to charge.refunded and charge.dispute.created so refunds and chargebacks revoke the license.
Then add a product mapping:
The HWID slots number is also the machine cap: the license can activate at most 5 distinct machines, ever. Pick a number that covers a buyer’s laptop, desktop and a couple of hardware changes. Raising it later for one buyer is easy; taking a file back is impossible.
Leave Email license key to buyer on (the default, in the app’s portal policy). Commerce emails the key as soon as the payment clears. The email is plain text, so the buyer can copy the key into your app.
Add the button to your page
A static page needs no server. Use a Stripe Payment Link or Stripe’s Buy Button for the same price:checkout.session.completed without line items, so Commerce needs your Stripe API key to look up the price. The Commerce setup covers this.
If you host your own Stripe webhook instead of using Commerce, create the license with expiresAt: null and maxHwidSlots: 5. See Custom Stripe webhooks.
3. Build the app
The app does three things on launch:- If a license file exists and
login_from_fileaccepts it, run. No network, no credits. - Otherwise, if the trial has time left, run in trial mode.
- Otherwise, ask for the key and call
activate_offlinewith the file path.
hwid_mismatch after new hardware), the app falls through to the key prompt. Activating on the new hardware counts as a new machine.
activate_offline is an online call, so the build needs the App Secret. This is different from air-gapped offline files, which ship without it. The secret cannot create, change or revoke licenses: activating still needs a valid, paid key. See Security best practices.Full example with the trial timer (Python)
AuthForge does not track trials. The trial below is a local timer: one hour from the first launch, stored in the app’s data folder. A user who deletes that file or winds back the clock gets more trial time. For a $2.99 app that is usually fine. If you prefer one hour of use, store the minutes used instead of the start time.activate(), so a buyer does not have to wait for the trial to end.
The activation pattern in every SDK
The core is the same in every language: try the file, otherwise prompt and activate. Onoffline_activation_limit_reached, tell the user to contact you.
activate_offline:
- Sends one request to
POST /auth/offline/activatewith the key, this machine’s HWID (the same onelogin()would send) and a fresh nonce. - Verifies the returned file locally (signature, app id, this machine’s HWID) before saving it. A file that fails is refused with
offline_file_rejectedand nothing is written. - Writes the file to the path atomically and creates parent folders. If the write fails, you get
file_write_failed; calling again returns the same file for free. - Leaves the client in an offline session, exactly as after
login_from_file: variables are available, no grace-period timer, no check-ins. - Never calls your failure callback and never exits the process. A failed activation leaves any existing session alone. You decide what to show.
login_from_file still reports a missing or rejected file through your failure callback as offline_login_failed in some SDKs. If you set a callback, let it return normally for that reason, or the app could exit before it reaches the key prompt.
4. Show clear errors
Every refusal is free: no credits are charged, no seat is bound and no machine is counted.
The full list is in the Error codes reference.
5. Support buyers who change machines
The license can activate at most 5 distinct machines, ever (its HWID slots). The rules:- Same machine again (reinstall, deleted file, lost response): free, and the identical file comes back. It never counts twice.
- New or changed hardware: counts as a new machine and costs 2 credits.
- HWID resets do not help. A reset in the portal or dashboard frees seats, but never machine slots, because the old machine’s file keeps working.
- Cap reached: the app gets
offline_activation_limit_reachedand tells the buyer to contact you.
- Release an offline machine on the license page in the dashboard, or with
DELETE /v1/licenses/{licenseKey}/offline-machines/{hwidHash}. This frees one machine slot and 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”).GET /v1/licenses/{licenseKey}/offline-machineslists the machines. - Raise the license’s HWID slots in the dashboard (up to 16).
- Mint a file by hand for a specific HWID (Offline license files). Hand-minted files do not count against the machine cap.
What this does not do
- No time-limited licenses. Subscriptions and fixed-term licenses cannot self-activate. Use online
login()and the grace period for those. - No unbound files at checkout. Every self-serve file is bound to one machine’s HWID.
- No remote kill. Refunds, chargebacks and revocation stop new activations only. Issued files keep working.
Checklist
- Self-serve offline activation turned on for the app
- Stripe price mapped as a one-time purchase with blank access length and several HWID slots
- Refund and dispute events subscribed, accepting that they cannot reach issued files
- Buyer email on, with your support contact in the portal policy
- App build has the App ID, App Secret and public key
- Launch order: file, then trial, then key prompt +
activate_offlinewith a path - Clear message for
offline_activation_limit_reachedthat points to support - Credit balance or auto-refill for activations that happen long after the sale
Next steps
- Offline license files: the feature in detail
- Commerce: the managed Stripe and Lemon Squeezy pipeline
- Offline licensing best practices: choosing between air-gapped files, self-serve activation and online activation