Software Integration Portal

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:

https://license.akvinfotech.com/api

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"
}
Always verify 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_xxxxxxxxxxxx
or the X-Api-Key header. Generate one from Vendor → API Keys. Never ship this key inside a distributed desktop app.
Never embed a Vendor API Key in client-distributed software. Anything shipped to end users can be extracted from the binary. Vendor keys belong on your own server only.

3 Rate Limits

ScopeLimitApplies 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

POST /api/license/activate License Key

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).

Request Body
{
  "license_key": "PRO-A1B2-C3D4-E5F6",
  "hardware_hash": "A1B2C3D4E5F67890",
  "machine_name": "Workstation-PC-01",
  "os_info": "Windows 11 Pro"
}
Response (Success)
{
  "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": "..."
}
200 success · 403 max activation limit reached / license inactive · 404 invalid license key
POST /api/license/validate License Key

Lightweight heartbeat to confirm a specific machine is still validly activated. Call this periodically (not on every app launch) rather than re-activating.

Request Body
{
  "license_key": "PRO-A1B2-C3D4-E5F6",
  "hardware_hash": "A1B2C3D4E5F67890"
}
Response (Success)
{
  "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": "..."
}
200 valid · 403 expired / suspended / machine not activated · 404 key not found
POST /api/license/deactivate License Key

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.

Request Body
{
  "license_key": "PRO-A1B2-C3D4-E5F6",
  "hardware_hash": "A1B2C3D4E5F67890"
}
Response (Success)
{
  "status": "success",
  "message": "Device deactivated successfully.",
  "deactivated_at": "2026-01-19T10:00:00+00:00",
  "signature": "..."
}
200 success · 404 license or active registration not found
GET /api/license/info License Key

Read-only lookup for support tooling — product, vendor, activation count, and expiry for a given key. Query string: ?license_key=...

Response (Success)
{
  "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": "..."
}
200 success · 404 not found · 422 missing license_key
POST /api/license/reset License Key or Vendor Key

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.

Request Body
{
  "license_key": "PRO-A1B2-C3D4-E5F6",
  "email": "[email protected]"
}
Response (Success)
{
  "status": "pending_confirmation",
  "message": "If the provided email matches our records, a confirmation code has been sent to it...",
  "signature": "..."
}
200 success or pending_confirmation · 401 no vendor key and no email provided · 404 license not found
POST /api/license/reset/confirm Confirmation Code

Completes an email-initiated reset. The code expires 15 minutes after being issued and can only be used once.

Request Body
{
  "license_key": "PRO-A1B2-C3D4-E5F6",
  "code": "482913"
}
Response (Success)
{
  "status": "success",
  "message": "All 2 active devices reset successfully.",
  "reset_count": 2,
  "timestamp": "2026-01-19T10:00:00+00:00",
  "signature": "..."
}
200 success · 401 invalid or expired code · 404 license not found
GET /api/update/check None

Checks whether a newer stable release exists for a product. Query string: ?product_slug=acme-cad-pro¤t_version=1.2.0

Response (Success)
{
  "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": "..."
}
200 success (has_update may be false) · 404 product not found · 422 missing product_slug
GET /api/update/download/{version} License Key

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).

200 file stream · 403 license invalid/expired or machine not activated · 404 installer not available
POST /api/offline/request License Key

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.

Request Body
{
  "license_key": "PRO-A1B2-C3D4-E5F6",
  "hardware_hash": "A1B2C3D4E5F67890"
}
Response (Success)
{
  "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": "..."
}
200 success · 403 activation not allowed (limit reached / inactive) · 404 license not found
POST /api/offline/activate Request Code

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 Body
{
  "request_code": "OFFLINE-REQ-8F3K2LX9QATZ"
}
Response (Success)
{
  "status": "success",
  "message": "Offline activation code already generated.",
  "activation_code": "...",
  "signature": "..."
}
200 approved (activation_code present) · 202 still pending approval · 403 request code expired · 404 code not found

5 Error Codes

StatusMeaningWhat to do
400Malformed webhook / signature payloadNot applicable to SDK integrations — internal only.
401Missing/invalid vendor key or reset confirmation codeRe-check the Authorization header or request a fresh code.
403Action not allowed for current license stateInspect message — expired, suspended, activation limit reached, or machine not registered.
404License key / request code / resource not foundDouble-check the identifier; don't retry blindly.
422Validation failed (missing/invalid field)Fix the request payload — this won't succeed on retry without changes.
429Rate limitedBack 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:

1

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.

2

An administrator reviews and approves the request in the Admin Portal's Offline Generator, which produces a signed activation code.

3

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_hash from 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 403 gracefully: show the human-readable message field 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.