Skip to content

Desktop Client Licensing Integration Guide

1. Big Picture

  1. User pays on the website (Stripe/PayPal). Server issues a license (plan Pro, idempotent per order) with key GRAV-XXXXX-XXXXX-XXXXX-XXXXX. Plaintext key is shown once on the Download page; server stores only its SHA-256 hash and can never re-display it.
  2. User pastes the key into the desktop app → app calls /api/license/verify at every startup. First verify registers the machine as an activation (max 3 machines per license).
  3. App also logs in with website credentials → gets a 30-day Bearer session token.
  4. App sends periodic heartbeats with that token; an expired/revoked session or license must lock Pro features.

Two Credentials — Never Confuse Them

CredentialSourcePurposeLifetime
License key GRAV-...Download page (one-time)Machine activation + verify365 days
Session tokenPOST /api/auth/loginBearer header for heartbeat/logout30 days

2. Conventions

  • Dev base URL: http://localhost:5243 (https profile: https://localhost:7130). Production: https://iamgravity.eu
  • All endpoints: POST, Content-Type: application/json, JSON responses (camelCase)
  • No cookies/CSRF — licensing APIs are anonymous; auth is body fields or Authorization: Bearer <token>
  • All timestamps are UTC ISO-8601; parse as UTC, display local
  • License denials return HTTP 200 with valid:false — always branch on the JSON body, never on the status code

3. Endpoints

3.1 POST /api/license/verify — Startup Activation Check

Request:

{
  "licenseKey": "GRAV-XXXXX-XXXXX-XXXXX-XXXXX",
  "machineId": "<stable id>",
  "machineName": "DESKTOP-ABC123",
  "appVersion": "1.0.0"
}

Success (200):

{
  "valid": true,
  "reason": "ok",
  "license": {
    "orderNumber": "PYPL-8XK4321",
    "plan": "Pro",
    "expiresAt": "2027-10-07T09:26:21Z",
    "activations": 1,
    "maxActivations": 3
  }
}

Failure (also HTTP 200):

{
  "valid": false,
  "reason": "license_expired",
  "message": "Your license has expired. Renew at iamgravity.eu/#pricing."
}

3.2 POST /api/auth/login — Account Login, Returns Session Token

Request:

{
  "email": "[email protected]",
  "password": "...",
  "machineId": "<stable id>",
  "machineName": "DESKTOP-ABC123",
  "appVersion": "1.0.0"
}

3.3 POST /api/license/heartbeat — Session Keep-Alive

Header: Authorization: Bearer <sessionToken>

{ "machineId": "<stable id>" }

3.4 POST /api/auth/logout — Revoke Session Token

Header: Authorization: Bearer <sessionToken>, empty body → 200 { "success": true }

4. License Key Format & Lifecycle

Format: GRAV- + 4 groups of 5 chars (--separated, 23 chars total). Alphabet: ABCDEFGHJKLMNPQRSTUVWXYZ23456789 — no I, O, 0, 1 (anti-confusion).

Client-side validation regex:

^GRAV-[A-HJ-NP-Z2-9]{5}-[A-HJ-NP-Z2-9]{5}-[A-HJ-NP-Z2-9]{5}-[A-HJ-NP-Z2-9]{5}$

Lifecycle: Issued on payment success, plan "Pro", valid 365 days. Status Active(0) → Expired(2) (auto-flipped) or Revoked(1) (manual). MaxActivations = 3 machines.

5. Machine ID Requirements

machineId is a client-generated, opaque, stable string. Rules:

  1. Deterministic — same machine, same ID, across restarts/updates/reinstalls
  2. Unique per machine — two PCs must never collide
  3. Recommended recipe (Windows): hex SHA-256 of the registry MachineGuid (HKLM\\SOFTWARE\\Microsoft\\Cryptography\\MachineGuid)
  4. Do NOT use values that change with hardware dongles, VPNs, MAC randomization, or current username
  5. Always send machineName (Environment.MachineName) and appVersion

6. Reason / Error Codes → Required Client Behavior

