EYMINT
DocumentationAPI Reference
BlogDashboard
EYMINT
SectionsDocumentationAPI Reference

Getting Started

  • Keymint Documentation
  • Introduction
  • Getting Started
  • License Keys
  • Changelog

SDK References

  • Software Development Kits (SDKs)
  • Node.js SDK
  • Go SDK
  • Python SDK
  • C# SDK

Integration Guides

  • Integration Guides
  • Customer Portal for Vendors
  • Roles & Permissions
  • Radar Alert Destinations
  • Payment Provider Integrations
  • Stripe Subscription Licensing
  • Email Delivery
  • API Key Management
  • Webhook Deliveries
  • Analytics and Retention
  • Dashboard Workflows
  • Electron Licensing
  • PyQt Licensing
  • Tauri Licensing
  • Next.js SaaS Licensing

Licensing Guides

  • Licensing Concepts
  • Node-Locking & Machine Identifiers
  • Floating Licenses
  • Offline Verification
  • License Radar
  • Public Key Management and Rotation
  • Feature Flags
  • Webhooks & Events

Templates

  • Starter Templates
  • Electron License Starter
  • PyQt License Starter
  • Tauri License Starter
  • Next.js SaaS License Starter

Security & Trust

  • Security & Trust

Support

  • Frequently Asked Questions
  • Customer Portal
  • Using the Customer Portal
  • Workspace Settings and Roles

Legal

  • Privacy Policy
  • Subprocessors
  • Data Processing Agreement
  • Security Overview
  • Terms of Service
  • Service Level Agreement (SLA)
  • GDPR Compliance
Go to Dashboard

Getting Started

  • Keymint Documentation
  • Introduction
  • Getting Started
  • License Keys
  • Changelog

SDK References

  • Software Development Kits (SDKs)
  • Node.js SDK
  • Go SDK
  • Python SDK
  • C# SDK

Integration Guides

  • Integration Guides
  • Customer Portal for Vendors
  • Roles & Permissions
  • Radar Alert Destinations
  • Payment Provider Integrations
  • Stripe Subscription Licensing
  • Email Delivery
  • API Key Management
  • Webhook Deliveries
  • Analytics and Retention
  • Dashboard Workflows
  • Electron Licensing
  • PyQt Licensing
  • Tauri Licensing
  • Next.js SaaS Licensing

Licensing Guides

  • Licensing Concepts
  • Node-Locking & Machine Identifiers
  • Floating Licenses
  • Offline Verification
  • License Radar
  • Public Key Management and Rotation
  • Feature Flags
  • Webhooks & Events

Templates

  • Starter Templates
  • Electron License Starter
  • PyQt License Starter
  • Tauri License Starter
  • Next.js SaaS License Starter

Security & Trust

  • Security & Trust

Support

  • Frequently Asked Questions
  • Customer Portal
  • Using the Customer Portal
  • Workspace Settings and Roles

Legal

  • Privacy Policy
  • Subprocessors
  • Data Processing Agreement
  • Security Overview
  • Terms of Service
  • Service Level Agreement (SLA)
  • GDPR Compliance

Electron Licensing

Add license activation, validation, and revocation to your Electron app in 15 minutes. Supports node-locked, floating, and offline licensing.

Problem

You built an Electron app and need to prevent unauthorized copying. You need license keys that bind to a specific machine, a user-friendly activation flow, and optional offline support for air-gapped deployments. Building this from scratch means dealing with hardware fingerprinting, secure local storage, activation counting, expiry checks, and a management dashboard.

Keymint provides the licensing backend, SDK, and dashboard. You add one dependency and a few lines of code.

Architecture Diagram

mermaid
Rendering diagram...

Create Product

Create a product from the Keymint Dashboard. This groups your Electron app's licenses together.

  1. Sign in to app.keymint.dev
  2. Navigate to Products
  3. Name it after your Electron app (e.g. "MyElectronApp")
  4. Copy the product ID

Create License Policy

When generating a license key from the dashboard, save your settings as a Template (in More options, open Save as a template). This lets you re-use the same configuration for future keys.

SettingRecommended ValuePurpose
licenseTypenode-lockedBinds license to one machine
maxActivations1Single device per license key
expiryNone (perpetual) or 365 daysAnnual subscription model
formatA8E2K-9F1BC-3D4GH-7J2KMUser-friendly segmented key format

Saved Templates store reusable defaults. When you generate keys, they can prefill the template's settings — no need to re-enter limits or format every time.

Issue License

