LeTissierDesign & Media
Cue 08IntegrateFor app developers

This deployment has no signing key configured, so the guide shows a placeholder instead of a real public key. Set LICENCE_PUBLIC_KEY and redeploy.

Licence integration guide

How to add LeTissier licence checking to an app. Written to be followed directly — by a person or by an agent working in one of the app repositories.

Service base URL: https://letissier.ie Signing public key (embed this): REPLACE_WITH_YOUR_PUBLIC_KEY_HEX

The public key is safe to ship inside a binary. It only verifies; it cannot mint a licence.

Which apps use this

AppRepositoryLanguageSDK to copy
Vizzlegofsalmon/vizzRusthttps://letissier.ie/integrate/sdk/rust/licence.rs
Lightlegofsalmon/lightRusthttps://letissier.ie/integrate/sdk/rust/licence.rs
Datamoshlegofsalmon/ffgl-datamoshC++17https://letissier.ie/integrate/sdk/cpp/licence.hpp + licence.cpp
Yeweelegofsalmon/facetrackPythonhttps://letissier.ie/integrate/sdk/python/letissier_licence.py
Crewboxlegofsalmon/crewboxTypeScripthttps://letissier.ie/integrate/sdk/node/licence.ts

Product ids used by the API: vizz, light, datamosh, yewee, crewbox.

A machine-readable index of everything on this page is at https://letissier.ie/integrate/manifest.json, and this guide in plain text is at https://letissier.ie/integrate/llms.txt.

How the model works

A licence is an Ed25519-signed claim blob: base64url(payload).base64url(signature). The app embeds the public key above and verifies it offline. There is no network call in the hot path, and no secret in the binary.

Two dates in the claims do different jobs, and confusing them is the main way an integration goes wrong.

maintUntil — what the customer is entitled to. Compare it against the build's own release date, compiled in. A build released at or before maintUntil runs forever. A build released after it is outside the customer's update window. This is what makes "one year of updates, yours to keep" work with no server: nothing expires, so a machine that never reconnects keeps running the version it is entitled to.

exp — the check-in deadline for the current lease. Passing it does not end the licence. The app checks in and receives a fresh token with a new exp. Default is 30 days; it is set per licence, so a tour licence may be 90 or 180 days without any code change.

Claims

{
  "v": 1,
  "key": "LT-V1ZZ-K7M2-9PQR-4XTC",
  "product": "vizz",
  "edition": "standard",
  "customer": "uuid",
  "name": "Buyer Name",
  "seats": 2,
  "maintUntil": 1791536000,
  "exp": 1762505600,
  "machine": "8b9dd6da2bcf47bdfe7ceb27c2a58680",
  "mode": "online",
  "iat": 1760000000,
  "jti": "uuid"
}

Reject any token whose v is not 1.

Statuses

check() returns exactly one of these. Handle all six.

StatusMeaningWhat the app should do
activeEverything is in orderRun normally
update_requiredLicence valid, but this build is newer than maintUntilRun an entitled build, or prompt to renew. Do not treat as piracy
check_in_requiredThe lease lapsedTry a heartbeat. Apply your own grace period before restricting
expiredA trial that ran outPrompt to buy
wrong_machineToken was issued for another machineRe-activate
invalidBad signature, malformed, or unknown versionTreat as unlicensed

Fail toward the customer, not against them. A rig mid-show should warn loudly and keep running rather than stop dead.

Machine fingerprint

Produce a stable per-machine string. The SDK hashes it before it leaves the machine — sha256(trimmed_fingerprint) hex, first 32 characters — and the service only ever stores that hash.

PlatformSource
macOSioreg -rd1 -c IOPlatformExpertDevice | awk -F'"' '/IOPlatformUUID/{print $4}'
Linux/etc/machine-id
WindowsHKLM\SOFTWARE\Microsoft\CryptographyMachineGuid

Do not use a MAC address: they change with docks, VPNs and USB adapters, which would burn a seat every time a customer plugs in a dongle.

API

All endpoints are POST, take and return JSON, and allow cross-origin use. Errors return { "ok": false, "reason": "...", "message": "..." }.

Activate a machine

POST https://letissier.ie/api/licence/activate
{ "key": "LT-V1ZZ-...", "machine": "<fingerprint>", "label": "FOH laptop" }
{
  "ok": true,
  "token": "eyJ2Ijox....abc",
  "product": "vizz",
  "edition": "standard",
  "seats": 2,
  "seatsUsed": 1,
  "checkInBy": "2026-09-18T00:00:00.000Z",
  "maintenanceUntil": "2027-08-21T00:00:00.000Z"
}

Activating the same machine twice renews its lease rather than consuming a second seat.

Check in

POST https://letissier.ie/api/licence/heartbeat
{ "key": "LT-V1ZZ-...", "machine": "<fingerprint>" }

Returns a fresh token and checkInBy. Call on launch and roughly daily. Persist the returned token — that is what resets the offline window.

Release a seat

POST https://letissier.ie/api/licence/deactivate
{ "key": "LT-V1ZZ-...", "machine": "<fingerprint>" }

Start a trial

