Documentation

License keys and activation

One key, one payment, forever. This page is the whole mechanism: what a key looks like, the three endpoints an app calls, and how to check a token yourself without trusting my server.

There is no account, no login and no telemetry anywhere in this system. An app sends a key and a machine identifier; it gets back a signed token that it can verify offline for the next 30 days.

What a key looks like

RG-DMXG-7K2M-9QW4-3XYZ

RG is the prefix for everything I sell. DMXG is the four-character product code — DMX Gateway Server here — and the last twelve characters are random.

Those twelve characters come from Crockford base32: 0123456789ABCDEFGHJKMNPQRSTVWXYZ. The letters I, L, O and U never appear, so a key dictated over a headset cannot be mistaken for a digit. Keys are case-insensitive; spaces and stray dashes are normalised before matching.

You get your key on the success page after payment and, if mail is configured, by email. Keep it. Losing it means asking me to look up your order.

Activating a machine

The app picks a stable machine identifier — a hardware UUID, a MAC-derived hash, anything that survives a reboot but changes with the hardware — and posts it with the key. machineName is optional and only there so the list of activations is readable when you ask me which machines are using your key.

curl -sS https://www.riegergeri.com/api/license/activate \
  -H 'Content-Type: application/json' \
  -d '{
        "key": "RG-DMXG-7K2M-9QW4-3XYZ",
        "product": "DMXG",
        "machineId": "9f1c8e5a-...",
        "machineName": "FOH MacBook"
      }'

A successful activation returns a token:

{
  "ok": true,
  "token": "RGL1.eyJ2IjoxLCJrZXkiOiJSRy1ETVhHLTdLMk0t...",
  "seatsUsed": 1,
  "seats": 1,
  "exp": 1772323200
}

Anything else comes back with "ok": false, the same HTTP status as the status field, and a stable machine-readable error:

{ "ok": false, "status": 409, "error": "seat_limit" }
HTTPerrorWhat it means
400bad_requestA field is missing or the body is not JSON.
404not_foundNo such key. Check for a typo — keys never contain I, L, O or U.
403wrong_productThe key belongs to a different product than the one asking.
403revokedThe key was revoked (refund or chargeback). Nothing will activate it again.
409seat_limitEvery seat is in use. Deactivate a machine you no longer use.

Re-activating a machine that already holds a seat is free and always allowed: it refreshes the token and never consumes a second seat. That is exactly what an app should do when its token is about to expire.

Checking in, and moving to another machine

POST /api/license/validate answers whether this machine still holds a seat on a live key. It is optional — the token already proves that — but it is the cheapest way to notice a revocation before the token expires.

curl -sS https://www.riegergeri.com/api/license/validate \
  -H 'Content-Type: application/json' \
  -d '{"key": "RG-DMXG-7K2M-9QW4-3XYZ", "machineId": "9f1c8e5a-..."}'

# -> { "ok": true, "status": 200, "seatsUsed": 1, "seats": 1 }

POST /api/license/deactivate gives the seat back. Run it when you decommission a machine; the key is then free to activate somewhere else. Seats are movable by design — a license follows you, not your hardware.

curl -sS https://www.riegergeri.com/api/license/deactivate \
  -H 'Content-Type: application/json' \
  -d '{"key": "RG-DMXG-7K2M-9QW4-3XYZ", "machineId": "9f1c8e5a-..."}'

# -> { "ok": true, "status": 200 }

All three endpoints accept and return JSON, allow any origin (Access-Control-Allow-Origin: *) and answer OPTIONS preflights, so a desktop app or a browser build can call them directly.

The activation token

The token is three dot-separated parts: RGL1.<payload>.<signature>. The payload is base64url-encoded JSON; the signature is a raw ed25519 signature, base64url-encoded, over the payload part as text — sign and verify the base64 characters, not the decoded JSON.

{
  "v": 1,
  "key": "RG-DMXG-7K2M-9QW4-3XYZ",
  "product": "DMXG",
  "machineId": "9f1c8e5a-...",
  "seats": 1,
  "iat": 1769731200,
  "exp": 1772323200
}

iat and exp are unix seconds, and exp is always iat + 2592000 seconds — 30 days. An app should store the token, work entirely offline while it is valid, and call /activate again in the background as it nears expiry. If the machine is offline at that moment, nothing breaks until the token actually expires.

Verifying a token yourself

Verification needs nothing but the public key below, which is safe to compile into your app. In Node:

import { createPublicKey, verify } from "node:crypto";

const PUBLIC_PEM = `-----BEGIN PUBLIC KEY-----
...ship this string inside your app...
-----END PUBLIC KEY-----`;

export function readToken(token, machineId, now = Date.now() / 1000) {
  const [prefix, payloadB64, sigB64] = token.split(".");
  if (prefix !== "RGL1") return null;

  const publicKey = createPublicKey(PUBLIC_PEM);
  const ok = verify(null, Buffer.from(payloadB64), publicKey, Buffer.from(sigB64, "base64url"));
  if (!ok) return null;

  const payload = JSON.parse(Buffer.from(payloadB64, "base64url").toString());
  if (payload.v !== 1 || payload.exp <= now) return null;
  if (payload.machineId !== machineId) return null;
  return payload;
}

In Rust, with ed25519-dalek:

// Cargo.toml: ed25519-dalek = "2", base64 = "0.22", serde_json = "1"
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use ed25519_dalek::{Signature, Verifier, VerifyingKey};

let (prefix, rest) = token.split_once('.').ok_or("bad token")?;
let (payload_b64, sig_b64) = rest.split_once('.').ok_or("bad token")?;
if prefix != "RGL1" { return Err("bad token".into()); }

// VerifyingKey::from_bytes(&[u8; 32]) — the 32 raw bytes at the end of the SPKI DER,
// i.e. the last 32 bytes of the base64 body of the PEM above.
let key = VerifyingKey::from_bytes(&PUBLIC_KEY_BYTES)?;
let sig = Signature::from_slice(&URL_SAFE_NO_PAD.decode(sig_b64)?)?;
key.verify(payload_b64.as_bytes(), &sig)?;   // signature is over the base64 text, not the JSON

Always compare payload.machineId with the machine you are running on. Without that check a token copied from another machine would verify perfectly well.

The public key

GET /api/license/public-key returns { "alg": "Ed25519", "format": "RGL1", "pem": "..." }. This is the live key of this installation:

-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAmVay5dHXMz8bJaD0o9UZIgXFd9UOYk6YAwYMdzx1syM=
-----END PUBLIC KEY-----

Pin it in your app rather than fetching it at runtime: a key you download is only as trustworthy as the connection that delivered it. It changes only if I have to rotate it, and I would announce that before old tokens stop verifying.

Revocation, refunds and support

A revoked key stops activating immediately: activation and validation both answer 403 revoked. Tokens already issued keep verifying until they expire — up to 30 days — because they are signed statements about the past, not a live permission. That is the trade for working offline.

I revoke keys for refunds and chargebacks. I do not revoke a key because a machine changed, a company was renamed, or a hard drive died; write to me and I will move or re-issue it.

Questions, a lost key, or an app of yours that needs a different seat count: gergo@riegergeri.com.