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
| App | Repository | Language | SDK to copy |
|---|---|---|---|
| Vizz | legofsalmon/vizz | Rust | https://letissier.ie/integrate/sdk/rust/licence.rs |
| Light | legofsalmon/light | Rust | https://letissier.ie/integrate/sdk/rust/licence.rs |
| Datamosh | legofsalmon/ffgl-datamosh | C++17 | https://letissier.ie/integrate/sdk/cpp/licence.hpp + licence.cpp |
| Yewee | legofsalmon/facetrack | Python | https://letissier.ie/integrate/sdk/python/letissier_licence.py |
| Crewbox | legofsalmon/crewbox | TypeScript | https://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.
| Status | Meaning | What the app should do |
|---|---|---|
active | Everything is in order | Run normally |
update_required | Licence valid, but this build is newer than maintUntil | Run an entitled build, or prompt to renew. Do not treat as piracy |
check_in_required | The lease lapsed | Try a heartbeat. Apply your own grace period before restricting |
expired | A trial that ran out | Prompt to buy |
wrong_machine | Token was issued for another machine | Re-activate |
invalid | Bad signature, malformed, or unknown version | Treat 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.
| Platform | Source |
|---|---|
| macOS | ioreg -rd1 -c IOPlatformExpertDevice | awk -F'"' '/IOPlatformUUID/{print $4}' |
| Linux | /etc/machine-id |
| Windows | HKLM\SOFTWARE\Microsoft\Cryptography → MachineGuid |
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
| Reason | HTTP | Meaning |
|---|---|---|
bad_request | 400 | Required field missing |
malformed_key | 400 | Key failed its checksum — likely a typo |
bad_email | 400 | Email did not parse |
unknown_key | 404 | No such licence |
not_activated | 404 | Heartbeat from a machine that never activated |
revoked | 403 | Licence revoked, usually a refund |
expired | 403 | Trial ended |
no_seats | 409 | All seats in use; release one first |
trial_already_used | 409 | This machine already had a trial |
server_error | 500 | Retry 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.
- Copy the SDK file(s) into the app.
- Replace
REPLACE_WITH_YOUR_PUBLIC_KEY_HEXwith the key at the top of this page. - 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.
- On launch: load the cached token, call
check, act on the status. - 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:
| Input | Expected |
|---|---|
tokens.valid | verifies |
tokens.tampered | fails — payload edited after signing |
tokens.wrongKey | fails — signed by an untrusted key |
tokens.malformed | fails |
fingerprint hashed | equals machineHash |
build date after maintUntil | update_required |
now after exp, standard licence | check_in_required |
now after exp, trial | expired |
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
maintUntilas an expiry date. It is an entitlement boundary — treating it as an expiry would switch off software people have paid to keep.