POST https://letissier.ie/api/licence/trial
{ "product": "vizz", "email": "vj@example.com", "machine": "<fingerprint>", "name": "VJ" }

Issues and activates a 30-day trial in one call, returning key, token and expiresAt. One trial per machine per product.

Error reasons

ReasonHTTPMeaning
bad_request400Required field missing
malformed_key400Key failed its checksum — likely a typo
bad_email400Email did not parse
unknown_key404No such licence
not_activated404Heartbeat from a machine that never activated
revoked403Licence revoked, usually a refund
expired403Trial ended
no_seats409All seats in use; release one first
trial_already_used409This machine already had a trial
server_error500Retry with backoff

Treat a network failure as not a licensing failure: fall back to the cached token and its exp. Never block launch because the service was unreachable.

Integration steps

The same five steps in every language.

  1. Copy the SDK file(s) into the app.
  2. Replace REPLACE_WITH_YOUR_PUBLIC_KEY_HEX with the key at the top of this page.
  3. Compile in the build's release date as a Unix timestamp. Bake it in — if it is read from the filesystem, anyone can edit it.
  4. On launch: load the cached token, call check, act on the status.
  5. In the background: heartbeat when online and persist the returned token.

Store the token wherever the app already keeps user state. It is not a secret — it is signed, and useless on another machine.

Rust — Vizz, Light

Add to Cargo.toml:

[dependencies]
ed25519-dalek = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
base64 = "0.22"
hex = "0.4"
use letissier_licence::{check, Status};

const BUILD_DATE: i64 = 1_761_000_000; // set from CI at build time
const PUBLIC_KEY: &str = "REPLACE_WITH_YOUR_PUBLIC_KEY_HEX";

let now = std::time::SystemTime::now()
    .duration_since(std::time::UNIX_EPOCH)?
    .as_secs() as i64;

match check(&token, &fingerprint, BUILD_DATE, now, PUBLIC_KEY).status {
    Status::Active => start(),
    Status::UpdateRequired => warn_update_window_ended(),
    Status::CheckInRequired => try_heartbeat_then_grace(),
    Status::Expired => prompt_purchase(),
    Status::WrongMachine => prompt_reactivate(),
    Status::Invalid => prompt_licence_entry(),
}

C++ — Datamosh

Pick a crypto backend at compile time:

c++ -std=c++17 -DLICENCE_BACKEND_SODIUM licence.cpp -lsodium   # for shipped plugins
c++ -std=c++17 licence.cpp -lcrypto                             # OpenSSL

Use libsodium for the FFGL plugin: it links statically, so the plugin has no runtime dependency for a customer to install.

#include "licence.hpp"

constexpr std::int64_t kBuildDate = 1761000000;

const auto verdict = letissier::check(token, fingerprint, kBuildDate, now);

if (verdict.status == letissier::Status::Active) {
  enable_plugin();
} else {
  show_status(letissier::to_string(verdict.status));
}

A plugin cannot show a dialog mid-render. Check once at load, cache the verdict, and surface the state as a parameter or a watermark rather than blocking the render thread.

Python — Yewee

Requires cryptography (add to requirements.txt).

from letissier_licence import check, ACTIVE, CHECK_IN_REQUIRED

BUILD_DATE = 1761000000

verdict = check(token, fingerprint, BUILD_DATE)

if verdict.status == ACTIVE:
    start_tracking()
elif verdict.status == CHECK_IN_REQUIRED:
    try_heartbeat()
else:
    show_licence_panel(verdict.status)

TypeScript — Crewbox

No dependencies; Node's crypto has Ed25519 built in.

import { check, LicenceClient } from "./licence";

const BUILD_DATE = 1761000000;

const verdict = check({ token, fingerprint, buildDate: BUILD_DATE });

if (verdict.status !== "active") {
  const client = new LicenceClient({ fingerprint });
  const fresh = await client.heartbeat(key).catch(() => null);
  if (fresh) persist(fresh.token);
}

Crewbox runs offline by design, so its check must never gate startup on the network. Verify the cached token, and treat an unreachable service as normal.

Verifying an integration

The SDKs are proven to agree with each other and with the server signer. Test vectors are published at https://letissier.ie/integrate/vectors.json — signed with a test key included in that file, not the production key above.

A correct integration reproduces these, using the vectors' own publicKeyHex:

InputExpected
tokens.validverifies
tokens.tamperedfails — payload edited after signing
tokens.wrongKeyfails — signed by an untrusted key
tokens.malformedfails
fingerprint hashedequals machineHash
build date after maintUntilupdate_required
now after exp, standard licencecheck_in_required
now after exp, trialexpired

If your implementation disagrees on any row, it is wrong — the four shipped SDKs all agree on all of them.

Rules

  • Never send the raw fingerprint anywhere but this service, and never log it.
  • Never ship the private signing key. Only the public key belongs in an app.
  • Do not invent extra claim fields; the signature covers the exact payload.
  • Do not fail closed on a network error.
  • Do not use maintUntil as an expiry date. It is an entitlement boundary — treating it as an expiry would switch off software people have paid to keep.