Developer API & Licensing Integration Guide
Everything needed to bind your desktop or server application to LICSERVER: hardware-locked activation, RSA-signed responses, license lifecycle management, auto-updates, and an offline air-gapped path for machines with no internet access.
1 Overview & Response Envelope
LICSERVER lets you list a product, issue license keys on purchase, and lock activation to a customer's hardware. Your client application talks to a small, stable REST surface under one base URL:
Every response — success or error — is a JSON object signed with the platform's RSA-2048 private key:
"status": "success",
"...": "...(endpoint-specific fields, keys sorted alphabetically)...",
"signature": "base64-encoded RSA-SHA256 signature over the JSON above, minus this field"
}
signature before trusting a response (see Section 6).
It's what stops a man-in-the-middle proxy from spoofing a "license valid" response to your app.
2 Authentication Model
There are two distinct trust levels in the API — use the lowest one that gets the job done:
| Tier | Used for | How it's presented |
|---|---|---|
| License Key + Hardware Hash | Activate, validate, deactivate, check for updates — the calls your shipped application makes on the customer's machine. | Sent as regular request fields (license_key, hardware_hash). Proof of possession of a purchased key is the credential — there's no separate secret to embed in your app. |
| Vendor API Key | Server-to-server calls you make from your own backend on a customer's behalf (e.g. an instant reset from your support tooling). |
Authorization: Bearer pk_live_xxxxxxxxxxxxor the X-Api-Key header. Generate one from Vendor → API Keys. Never ship this key inside a distributed desktop app.
|
3 Rate Limits
| Scope | Limit | Applies to |
|---|---|---|
| Standard | 60 requests / minute per IP address | All /api/license/*, /api/update/*, and /api/offline/* endpoints |
| Sensitive | 10 requests / minute per license key + IP combination | /api/license/reset and /api/license/reset/confirm |
A throttled request returns HTTP 429. Implement exponential backoff in your client
rather than retrying immediately — a heartbeat validate call every few seconds is unnecessary;
once every 15–60 minutes is typical.
4 REST API Reference
Binds a license key to a device hardware hash and registers the activation. Calling it again with the same license_key + hardware_hash is idempotent (it just refreshes the check-in).
{
"license_key": "PRO-A1B2-C3D4-E5F6",
"hardware_hash": "A1B2C3D4E5F67890",
"machine_name": "Workstation-PC-01",
"os_info": "Windows 11 Pro"
}
{
"status": "success",
"license_key": "PRO-A1B2-C3D4-E5F6",
"product_name": "Acme CAD Pro",
"license_type": "standard",
"expires_at": null,
"max_activations": 3,
"active_count": 1,
"hardware_hash": "A1B2C3D4E5F67890",
"activated_at": "2026-01-19T10:00:00+00:00",
"timestamp": "2026-01-19T10:00:00+00:00",
"signature": "..."
}
Lightweight heartbeat to confirm a specific machine is still validly activated. Call this periodically (not on every app launch) rather than re-activating.
{
"license_key": "PRO-A1B2-C3D4-E5F6",
"hardware_hash": "A1B2C3D4E5F67890"
}
{
"status": "success",
"valid": true,
"license_key": "PRO-A1B2-C3D4-E5F6",
"license_status": "active",
"hardware_hash": "A1B2C3D4E5F67890",
"expires_at": null,
"timestamp": "2026-01-19T10:00:00+00:00",
"signature": "..."
}
Releases one device slot — call this from your app's "deactivate this machine" / uninstall flow so the customer can move the license to another computer.
{
"license_key": "PRO-A1B2-C3D4-E5F6",
"hardware_hash": "A1B2C3D4E5F67890"
}
{
"status": "success",
"message": "Device deactivated successfully.",
"deactivated_at": "2026-01-19T10:00:00+00:00",
"signature": "..."
}
Read-only lookup for support tooling — product, vendor, activation count, and expiry for a given key. Query string: ?license_key=...
{
"status": "success",
"license_key": "PRO-A1B2-C3D4-E5F6",
"product_name": "Acme CAD Pro",
"product_slug": "acme-cad-pro",
"vendor_name": "Acme Software Inc.",
"license_type": "standard",
"status": "active",
"max_activations": 3,
"active_activations_count": 1,
"expires_at": null,
"created_at": "2026-01-10T09:00:00+00:00",
"signature": "..."
}
Deactivates every active device on a license. With a Vendor API Key it resets immediately. With just the customer's email, it never resets anything directly — it emails a one-time 6-digit code and returns pending_confirmation. This two-step design exists specifically so a leaked license key + a public email address can't be used to grief a customer's devices.
{
"license_key": "PRO-A1B2-C3D4-E5F6",
"email": "[email protected]"
}
{
"status": "pending_confirmation",
"message": "If the provided email matches our records, a confirmation code has been sent to it...",
"signature": "..."
}
Completes an email-initiated reset. The code expires 15 minutes after being issued and can only be used once.
{
"license_key": "PRO-A1B2-C3D4-E5F6",
"code": "482913"
}
{
"status": "success",
"message": "All 2 active devices reset successfully.",
"reset_count": 2,
"timestamp": "2026-01-19T10:00:00+00:00",
"signature": "..."
}
Checks whether a newer stable release exists for a product. Query string: ?product_slug=acme-cad-pro¤t_version=1.2.0
{
"status": "success",
"product_slug": "acme-cad-pro",
"current_version": "1.2.0",
"latest_version": "1.3.0",
"has_update": true,
"changelog": "Bug fixes and performance improvements.",
"checksum_sha256": "...",
"release_date": "2026-02-01T00:00:00+00:00",
"download_url": "{{ https://license.akvinfotech.com }}/api/update/download/42",
"signature": "..."
}
Streams the installer file for an activated, non-expired license. Query string: ?license_key=...&hardware_hash=.... Returns the binary file directly on success (not JSON).
First step of the air-gapped flow (see Section 8) — run on any internet-connected machine to generate a request code an admin can approve.
{
"license_key": "PRO-A1B2-C3D4-E5F6",
"hardware_hash": "A1B2C3D4E5F67890"
}
{
"status": "success",
"request_code": "OFFLINE-REQ-8F3K2LX9QATZ",
"expires_at": "2026-01-26T10:00:00+00:00",
"message": "Offline request submitted. An administrator must approve it before activation.",
"signature": "..."
}
Poll this with the request code once an admin has approved it in the Admin Portal, to retrieve the signed activation code for the air-gapped machine.
{
"request_code": "OFFLINE-REQ-8F3K2LX9QATZ"
}
{
"status": "success",
"message": "Offline activation code already generated.",
"activation_code": "...",
"signature": "..."
}
5 Error Codes
| Status | Meaning | What to do |
|---|---|---|
| 400 | Malformed webhook / signature payload | Not applicable to SDK integrations — internal only. |
| 401 | Missing/invalid vendor key or reset confirmation code | Re-check the Authorization header or request a fresh code. |
| 403 | Action not allowed for current license state | Inspect message — expired, suspended, activation limit reached, or machine not registered. |
| 404 | License key / request code / resource not found | Double-check the identifier; don't retry blindly. |
| 422 | Validation failed (missing/invalid field) | Fix the request payload — this won't succeed on retry without changes. |
| 429 | Rate limited | Back off. See Section 3. |
6 RSA Signature Verification
Every response includes a signature field: an RSA-SHA256 signature (base64-encoded)
computed over the JSON body with keys sorted alphabetically and the signature field
itself excluded. Verify it against this platform public key before trusting a response:
using System.Security.Cryptography;
using System.Text;
bool VerifySignature(string canonicalJson, string signatureBase64, string publicKeyPem)
{
using var rsa = RSA.Create();
rsa.ImportFromPem(publicKeyPem);
var data = Encoding.UTF8.GetBytes(canonicalJson);
var signature = Convert.FromBase64String(signatureBase64);
return rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
}
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
import base64
def verify_signature(canonical_json: str, signature_b64: str, public_key_pem: str) -> bool:
public_key = serialization.load_pem_public_key(public_key_pem.encode())
try:
public_key.verify(
base64.b64decode(signature_b64),
canonical_json.encode(),
padding.PKCS1v15(),
hashes.SHA256(),
)
return True
except Exception:
return False
const crypto = require('crypto');
function verifySignature(canonicalJson, signatureB64, publicKeyPem) {
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(canonicalJson, 'utf8');
verifier.end();
return verifier.verify(publicKeyPem, Buffer.from(signatureB64, 'base64'));
}
$publicKey = openssl_pkey_get_public($publicKeyPem);
$valid = openssl_verify(
$canonicalJson,
base64_decode($signatureB64),
$publicKey,
OPENSSL_ALGO_SHA256
) === 1;
Building the canonical JSON: take the response body, remove the signature
key, sort the remaining keys alphabetically, and re-encode with no extra whitespace —
exactly how the server built it before signing.
7 SDK Integration Code
A minimal activation client in each language — adapt the hardware fingerprint function to your platform.
using System.Net.Http;
using System.Text;
using System.Text.Json;
public class LicServerClient
{
private static readonly HttpClient client = new HttpClient();
private const string BaseUrl = "https://license.akvinfotech.com";
public static async Task<bool> ValidateAsync(string licenseKey, string hardwareHash)
{
var payload = new { license_key = licenseKey, hardware_hash = hardwareHash };
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
var response = await client.PostAsync($"{BaseUrl}/api/license/validate", content);
var body = await response.Content.ReadAsStringAsync();
using var doc = JsonDocument.Parse(body);
return response.IsSuccessStatusCode
&& doc.RootElement.GetProperty("valid").GetBoolean();
}
}
import requests
BASE_URL = "https://license.akvinfotech.com"
def activate_license(license_key, hardware_hash, machine_name="Workstation-01"):
response = requests.post(f"{BASE_URL}/api/license/activate", json={
"license_key": license_key,
"hardware_hash": hardware_hash,
"machine_name": machine_name,
})
data = response.json()
if response.status_code == 200:
print("Activated:", data["message"])
return True
print("Activation failed:", data.get("message"))
return False
const BASE_URL = "https://license.akvinfotech.com";
async function validateLicense(licenseKey, hardwareHash) {
const res = await fetch(`${BASE_URL}/api/license/validate`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ license_key: licenseKey, hardware_hash: hardwareHash }),
});
const data = await res.json();
return res.ok && data.valid === true;
}
use GuzzleHttp\Client;
$client = new Client(['base_uri' => 'https://license.akvinfotech.com']);
function activateLicense(Client $client, string $licenseKey, string $hardwareHash): array
{
$response = $client->post('/api/license/activate', [
'json' => [
'license_key' => $licenseKey,
'hardware_hash' => $hardwareHash,
],
'http_errors' => false,
]);
return json_decode((string) $response->getBody(), true);
}
curl -X POST https://license.akvinfotech.com/api/license/activate \
-H "Content-Type: application/json" \
-d '{
"license_key": "PRO-A1B2-C3D4-E5F6",
"hardware_hash": "A1B2C3D4E5F67890",
"machine_name": "Workstation-PC-01"
}'
# Vendor-authenticated call (server-side only, never in a shipped app):
curl -X POST https://license.akvinfotech.com/api/license/reset \
-H "Authorization: Bearer pk_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d '{"license_key": "PRO-A1B2-C3D4-E5F6"}'
8 Offline Air-Gapped Activation
For enterprise workstations with no internet access, activation moves through an approval step instead of happening instantly:
On any internet-connected machine, call POST /api/offline/request with the license key and the air-gapped machine's hardware hash. You get back a request_code.
An administrator reviews and approves the request in the Admin Portal's Offline Generator, which produces a signed activation code.
Poll POST /api/offline/activate with the same request_code (from any connected machine) to retrieve the activation_code, then apply it on the air-gapped machine.
9 Best Practices
- Hardware fingerprint: derive
hardware_hashfrom stable identifiers (disk serial, motherboard UUID, MAC address) hashed together with SHA-256 — never from something that changes on a routine driver update. - Cache the last successful validation locally (with its signature) and allow a grace period (e.g. 7–14 days) offline before forcing re-validation, so laptops without constant internet aren't locked out.
- Don't poll aggressively: validate once per app launch and then on an hourly-or-longer timer, not every request/action.
- Always verify the signature on cached/offline responses before trusting them — that's what prevents a tampered local cache file from faking a valid license forever.
- Handle
403gracefully: show the human-readablemessagefield to the user (expired, limit reached, etc.) rather than a generic failure. - Keep Vendor API Keys server-side only and rotate them from Vendor → API Keys if you suspect exposure.