{{-- ── Sidebar ── --}} {{-- ── Main content ── --}}
{{-- Page header --}} {{-- ════ OVERVIEW ════ --}}

Overview

All endpoints accept POST requests with a JSON body and respond with JSON. Every request is authenticated using a per-license HMAC-SHA256 signature — no API keys or Bearer tokens. The license secret key is shown in each license's credentials page.

@foreach([ ['Base URL', $baseUrl . '/api'], ['Protocol', 'HTTPS · JSON · POST'], ['Auth', 'HMAC-SHA256 per request'], ['Rate limit', '60 req / min / IP'], ['Timestamp window', '±5 minutes'], ['Nonce TTL', '10 minutes (anti-replay)'], ] as [$k,$v])
{{ $k }}
{{ $v }}
@endforeach
{{-- ════ AUTHENTICATION ════ --}}

Authentication

Every request body must include three security fields in addition to the endpoint-specific fields. The secret key is unique per license and available in the customer portal under Licenses → [license] → Credentials.

FieldTypeDescription
timestamp integer Unix timestamp (seconds). Must be within ±300 seconds of the server clock.
nonce string Random unique hex string (16+ chars). Each nonce can only be used once within a 10-minute window — prevents replay attacks.
signature string HMAC-SHA256 hex digest of the payload computed with the license's secret key. See Signing Requests.
The secret key is per-license, not a global API key. Each license has its own unique secret key shown in the license credentials. Never expose the secret key in client-side code.
{{-- ════ SIGNING ════ --}}

Signing Requests

The signature is computed over the entire payload (excluding the signature field itself) using the license secret key. Steps:

  1. Add timestamp = time() and nonce = random 16-byte hex to the payload.
  2. Sort all payload keys alphabetically (ksort).
  3. Build the message: timestamp + ":" + nonce + ":" + json_encode(sorted_payload)
  4. Compute: signature = hash_hmac('sha256', message, secretKey)
  5. Add signature to the payload and POST the full JSON.
PHP — signRequest() helper
function signRequest(array $payload, string $secretKey): array
{
    $payload['timestamp'] = time();
    $payload['nonce']     = bin2hex(random_bytes(16));

    $data = $payload;
    ksort($data); // sort alphabetically — required

    $message = $payload['timestamp'] . ':' . $payload['nonce'] . ':' . json_encode($data);
    $payload['signature'] = hash_hmac('sha256', $message, $secretKey);

    return $payload; // now contains timestamp, nonce, signature
}
PHP — reusable HTTP helper
function apiPost(string $url, array $payload): ?object
{
    $context = stream_context_create(['http' => [
        'method'        => 'POST',
        'header'        => "Content-Type: application/json\r\nAccept: application/json\r\n",
        'content'       => json_encode($payload),
        'timeout'       => 10,
        'ignore_errors' => true,
    ]]);
    $body = @file_get_contents($url, false, $context);
    return $body ? json_decode($body) : null;
}
{{-- ════ PHP SDK ════ --}}

PHP SDK — Complete Drop-in Class

Copy this class into your project. It handles signing, requests, token storage, and all 5 endpoints.

LicensePlatformSDK.php
<?php

class LicensePlatformSDK
{
    public function __construct(
        private string $licenseKey,
        private string $secretKey,
        private string $baseUrl = '{{ $baseUrl }}'
    ) {}

    // Call once on first install
    public function activate(array $extra = []): ?object
    {
        $response = $this->req('activate', array_merge([
            'domain'          => $_SERVER['HTTP_HOST'] ?? gethostname(),
            'installation_id' => hash('sha256', gethostname()),
        ], $extra));

        if ($response?->success) {
            $this->saveToken([
                'activation_token' => $response->activation_token,
                'activation_id'    => $response->activation_id,
                'expires_at'       => $response->expires_at,
            ]);
        }
        return $response;
    }

    // Call on every app boot — returns true if valid
    public function validate(): bool
    {
        $token = $this->loadToken();
        if (!$token) return false;
        $r = $this->req('validate', ['activation_token' => $token['activation_token']]);
        return (bool) ($r?->valid ?? false);
    }

    // Daily heartbeat — returns response with expires_at
    public function heartbeat(): ?object
    {
        $token = $this->loadToken();
        if (!$token) return null;
        return $this->req('heartbeat', ['activation_token' => $token['activation_token']]);
    }

    // Call from uninstall to free up an activation slot
    public function deactivate(): bool
    {
        $token = $this->loadToken();
        if (!$token) return false;
        $r = $this->req('deactivate', ['activation_token' => $token['activation_token']]);
        if ($r?->success) { @unlink($this->tokenPath()); return true; }
        return false;
    }

    // Check for new version — pass the currently installed version
    public function checkUpdate(string $currentVersion): ?object
    {
        return $this->req('check-update', ['current_version' => $currentVersion]);
    }

