One REST API call. Any language. Any platform.
Download this file from your dashboard and place it next to your executable.
[Licentio]
ProgramID=your-program-uuid
ClientID=your-client-uuid
APIKey=your-api-key
Your app also writes a licentio.cache file next to the executable at runtime — do not ship it, it is created on the first successful validation.
Set an Offline grace (days) on the license in your dashboard. On every successful validation the API returns a signed window the client caches, so a user can keep working when their internet — or your server — is temporarily unreachable.
Each client below follows the same algorithm when a request can't reach the server:
IssuedAt, ValidUntil (Unix epoch seconds, UTC) and Token.HMAC-SHA256(APIKey, "ClientID|IssuedAt|ValidUntil") and check it equals the cached Token — proves the window wasn't edited.now < IssuedAt (the clock was set back) or now > ValidUntil (grace expired). Otherwise, run.The simplest way to test the API from any terminal.
curl -X POST https://licentio.dev/validate \
-H "Content-Type: application/json" \
-d '{
"program_id": "your-program-uuid",
"client_id": "your-client-uuid",
"api_key": "your-api-key"
}'
When the license has an offline grace, a valid response also carries the signed window the client caches (issued_at/valid_until are Unix epoch seconds).
{
"valid": true,
"message": "License valid",
"license_type": "EXPIRY_DATE",
"days_remaining": 17,
"warning": true,
"warning_message": "License expires in 17 days",
"offline_days": 7,
"issued_at": 1785484800,
"valid_until": 1786089600,
"offline_token": "3cce096c1eb9...b90ea1",
"offline_message": "Could not reach the license server."
}
Uses NetWebClient (NetTalk) + jFiles. StringTheory only does plain hashes, so the HMAC uses CapeSoft Cryptonite — MakeHMAC(message, secret, hashType, hexEncode), which writes the hex result back into the message object. Add both global extensions.
INCLUDE('StringTheory.inc'),ONCE
INCLUDE('Cryptonite.inc'),ONCE
! --- Group to receive the JSON response ---
element GROUP,NAME('element')
valid BYTE,NAME('valid | boolean')
message STRING(255),NAME('message')
warning_message STRING(255),NAME('warning_message')
offline_token STRING(64),NAME('offline_token')
offline_message STRING(255),NAME('offline_message')
issued_at STRING(20),NAME('issued_at') ! epoch seconds (UTC), kept as text
valid_until STRING(20),NAME('valid_until') ! epoch seconds (UTC), kept as text
END
! --- Windows API: current UTC time ---
SYSTEMTIME GROUP,TYPE
wYear USHORT
wMonth USHORT
wDayOfWeek USHORT
wDay USHORT
wHour USHORT
wMinute USHORT
wSecond USHORT
wMilliseconds USHORT
END
MODULE('api')
GetSystemTime(*SYSTEMTIME),PASCAL,RAW,NAME('GetSystemTime')
END
crypto Cryptonite
stHMAC StringTheory
! ============ On a successful 200 response ============
! (after jFiles has loaded the body into `element`)
IF element.valid = TRUE
! Cache the signed offline window for future offline runs
IF element.offline_token <> ''
PUTINI('Cache','IssuedAt', CLIP(element.issued_at), 'licentio.cache')
PUTINI('Cache','ValidUntil',CLIP(element.valid_until), 'licentio.cache')
PUTINI('Cache','Token', CLIP(element.offline_token), 'licentio.cache')
PUTINI('Cache','Msg', CLIP(element.offline_message),'licentio.cache')
END
END
! ============ CheckOffline — when the server is unreachable ============
CheckOffline PROCEDURE(STRING pClientID, STRING pAPIKey)
sToken STRING(64)
sMyToken STRING(64)
sCanonical STRING(160)
nIssued LONG
nValid LONG
nNow LONG
ut LIKE(SYSTEMTIME)
CODE
IF NOT EXISTS('licentio.cache')
Message('No internet connection and no cached license.')
RETURN FALSE
END
nIssued = GETINI('Cache','IssuedAt','0','licentio.cache')
nValid = GETINI('Cache','ValidUntil','0','licentio.cache')
sToken = GETINI('Cache','Token','','licentio.cache')
! 1) Rebuild the HMAC-SHA256 and compare (proves the window wasn't edited)
sCanonical = CLIP(pClientID) & '|' & nIssued & '|' & nValid
stHMAC.SetValue(sCanonical)
! MakeHMAC(message, secret, hashType, hexEncode) -> the message object holds the result
IF crypto.MakeHMAC(stHMAC, CLIP(pAPIKey), cs:CALG_SHA_256, 1) <> Crypto:OK
Message('Could not verify the cached license.')
RETURN FALSE
END
stHMAC.Lower() ! server sends lowercase hex
sMyToken = stHMAC.GetValue()
IF sMyToken <> CLIP(sToken)
Message('Cached license is corrupt.')
RETURN FALSE
END
! 2) Current UTC time as Unix epoch seconds
GetSystemTime(ut)
nNow = (DATE(ut.wMonth, ut.wDay, ut.wYear) - DATE(1,1,1970)) * 86400 |
+ ut.wHour * 3600 + ut.wMinute * 60 + ut.wSecond
IF nNow < nIssued
Message('The system clock has been set back.') ; RETURN FALSE
END
IF nNow > nValid
Message('Offline grace period expired. Please reconnect.') ; RETURN FALSE
END
Message(GETINI('Cache','Msg','','licentio.cache')) ! your offline message
RETURN TRUE
Uses the native HTTPSend, HashString and INIWrite functions — no external libraries required.
PROCEDURE ValidateLicense() : boolean
sIniPath is string = fExeDir() + "\" + "licentio.ini"
sCachePath is string = fExeDir() + "\" + "licentio.cache"
IF NOT fFileExist(sIniPath) THEN
Error("License configuration file not found.")
RESULT False
END
sProgramID is string = INIRead("Licentio", "ProgramID", "", sIniPath)
sClientID is string = INIRead("Licentio", "ClientID", "", sIniPath)
sAPIKey is string = INIRead("Licentio", "APIKey", "", sIniPath)
sJSON is ANSI string = [
{"program_id":"%1","client_id":"%2","api_key":"%3","machine_id":"%4"}
]
sJSON = StringBuild(sJSON, sProgramID, sClientID, sAPIKey, NetMachineName())
oRequest is httpRequest
oResponse is httpResponse
oRequest..URL = "https://licentio.dev/validate"
oRequest..Method = httpPost
oRequest..ContentType = "application/json"
oRequest..Content = sJSON
! Try online; on ANY network failure, fall back to the cached offline grace
bNetError is boolean = False
WHEN EXCEPTION IN
oResponse = HTTPSend(oRequest)
DO
bNetError = True
END
IF bNetError OR ErrorOccurred OR oResponse..StatusCode <> 200 THEN
RESULT CheckOffline(sClientID, sAPIKey, sCachePath)
END
vResult is Variant = JSONToVariant(oResponse..Content)
IF NOT vResult.valid..Value THEN
Error("License invalid: " + vResult.message..Value)
RESULT False
END
! Cache the signed offline window (only present if the license enables grace)
sToken is string = vResult.offline_token..Value
IF sToken <> "" THEN
INIWrite("Cache", "IssuedAt", vResult.issued_at..Value, sCachePath)
INIWrite("Cache", "ValidUntil", vResult.valid_until..Value, sCachePath)
INIWrite("Cache", "Token", sToken, sCachePath)
INIWrite("Cache", "Msg", vResult.offline_message..Value, sCachePath)
END
IF vResult.warning..Value THEN Info(vResult.warning_message..Value) END
RESULT True
! --- Offline grace check --------------------------------------------
PROCEDURE CheckOffline(sClientID is string, sAPIKey is string, sCachePath is string) : boolean
IF NOT fFileExist(sCachePath) THEN
Error("No internet connection and no cached license.")
RESULT False
END
nIssuedAt is 8-byte int = Val(INIRead("Cache", "IssuedAt", "0", sCachePath))
nValidUntil is 8-byte int = Val(INIRead("Cache", "ValidUntil", "0", sCachePath))
sToken is string = INIRead("Cache", "Token", "", sCachePath)
sMsg is string = INIRead("Cache", "Msg", "", sCachePath)
! Rebuild the signature with our own API key and compare
sCanonical is string = sClientID + "|" + nIssuedAt + "|" + nValidUntil
bufHash is Buffer = HashString(HA_HMAC_SHA_256, sCanonical, sAPIKey)
sMyToken is string = BufferToHexa(bufHash, WithoutGrouping) // SansRegroupement in the FR IDE
IF sMyToken <> sToken THEN // WLanguage string compare is case-insensitive
Error("Cached license is corrupt.")
RESULT False
END
nNow is 8-byte int = DateTimeToEpoch(DateTimeLocalToUTC(Now()))
IF nNow < nIssuedAt THEN
Error("System clock has been set back.")
RESULT False
END
IF nNow > nValidUntil THEN
Error("Offline grace period has expired. Please reconnect.")
RESULT False
END
Info(sMsg)
RESULT True
! At program startup:
IF NOT ValidateLicense() THEN
EndProgram()
END
Uses the requests library. Install with pip install requests. Standard-library hmac handles the offline check.
import configparser, hmac, hashlib, time, os, socket, sys
import requests
INI, CACHE = "licentio.ini", "licentio.cache"
def _sign(api_key, client_id, issued_at, valid_until):
canonical = f"{client_id}|{issued_at}|{valid_until}"
return hmac.new(api_key.encode(), canonical.encode(), hashlib.sha256).hexdigest()
def validate_license():
cfg = configparser.ConfigParser(); cfg.read(INI)
program_id = cfg["Licentio"]["ProgramID"]
client_id = cfg["Licentio"]["ClientID"]
api_key = cfg["Licentio"]["APIKey"]
try:
data = requests.post("https://licentio.dev/validate", timeout=10, json={
"program_id": program_id, "client_id": client_id,
"api_key": api_key, "machine_id": socket.gethostname(),
}).json()
except requests.RequestException:
return check_offline(client_id, api_key) # no connection → try grace
if not data["valid"]:
print(f"License invalid: {data['message']}")
return False
# Cache the signed offline window for future offline runs
if data.get("offline_token"):
c = configparser.ConfigParser()
c["Cache"] = {"IssuedAt": str(data["issued_at"]),
"ValidUntil": str(data["valid_until"]),
"Token": data["offline_token"],
"Msg": data.get("offline_message", "")}
with open(CACHE, "w") as f: c.write(f)
if data.get("warning"):
print(f"Warning: {data['warning_message']}")
return True
def check_offline(client_id, api_key):
if not os.path.exists(CACHE):
print("No internet connection and no cached license.")
return False
c = configparser.ConfigParser(); c.read(CACHE)
cache = c["Cache"]
issued_at, valid_until = int(cache["IssuedAt"]), int(cache["ValidUntil"])
now = int(time.time())
if not hmac.compare_digest(_sign(api_key, client_id, issued_at, valid_until), cache["Token"]):
print("Cached license is corrupt."); return False # tampered window
if now < issued_at:
print("System clock has been set back."); return False # anti-rollback
if now > valid_until:
print("Offline grace period has expired. Please reconnect."); return False
print(cache.get("Msg") or "Running offline.")
return True
if not validate_license():
sys.exit(1)
Uses HttpClient, System.Text.Json and HMACSHA256 — all in the standard library.
using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
static class Licentio
{
const string Ini = "licentio.ini", Cache = "licentio.cache";
static Dictionary<string,string> ReadIni(string path, string section)
{
var d = new Dictionary<string,string>(); var cur = "";
foreach (var raw in File.ReadAllLines(path))
{
var line = raw.Trim();
if (line.StartsWith("[") && line.EndsWith("]")) cur = line[1..^1];
else if (cur == section && line.Contains('='))
{ var i = line.IndexOf('='); d[line[..i].Trim()] = line[(i+1)..].Trim(); }
}
return d;
}
static string Sign(string apiKey, string clientId, long issuedAt, long validUntil)
{
using var h = new HMACSHA256(Encoding.UTF8.GetBytes(apiKey));
var canonical = $"{clientId}|{issuedAt}|{validUntil}";
return Convert.ToHexString(h.ComputeHash(Encoding.UTF8.GetBytes(canonical))).ToLowerInvariant();
}
public static bool Validate()
{
var cfg = ReadIni(Ini, "Licentio");
string programId = cfg["ProgramID"], clientId = cfg["ClientID"], apiKey = cfg["APIKey"];
try
{
using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) };
var body = JsonSerializer.Serialize(new {
program_id = programId, client_id = clientId,
api_key = apiKey, machine_id = Environment.MachineName });
var resp = http.PostAsync("https://licentio.dev/validate",
new StringContent(body, Encoding.UTF8, "application/json")).Result;
var data = JsonSerializer.Deserialize<JsonElement>(resp.Content.ReadAsStringAsync().Result);
if (!data.GetProperty("valid").GetBoolean())
{
Console.WriteLine("License invalid: " + data.GetProperty("message").GetString());
return false;
}
if (data.TryGetProperty("offline_token", out var tok) && tok.ValueKind == JsonValueKind.String)
File.WriteAllText(Cache,
"[Cache]\n" +
$"IssuedAt={data.GetProperty("issued_at").GetInt64()}\n" +
$"ValidUntil={data.GetProperty("valid_until").GetInt64()}\n" +
$"Token={tok.GetString()}\n" +
$"Msg={data.GetProperty("offline_message").GetString()}\n");
return true;
}
catch (Exception) // no connection → try grace
{
return CheckOffline(clientId, apiKey);
}
}
static bool CheckOffline(string clientId, string apiKey)
{
if (!File.Exists(Cache)) { Console.WriteLine("No connection and no cached license."); return false; }
var c = ReadIni(Cache, "Cache");
long issuedAt = long.Parse(c["IssuedAt"]), validUntil = long.Parse(c["ValidUntil"]);
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
if (Sign(apiKey, clientId, issuedAt, validUntil) != c["Token"])
{ Console.WriteLine("Cached license is corrupt."); return false; }
if (now < issuedAt) { Console.WriteLine("System clock has been set back."); return false; }
if (now > validUntil) { Console.WriteLine("Offline grace period has expired. Please reconnect."); return false; }
Console.WriteLine(c.TryGetValue("Msg", out var m) ? m : "Running offline.");
return true;
}
static void Main() { if (!Validate()) Environment.Exit(1); }
}
Ready to protect your software?
Get Started Free