Quickstart

From creating a workspace to the first successful activation, in about five minutes.

This guide assumes you already have a workspace. Each step gives the dashboard action and the underlying API.

1. Create a workspace

Workspaces are provisioned manually for now. Email sales@unionuser.com with your product name and expected volume; you will receive a sign-in link.

2. Create a product

Dashboard → Products → New. Desktop apps usually use the desktop type with device activation enabled. Keep the license prefix stable once set.

3. Create a plan

Choose the license model (subscription, perpetual or trial), device limit, grace period and entitlements. A common desktop setup:

plan.json
{
  "name": "Pro Yearly",
  "license_model": "subscription",
  "max_activations": 3,
  "grace_days": 7,
  "token_ttl_days": 7,
  "entitlements": {
    "publish": true,
    "exports": 500
  }
}

4. Issue a license

Dashboard → Licenses → New, pick the product and plan, and enter the customer email. Send the key to the user, or let Stripe issue it automatically.

Note If the license email differs from the customer’s sign-in email, the portal will not show the license. The user can still enter the key manually in the app.

5. Activate in the client

activate.js
const BASE = 'https://keygate.unionuser.com';

async function activate(licenseKey, deviceId) {
  const res = await fetch(BASE + '/api/v1/license/activate', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      license_key: licenseKey,
      identifier: deviceId,
      identifier_type: 'device',
    }),
  });
  if (!res.ok) throw new Error('activation failed: ' + res.status);
  const body = await res.json();
  return body.data.token; // store it locally
}

6. Verify offline

Pin the public key in the client. The token is base64url(payload).base64url(sig), and the signature covers the payload’s base64 string.

verify.mjs
import { verify, createPublicKey } from 'node:crypto';

const RAW = Buffer.from('bf17435ee7b937b3592808941683c21d37c4e124d95d4e8fdd86fe775dd23b11', 'hex');
const SPKI = Buffer.concat([
  Buffer.from('302a300506032b6570032100', 'hex'),
  RAW,
]);
const publicKey = createPublicKey({ key: SPKI, format: 'der', type: 'spki' });

export function verifyToken(token) {
  const [payload, sig] = token.split('.');
  const ok = verify(
    null,
    Buffer.from(payload, 'ascii'),
    publicKey,
    Buffer.from(sig, 'base64url'),
  );
  if (!ok) throw new Error('INVALID_SIGNATURE');
  const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
  if (claims.exp * 1000 < Date.now()) throw new Error('TOKEN_EXPIRED');
  return claims;
}

Common pitfalls

  • Unstable device id: avoid MAC addresses and hostnames. Use MachineGuid, IOPlatformUUID or machine-id, then hash it.
  • Confusing exp with expiry: exp is the next check-in; vun is the license end.
  • Clock tampering: offline checks are affected; pair with a locally recorded monotonic time.
  • Reinstalls consume slots: the machine id can change; release the old device in the portal.