    // ── Private helpers ──────────────────────────────────────────
    private function req(string $endpoint, array $data): ?object
    {
        $data['license_key'] = $this->licenseKey;
        $payload = $this->sign($data);
        $ctx = stream_context_create(['http' => [
            'method'        => 'POST',
            'header'        => "Content-Type: application/json\r\nAccept: application/json\r\n",
            'content'       => json_encode($payload),
            'timeout'       => 10,
            'ignore_errors' => true,
        ]]);
        $body = @file_get_contents(rtrim($this->baseUrl,'/').'/api/license/'.$endpoint, false, $ctx);
        return $body ? json_decode($body) : null;
    }

    private function sign(array $p): array
    {
        $p['timestamp'] = time();
        $p['nonce']     = bin2hex(random_bytes(16));
        $d = $p; ksort($d);
        $p['signature'] = hash_hmac('sha256', $p['timestamp'].':'.$p['nonce'].':'.json_encode($d), $this->secretKey);
        return $p;
    }

    private function tokenPath(): string
    {
        return sys_get_temp_dir() . '/lp_' . md5($this->licenseKey) . '.json';
    }

    private function saveToken(array $data): void
    {
        file_put_contents($this->tokenPath(), json_encode($data));
    }

    private function loadToken(): ?array
    {
        $p = $this->tokenPath();
        return file_exists($p) ? json_decode(file_get_contents($p), true) : null;
    }
}

// ── Usage ─────────────────────────────────────────────────────────────────────
$sdk = new LicensePlatformSDK(
    licenseKey: 'ABCD1234-EFGH5678-IJKL9012-MNOP3456',
    secretKey:  'secret-key-from-license-credentials-page'
);

// First install
$r = $sdk->activate();
if (!$r?->success) die('Activation failed: ' . ($r?->message ?? 'no response'));

// Every boot
if (!$sdk->validate()) die('License invalid.');

// Hourly / daily cron
$sdk->heartbeat();

// Uninstall
$sdk->deactivate();
{{-- ════ ENDPOINTS ════ --}} {{-- ACTIVATE --}}
POST /api/license/activate
Activates a license for a domain/installation. Idempotent — calling it again with the same domain/installation returns the existing activation token without consuming another slot. Store the returned activation_token; it is required for all subsequent calls.

Request fields

@foreach([ ['license_key', 'string', 'yes', 'The license key (XXXXXXXX-XXXXXXXX-XXXXXXXX-XXXXXXXX)'], ['domain', 'string', 'rec', 'Hostname of the server being activated (example.com)'], ['installation_id', 'string', 'rec', 'Unique ID for this installation. Recommended: hash(\'sha256\', gethostname())'], ['machine_fingerprint','string', 'no', 'Hardware fingerprint for device-locked licenses'], ['server_hostname', 'string', 'no', 'Human-readable server name (for audit logs)'], ['os_platform', 'string', 'no', 'Operating system: Linux, Windows, macOS…'], ['product_version', 'string', 'no', 'Your product\'s current version (e.g. 2.1.0)'], ['timestamp', 'integer', 'yes', 'Unix timestamp — see Authentication'], ['nonce', 'string', 'yes', 'Unique random hex — see Authentication'], ['signature', 'string', 'yes', 'HMAC-SHA256 — see Signing Requests'], ] as [$f,$t,$r,$d]) @endforeach
FieldTypeRequiredDescription
{{ $f }} {{ $t }} {{ $r==='yes'?'required':($r==='rec'?'recommended':'optional') }} {{ $d }}

Success (200)

{
  "success":          true,
  "message":          "activated",
  "license_key":      "ABCD1234-...",
  "activation_id":    "uuid-v4",
  "activation_token": "64-char-hex",
  "status":           "active",
  "type":             "regular",
  "product":          "Your Product",
  "expires_at":       "2026-12-31T00:00:00Z",
  "is_lifetime":      false,
  "max_activations":  3,
  "activations_used": 1,
  "timestamp":        1748000001,
  "signature":        "server-hmac"
}

Error (422)

{
  "success": false,
  "error":   "MAX_ACTIVATIONS",
  "message": "Maximum activations reached."
}

// Possible error codes:
// INVALID_LICENSE
// INVALID_SIGNATURE
// MAX_ACTIVATIONS
// INVALID_DOMAIN
{{-- VALIDATE --}}
POST /api/license/validate
Validates an existing activation without changing state. Verifies the activation integrity checksum. Call this on every application boot. Also updates last_validated_at on the server.

Request

{
  "license_key":      "ABCD1234-...",
  "activation_token": "64-char-hex",
  // OR use domain instead:
  "domain":           "example.com",
  "timestamp":        1748000000,
  "nonce":            "abc123...",
  "signature":        "hmac-sha256"
}