CodeWhereMeaningClient Action
ok (valid:true)verifyLicensedUnlock Pro features, cache license info
license_not_foundverifyKey unknownRe-prompt; link to Download page
license_revokedverifyLicense revoked by staffLock; show [email protected]
license_expiredverifyPast ExpiresAtLock; deep-link /pricing to renew
activation_limit_reachedverifyMore than 3 machinesLock; deep-link account page to revoke a device
machine_revokedverifyThis device was revokedLock; user must re-activate from account page
session_invalid_or_expired (401)heartbeatSession dead or machine not activatedSilent re-login; if still failing → re-verify, then lock
no_active_license (402)loginNo active license on accountLock; deep-link /pricing

7. Recommended Startup Flow

App start
 ├─ No stored license key → Activation screen (paste key) ─────────────────────────────────────────────────────────────────────────────────────────────│
 ├─ Stored key:                                             │
 │   1. POST /api/license/verify {key, machineId, ...}  ├──────────────────────────────────────────────────────│
 │        valid:true   → cache license, continue
 │        valid:false   → show message, LOCK Pro features
 │        network error → offline grace
 │   2. POST /api/auth/login {email, password, machineId, ...}
 │        200            → store sessionToken + expiresAt securely
 │        402/401/429    → handle per error codes
 │   3. Start heartbeat timer (every 30–60 min)
 └─ Runtime:
        heartbeat 200   → refresh cached license info, stay unlocked
        heartbeat 401   → silent re-login once; 402 → LOCK
        user sign-out / app exit → POST /api/auth/logout (fire-and-forget)

8. Offline Grace Policy

If the last successful verify/heartbeat is within N days (recommend 7; max 30 = session lifetime), allow Pro features offline and retry in background with backoff (1h → 6h → 24h). Lock when grace window exceeded or explicit valid:false / 402 arrives.

9. Storage & Security Checklist

  • sessionToken → Windows Credential Manager or DPAPI (ProtectedData). Never plaintext on disk, never in logs/telemetry.
  • Password → Credential Manager only with explicit "remember me"; otherwise memory only.
  • License key → local settings file is acceptable (low sensitivity); display masked in UI.
  • machineId → compute deterministically or cache after first generation; reuse forever.
  • Gate ALL Pro features behind a single IsLicensed service — no scattered booleans.
  • Never embed a default/shared license key in the binary.
  • Log reason codes (not keys/tokens) for support diagnostics.

10. Reference C# Client

public sealed class LicenseApiClient
{
    private readonly HttpClient _http;
    public string? SessionToken { get; set; }

    public LicenseApiClient(string baseUrl)
    {
        _http = new HttpClient { BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/") };
        _http.DefaultRequestHeaders.Accept.Add(new("application/json"));
    }

    public static string GetMachineId()
    {
        using var key = Microsoft.Win32.Registry
            .OpenBaseKey(Microsoft.Win32.RegistryHive.LocalMachine,
                         Microsoft.Win32.RegistryView.Registry64)
            .OpenSubKey("SOFTWARE\\\\Microsoft\\\\Cryptography");
        var guid = (string?)key?.GetValue("MachineGuid")
                   ?? throw new InvalidOperationException("MachineGuid unavailable");
        return Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(
            System.Text.Encoding.UTF8.GetBytes(guid))).ToLowerInvariant();
    }

    public async Task VerifyAsync(string licenseKey) { ... }
    public async Task LoginAsync(string email, string password) { ... }
    public async Task HeartbeatAsync() { ... }
    public async Task LogoutAsync() { ... }
}

11. Integration Checklist

  • [ ] Settings UI to paste the license key (validate shape locally with regex, then send as-is)
  • [ ] Deterministic machineId + machineName + appVersion on every call
  • [ ] Startup sequence: verify → login → heartbeat timer (30–60 min)
  • [ ] Every reason code mapped to a user message + app state
  • [ ] Offline grace window with authoritative-lock overrides
  • [ ] Silent re-login on 401 session_invalid_or_expired
  • [ ] Secure token/password storage; masked key display
  • [ ] Logout on sign-out/app exit
  • [ ] Single IsLicensed gate for ALL Pro features
  • [ ] Test matrix: wrong key, expired license, 4th machine, revoked machine, revoked license, session expiry, offline start, rate-limited login