Generate license keys from the Dashboard or programmatically:

  1. Navigate to Licenses
  2. Click Generate license key
  3. Select your product and, optionally, a saved template
  4. Set quantity, optional customer assignment
  5. Download or copy the generated keys

Save the raw license key when it's returned — it's encrypted at rest, and the API can't look a key up without already knowing it. You can still view or export keys in the dashboard.

Validate License

Install the Keymint SDK and electron-store for local license state. The SDK's KeyMint.getOrCreateInstallationId() provides a stable host ID, so no separate fingerprinting package is needed:

bash
npm install keymint electron-store

The SDK throws on every non-2xx response, with the HTTP status, the numeric code and the stable errorCode (for example license_invalid_or_forbidden) on the error. Treat 401, 402, 403 and 404 as "this license was rejected"; anything else (no network, 429, 5xx) is transient and must not lock the user out.

Create a license module in your Electron app:

typescript
import { KeyMint } from 'keymint';
import Store from 'electron-store';

// process.env is only populated in development unless you load it yourself
// (for example with dotenv) or inject the values at build time.
const client = new KeyMint(process.env.KEYMINT_CLIENT_API_KEY!);
const PRODUCT_ID = process.env.KEYMINT_PRODUCT_ID!;
const store = new Store<{ licenseKey?: string; activated?: boolean }>();

export function getHostId(): string {
  return KeyMint.getOrCreateInstallationId();
}

// 401 = invalid, expired, blocked or not allowed on this device;
// 402 = vendor subscription lapsed; 403 = activation limit reached;
// 404 = the product was deactivated or removed.
export function isLicenseRejected(error: unknown): boolean {
  const status = (error as { status?: number } | null)?.status;
  return status === 401 || status === 402 || status === 403 || status === 404;
}

export function getStoredLicenseKey(): string | undefined {
  return store.get('licenseKey');
}

export function isActivated(): boolean {
  return store.get('activated') === true;
}

export async function activateLicense(licenseKey: string): Promise<{
  success: boolean;
  message: string;
}> {
  try {
    const result = await client.activateKey({
      productId: PRODUCT_ID,
      licenseKey,
      hostId: getHostId(),
    });

    store.set('licenseKey', licenseKey);
    store.set('activated', true);
    return { success: true, message: result.message };
  } catch (error: any) {
    return {
      success: false,
      message: isLicenseRejected(error)
        ? error.message
        : 'Could not reach the licensing server. Try again later.',
    };
  }
}

export async function deactivateLicense(): Promise<boolean> {
  const licenseKey = getStoredLicenseKey();
  if (!licenseKey) return false;

  try {
    await client.deactivateKey({
      productId: PRODUCT_ID,
      licenseKey,
      hostId: getHostId(),
    });
    store.delete('licenseKey');
    store.delete('activated');
    return true;
  } catch {
    return false;
  }
}

In your Electron main process, gate app launch behind license validation. Re-activating on the same device doesn't use another seat, so it doubles as a validity check:

typescript
import { app, BrowserWindow } from 'electron';
import { activateLicense, getStoredLicenseKey } from './license';

app.whenReady().then(async () => {
  const storedKey = getStoredLicenseKey();
  if (storedKey) {
    const result = await activateLicense(storedKey);
    if (result.success) {
      createWindow();
      return;
    }
  }

  // Show activation window — user enters key, calls activateLicense()
  createActivationWindow();
});

function createWindow() {
  const win = new BrowserWindow({ width: 1200, height: 800 });
  win.loadFile('index.html');
}

function createActivationWindow() {
  const win = new BrowserWindow({ width: 480, height: 320 });
  win.loadFile('activation.html');
}

Don't deactivate on quit. before-quit runs every time the app closes, so it would free the seat and forget the key on every exit. Offer deactivation as an explicit action instead, for example "Move license to another computer" in settings.

To allow offline use, open the app when isActivated() is true even if the launch check couldn't reach the server, and re-check later.

Handle Activations

Build a simple activation UI using Electron's IPC:

typescript
import { contextBridge, ipcRenderer } from 'electron';

contextBridge.exposeInMainWorld('licenseAPI', {
  activate: (key: string) => ipcRenderer.invoke('license:activate', key),
  getStatus: () => ipcRenderer.invoke('license:status'),
});
typescript
import { ipcMain } from 'electron';
import { activateLicense, isActivated, getStoredLicenseKey } from './license';

ipcMain.handle('license:activate', async (_event, licenseKey: string) => {
  return activateLicense(licenseKey);
});