Success (200)

{
  "valid":         true,
  "license_key":   "ABCD1234-...",
  "status":        "active",
  "type":          "regular",
  "product":       "Your Product",
  "expires_at":    "2026-12-31T00:00:00Z",
  "is_lifetime":   false,
  "activation_id": "uuid-v4",
  "timestamp":     1748000001,
  "signature":     "server-hmac"
}
// Errors: INVALID_LICENSE,
// INVALID_SIGNATURE, NOT_ACTIVATED,
// TAMPER_DETECTED
{{-- HEARTBEAT --}}
POST /api/license/heartbeat
Lightweight ping to keep the activation alive. Returns current expiry so you can warn the user before renewal is needed. Recommended: send once every 24 hours from a background task or cron.
{
  "license_key":      "ABCD1234-...",
  "activation_token": "64-char-hex",
  "timestamp":        1748000000,
  "nonce":            "abc123...",
  "signature":        "hmac-sha256"
}
{
  "alive":       true,
  "expires_at":  "2026-12-31T00:00:00Z",
  "server_time": "2026-06-02T10:00:00Z",
  "timestamp":   1748000001,
  "signature":   "server-hmac"
}
// Errors: INVALID_REQUEST, NOT_FOUND
{{-- DEACTIVATE --}}
POST /api/license/deactivate
Removes an activation and frees the slot. Call this from your uninstall script so the user can activate on a new server. Works even if the license is expired.
{
  "license_key":      "ABCD1234-...",
  "activation_token": "64-char-hex",
  // OR: "domain": "example.com"
  "timestamp":        1748000000,
  "nonce":            "abc123...",
  "signature":        "hmac-sha256"
}
{
  "success":   true,
  "message":   "Activation removed.",
  "timestamp": 1748000001,
  "signature": "server-hmac"
}
// Errors: INVALID_REQUEST, NOT_FOUND
{{-- CHECK-UPDATE --}}
POST /api/license/check-update
Checks whether a newer stable version exists. Returns the version number, changelog, SHA-256 checksum, and a signed download URL. Verify the checksum after downloading before applying the update.
{
  "license_key":     "ABCD1234-...",
  "current_version": "2.0.1",
  "timestamp":       1748000000,
  "nonce":           "abc123...",
  "signature":       "hmac-sha256"
}
{
  "update_available": true,
  "current_version":  "2.0.1",
  "latest_version":   "2.1.0",
  "changelog":        "- Fixed X\n- Added Y",
  "download_url":     "https://panel.../dl/...",
  "checksum":         "sha256-hex",
  "timestamp":        1748000001,
  "signature":        "server-hmac"
}
// No update: update_available: false
// changelog + download_url = null
{{-- ════ ERRORS ════ --}}

Error Codes

All errors return HTTP 422 with "success": false. Rate limit violations return HTTP 429.

@foreach([ ['INVALID_LICENSE', '422','Key doesn\'t exist, is revoked, suspended, or expired. Show the user "License key is invalid" and prompt re-entry.'], ['INVALID_SIGNATURE', '422','Signature mismatch. Check secret key, signing order, and that your clock is within 5 minutes of server time.'], ['MAX_ACTIVATIONS', '422','All activation slots are used. User must deactivate from another installation before activating here.'], ['INVALID_DOMAIN', '422','Domain is not a valid hostname or IP address. Sanitize the domain before sending.'], ['NOT_ACTIVATED', '422','No active activation found for the provided token or domain. Call /activate first.'], ['TAMPER_DETECTED', '422','The local activation token or storage was tampered with. Re-activate the license.'], ['NOT_FOUND', '422','Activation token not found or already deactivated.'], ['INVALID_REQUEST', '422','Generic — missing required fields, or invalid signature/timestamp.'], ['TOO_MANY_REQUESTS', '429','Rate limit exceeded (10 failed attempts per IP in 5 min). Wait before retrying.'], ] as [$code,$http,$desc]) @endforeach
CodeHTTPMeaning & Action
{{ $code }} {{ $http }} {{ $desc }}
{{-- ════ RATE LIMITING ════ --}}

Rate Limiting

@foreach([ ['Global (all endpoints)', '60 req / min / IP', 'Returns HTTP 429. Wait 60 seconds and retry.'], ['Brute force guard', '10 failures / 5 min / IP', 'Tracks failed activation attempts and logs a security event.'], ['Nonce uniqueness', '1 use per 10 min', 'Each nonce string can only be used once within its TTL window.'], ['Timestamp tolerance', '±300 seconds', 'Requests older than 5 min or timestamped in the future are rejected.'], ] as [$l,$v,$n]) @endforeach
LimitValueNotes
{{ $l }} {{ $v }} {{ $n }}
{{ $appName }} · License API v1 Admin Panel