ipcMain.handle('license:status', () => {
  return {
    activated: isActivated(),
    licenseKey: getStoredLicenseKey() ? '••••••••' : null,
  };
});
html
<!DOCTYPE html>
<html>
<body>
  <h2>Activate License</h2>
  <input id="key" type="text" placeholder="A8E2K-9F1BC-3D4GH-7J2KM" />
  <button id="activate">Activate</button>
  <p id="status"></p>

  <script>
    const status = document.getElementById('status');
    document.getElementById('activate').addEventListener('click', async () => {
      status.textContent = 'Activating...';
      const key = document.getElementById('key').value.trim();
      const result = await window.licenseAPI.activate(key);
      status.textContent = result.success
        ? 'License valid!'
        : `Failed: ${result.message}`;
    });
  </script>
</body>
</html>

Handle Revocations

You can block a license from the Dashboard or programmatically:

typescript
// From your backend (admin API key required)
await admin.blockKey({
  productId: "YOUR_PRODUCT_ID",
  licenseKey: "A8E2K-9F1BC-3D4GH-7J2KM",
});

When a license is blocked, the next activateKey call throws an error with status: 401 and errorCode: "license_invalid_or_forbidden". Expired, unknown and blocked keys deliberately return the same response. Branch on status or errorCode, not on the message text, which may be reworded.

Re-check periodically, and only clear the license when the server rejects it:

typescript
export async function checkLicenseHealth(): Promise<'valid' | 'rejected' | 'unknown'> {
  const licenseKey = getStoredLicenseKey();
  if (!licenseKey) return 'rejected';

  try {
    await client.activateKey({
      productId: PRODUCT_ID,
      licenseKey,
      hostId: getHostId(),
    });
    return 'valid';
  } catch (error) {
    return isLicenseRejected(error) ? 'rejected' : 'unknown';
  }
}

// Run periodically (e.g. once per day)
setInterval(async () => {
  if ((await checkLicenseHealth()) === 'rejected') {
    store.delete('activated');
    // Notify renderer to show re-activation dialog
  }
}, 24 * 60 * 60 * 1000);

Production Considerations

API Key Security

  • Bundle the client-scoped API key in your Electron app. This is the only scope safe for distribution — it can only activate and deactivate devices and check out, heartbeat and check in floating seats.
  • Never bundle admin or read-only API keys. These can read all customer data and manage all licenses.
  • Rotate API keys from the Dashboard every 90 days.

Host ID Stability

Choose a host ID strategy that survives OS reinstalls and hardware changes:

MethodStabilityExample
KeyMint.getOrCreateInstallationId() (default)Random installation ID mixed with a hardware anchor, hashed and persisted to ~/.keymint/installation-id. Stable across restarts and app updates; a new ID (and a new seat) after an OS reinstall or a wiped home directoryKeyMint.getOrCreateInstallationId()
node-machine-idSurvives OS reinstall on most systemsmachineIdSync()
MAC addressChanges with network adaptersNot recommended

If hostId changes unexpectedly, the activation counts as a new device and consumes another seat. Use a stable, predictable host ID.

Offline Support

If your Electron app needs to work offline, use Keymint's offline licensing:

typescript
// From your backend (admin key). Signing also activates hostId on the
// license, so it uses a seat like an online activation.
const offlineLicense = await admin.signKey({
  productId: "YOUR_PRODUCT_ID",
  licenseKey: "A8E2K-9F1BC-3D4GH-7J2KM",
  hostId: "HOST_ID_FROM_THE_MACHINE", // getHostId() on the air-gapped machine
  ttl: 60 * 60 * 24 * 30, // 30 days
});

// offlineLicense.file is the .lic file contents (JSON with the signed JWT)
// Save this file and distribute it to the air-gapped machine

Then verify offline in Electron using jose:

typescript
import { jwtVerify, decodeProtectedHeader, importSPKI } from 'jose';
import * as fs from 'fs';

const TRUSTED_KEYS = new Map([
  ['AbC123XyZ', '-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAr7Z8s2Z...\n-----END PUBLIC KEY-----']
]);

export async function verifyOfflineLicense(filePath: string, productId: string, machineCode: string) {
  const fileContent = JSON.parse(fs.readFileSync(filePath, 'utf8'));
  const { signedKey } = fileContent;

  const { kid } = decodeProtectedHeader(signedKey);
  if (!kid) throw new Error('Invalid token: missing kid');

  const pem = TRUSTED_KEYS.get(kid);
  if (!pem) throw new Error('Untrusted key ID');

  const publicKey = await importSPKI(pem, 'EdDSA');
  const { payload } = await jwtVerify(signedKey, publicKey, {
    algorithms: ['EdDSA'],
  });

  // One signing key covers every product in your workspace, so always check
  // the product as well as the machine.
  if (payload.type !== 'offline') throw new Error('Not an offline license');
  if (payload.productId !== productId) throw new Error('Product mismatch');
  if (payload.machineCode !== machineCode) throw new Error('Hardware mismatch');
  return payload;
}

Offline licensing is available on all plans. See Offline Verification for the full workflow.

Code Signing

Always code-sign your Electron app. Unsigned apps on macOS trigger Gatekeeper warnings that discourage users from entering license keys.

Auto-Updates

If you use electron-updater, store the license key and activation state outside the app bundle (e.g. in app.getPath('userData')) so updates don't wipe the license.

Complete Source Code

Full Electron license module
typescript
// src/main/license.ts
import { KeyMint } from 'keymint';
import Store from 'electron-store';

interface LicenseStore {
  licenseKey?: string;
  activated?: boolean;
  lastCheck?: number;
}

const client = new KeyMint(process.env.KEYMINT_CLIENT_API_KEY!);
const PRODUCT_ID = process.env.KEYMINT_PRODUCT_ID!;
const store = new Store<LicenseStore>();
const CHECK_INTERVAL = 24 * 60 * 60 * 1000; // 24 hours

export type LicenseHealth = 'valid' | 'rejected' | 'unknown';

export function getHostId(): string {
  return KeyMint.getOrCreateInstallationId();
}

// The SDK throws on every non-2xx response. 401/402/403/404 mean the license
// was rejected; no response, 429 and 5xx are transient.
export function isLicenseRejected(error: unknown): boolean {
  const status = (error as { status?: number } | null)?.status;
  return status === 401 || status === 402 || status === 403 || status === 404;
}

export function getStoredLicenseKey(): string | undefined {
  return store.get('licenseKey');
}

export function isActivated(): boolean {
  return store.get('activated') === true;
}

export async function activateLicense(licenseKey: string): Promise<{
  success: boolean;
  message: string;
}> {
  try {
    const result = await client.activateKey({
      productId: PRODUCT_ID,
      licenseKey,
      hostId: getHostId(),
    });

    store.set('licenseKey', licenseKey);
    store.set('activated', true);
    store.set('lastCheck', Date.now());
    return { success: true, message: result.message };
  } catch (error: any) {
    return {
      success: false,
      message: isLicenseRejected(error)
        ? error.message
        : 'Could not reach the licensing server. Try again later.',
    };
  }
}

// Call from an explicit "Deactivate" action, never from before-quit.
export async function deactivateLicense(): Promise<boolean> {
  const licenseKey = getStoredLicenseKey();
  if (!licenseKey) return false;

  try {
    await client.deactivateKey({
      productId: PRODUCT_ID,
      licenseKey,
      hostId: getHostId(),
    });
    store.delete('licenseKey');
    store.delete('activated');
    store.delete('lastCheck');
    return true;
  } catch {
    return false;
  }
}

export async function checkLicenseHealth(): Promise<LicenseHealth> {
  const licenseKey = getStoredLicenseKey();
  if (!licenseKey) return 'rejected';

  // Skip if checked recently
  const lastCheck = store.get('lastCheck') ?? 0;
  if (Date.now() - lastCheck < CHECK_INTERVAL) {
    return isActivated() ? 'valid' : 'rejected';
  }

  try {
    await client.activateKey({
      productId: PRODUCT_ID,
      licenseKey,
      hostId: getHostId(),
    });
    store.set('activated', true);
    store.set('lastCheck', Date.now());
    return 'valid';
  } catch (error) {
    if (!isLicenseRejected(error)) return 'unknown'; // offline: keep cached state
    store.set('activated', false);
    return 'rejected';
  }
}

export function startHealthCheck(onRejected: () => void, interval = CHECK_INTERVAL) {
  setInterval(async () => {
    if ((await checkLicenseHealth()) === 'rejected') onRejected();
  }, interval);
}

On this page

  • Problem
  • Architecture Diagram
  • Create Product
  • Create License Policy
  • Issue License
  • Validate License
  • Handle Activations
  • Handle Revocations
  • Production Considerations
  • API Key Security
  • Host ID Stability
  • Offline Support
  • Code Signing
  • Auto-Updates
  • Complete Source Code