What's new
Panelica Community Forum

Welcome to the official Panelica Community Forum — the central hub for server administrators, developers, and hosting professionals. Register a free account today to access technical discussions, product announcements, feature requests, and direct support from the Panelica team. Be part of the growing community shaping the future of server management.

API Keys & HMAC Authentication --- Complete Guide

admin

Administrator
Staff member
API Keys & HMAC Authentication --- Complete Guide​

This guide covers everything about Panelica's API key system --- creating keys, HMAC-SHA256 authentication, permission scopes, rate limiting, IP whitelisting, and integration examples in 6 languages.

Overview​

Panelica provides a full-featured External API on port 3002 that allows you to manage your server programmatically. All API requests are authenticated using HMAC-SHA256 signatures --- a secure, industry-standard method that never sends your secret over the wire.

Key Features:
  • HMAC-SHA256 Authentication --- Cryptographic request signing (no passwords in transit)
  • 62 Permission Scopes --- Granular access control across 16 resource categories
  • 4 Rate Limit Tiers --- From 60 req/min (Starter) to unlimited (Enterprise)
  • IP Whitelisting --- Restrict API access to specific IP addresses
  • Environment Separation --- Live (pk_live_) and Test (pk_test_) keys
  • Encrypted Storage --- Secrets encrypted with AES-256-GCM
  • Usage Analytics --- Per-key request logging with method, path, status, response time

---

Accessing the API Management Page​

Go to Developer > API Management in the panel navigation.

The page has 6 tabs:

TabDescription
API EndpointsBrowse all available External API endpoints with documentation
API KeysCreate and manage your API keys
SSH CommandsCLI command examples for terminal-based access
AnalyticsUsage metrics, charts, success/failure rates
Quick Start GuideStep-by-step integration guide with code examples
Mobile PairingQR code pairing for the Panelica mobile app

Security Note: This page is protected by High Security Gate. If you have High Security Mode enabled, you'll need to verify with your 2FA code before accessing it.

---

Creating an API Key​

Step 1: Go to the API Keys tab and click Create API Key.

Step 2: Fill in the form:

FieldRequiredDescription
NameYesA descriptive name (e.g., "Monitoring Script", "WHMCS Integration")
DescriptionNoOptional notes about what this key is used for
ScopesYesPermission scopes --- what this key can access (minimum 1)
Rate Limit TierNoRequest rate limit: Starter, Professional, Business, or Enterprise
IP WhitelistNoRestrict access to specific IP addresses (empty = allow all)
Expires InNoDays until expiration (0 = never expires)
EnvironmentNo"live" (production) or "test" (testing/development)

Step 3: Click Create.

Step 4: IMPORTANT --- Copy your credentials immediately!

A modal displays your credentials:

  • API Key: pk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
  • API Secret: sk_live_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

WARNING: The full API Key and Secret are shown only once at creation time. After closing the modal, only the key prefix (first 8 characters) is visible. Store your credentials in a password manager or secure location immediately.

---

API Key Format​

CredentialFormatLengthExample
API Keypk_{env}_{random}~40 charspk_live_a1b2c3d4e5f6g7h8i9j0...
API Secretsk_{env}_{random}~56 charssk_live_x9y8z7w6v5u4t3s2r1q0...

  • pk_ = Public Key (sent in headers, identifies the key)
  • sk_ = Secret Key (never sent directly, used to create HMAC signatures)
  • live = Production environment
  • test = Testing environment

---

HMAC-SHA256 Authentication​

Every API request must include 3 headers:

HeaderValueExample
X-API-KeyYour API keypk_live_a1b2c3d4...
X-TimestampCurrent Unix timestamp (seconds)1709827200
X-SignatureHMAC-SHA256 signature (hex)a1b2c3d4e5f6... (64 chars)

How the Signature is Generated:

  1. Build the string to sign: METHOD + PATH + TIMESTAMP + BODY
  2. For DELETE requests, the body is excluded
  3. Compute HMAC-SHA256 using your API Secret as the key
  4. Convert to lowercase hex string (64 characters)

Example:
Code:
Method:    GET
Path:      /v1/domains
Timestamp: 1709827200
Body:      (empty for GET)

String to sign: "GET/v1/domains1709827200"
HMAC Key:       "sk_live_your_secret_here"
Result:         "a1b2c3d4e5f6789..." (64 hex chars)

Validation Rules:
  • Timestamp must be within 5 minutes of server time (prevents replay attacks)
  • Future timestamps accepted up to 1 minute ahead (clock skew tolerance)
  • Signature comparison uses constant-time comparison (prevents timing attacks)

---

Base URL​

The External API runs on port 3002:

Code:
https://your-server.com:3002/api/external/v1/

All endpoint paths in this guide are relative to this base URL.

---

Code Examples​

The API Management page includes ready-to-use code examples in 6 languages. Here are complete examples:

cURL (Bash):
Code:
#!/bin/bash
API_KEY="pk_live_your_key_here"
API_SECRET="sk_live_your_secret_here"
BASE_URL="https://your-server.com:3002/api/external"

METHOD="GET"
PATH="/v1/domains"
TIMESTAMP=$(date +%s)
BODY=""

STRING_TO_SIGN="${METHOD}${PATH}${TIMESTAMP}${BODY}"
SIGNATURE=$(echo -n "$STRING_TO_SIGN" | openssl dgst -sha256 -hmac "$API_SECRET" | cut -d' ' -f2)

curl -s -X "$METHOD" \
  -H "X-API-Key: $API_KEY" \
  -H "X-Timestamp: $TIMESTAMP" \
  -H "X-Signature: $SIGNATURE" \
  -H "Content-Type: application/json" \
  "${BASE_URL}${PATH}"

Python:
Code:
import hmac
import hashlib
import time
import requests

API_KEY = "pk_live_your_key_here"
API_SECRET = "sk_live_your_secret_here"
BASE_URL = "https://your-server.com:3002/api/external"

def make_request(method, path, body=""):
    timestamp = str(int(time.time()))
    string_to_sign = f"{method}{path}{timestamp}{body}"

    signature = hmac.new(
        API_SECRET.encode(),
        string_to_sign.encode(),
        hashlib.sha256
    ).hexdigest()

    headers = {
        "X-API-Key": API_KEY,
        "X-Timestamp": timestamp,
        "X-Signature": signature,
        "Content-Type": "application/json"
    }

    response = requests.request(
        method,
        f"{BASE_URL}{path}",
        headers=headers,
        data=body if body else None,
        verify=True
    )
    return response.json()

# List all domains
domains = make_request("GET", "/v1/domains")
print(domains)

# Create a domain
import json
body = json.dumps({"domain": "example.com", "php_version": "8.4"})
result = make_request("POST", "/v1/domains", body)
print(result)

Node.js:
Code:
const crypto = require('crypto');
const https = require('https');

const API_KEY = 'pk_live_your_key_here';
const API_SECRET = 'sk_live_your_secret_here';
const BASE_URL = 'https://your-server.com:3002/api/external';

function makeRequest(method, path, body = '') {
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const stringToSign = `${method}${path}${timestamp}${body}`;

  const signature = crypto
    .createHmac('sha256', API_SECRET)
    .update(stringToSign)
    .digest('hex');

  const url = new URL(`${BASE_URL}${path}`);

  const options = {
    hostname: url.hostname,
    port: url.port,
    path: url.pathname,
    method: method,
    headers: {
      'X-API-Key': API_KEY,
      'X-Timestamp': timestamp,
      'X-Signature': signature,
      'Content-Type': 'application/json'
    }
  };

  return new Promise((resolve, reject) => {
    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', (chunk) => data += chunk);
      res.on('end', () => resolve(JSON.parse(data)));
    });
    req.on('error', reject);
    if (body) req.write(body);
    req.end();
  });
}

// List domains
makeRequest('GET', '/v1/domains').then(console.log);

PHP:
Code:
<?php

class PanelicaAPI {
    private string $apiKey;
    private string $apiSecret;
    private string $baseUrl;

    public function __construct(string $apiKey, string $apiSecret, string $baseUrl) {
        $this->apiKey = $apiKey;
        $this->apiSecret = $apiSecret;
        $this->baseUrl = $baseUrl;
    }

    public function request(string $method, string $path, array $body = []): array {
        $timestamp = (string) time();
        $bodyJson = !empty($body) ? json_encode($body) : '';

        $stringToSign = $method . $path . $timestamp . $bodyJson;
        $signature = hash_hmac('sha256', $stringToSign, $this->apiSecret);

        $ch = curl_init($this->baseUrl . $path);
        curl_setopt_array($ch, [
            CURLOPT_CUSTOMREQUEST => $method,
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HTTPHEADER => [
                "X-API-Key: {$this->apiKey}",
                "X-Timestamp: {$timestamp}",
                "X-Signature: {$signature}",
                "Content-Type: application/json"
            ],
        ]);

        if ($bodyJson) {
            curl_setopt($ch, CURLOPT_POSTFIELDS, $bodyJson);
        }

        $response = curl_exec($ch);
        curl_close($ch);

        return json_decode($response, true);
    }
}

$api = new PanelicaAPI(
    'pk_live_your_key_here',
    'sk_live_your_secret_here',
    'https://your-server.com:3002/api/external'
);

// List domains
$domains = $api->request('GET', '/v1/domains');
print_r($domains);

// Create a domain
$result = $api->request('POST', '/v1/domains', [
    'domain' => 'example.com',
    'php_version' => '8.4'
]);
print_r($result);

Go:
Code:
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "io"
    "net/http"
    "strconv"
    "strings"
    "time"
)

const (
    apiKey    = "pk_live_your_key_here"
    apiSecret = "sk_live_your_secret_here"
    baseURL   = "https://your-server.com:3002/api/external"
)

func makeRequest(method, path, body string) (string, error) {
    timestamp := strconv.FormatInt(time.Now().Unix(), 10)
    stringToSign := method + path + timestamp + body

    mac := hmac.New(sha256.New, []byte(apiSecret))
    mac.Write([]byte(stringToSign))
    signature := hex.EncodeToString(mac.Sum(nil))

    var bodyReader io.Reader
    if body != "" {
        bodyReader = strings.NewReader(body)
    }

    req, _ := http.NewRequest(method, baseURL+path, bodyReader)
    req.Header.Set("X-API-Key", apiKey)
    req.Header.Set("X-Timestamp", timestamp)
    req.Header.Set("X-Signature", signature)
    req.Header.Set("Content-Type", "application/json")

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    result, _ := io.ReadAll(resp.Body)
    return string(result), nil
}

func main() {
    result, _ := makeRequest("GET", "/v1/domains", "")
    fmt.Println(result)
}

---

Permission Scopes​

API keys use granular scopes to control access. Each scope follows the pattern resource:action.

Wildcard Support:
  • domains:* --- All actions on domains (read, write, delete)
  • *:* --- Full access to everything (admin-level)

CategoryScopes
Accountsaccounts:read accounts:write accounts:delete accounts:*
Domainsdomains:read domains:write domains:delete domains:*
Databasesdatabases:read databases:write databases:delete databases:*
Emailemail:read email:write email:delete email:*
FTPftp:read ftp:write ftp:delete ftp:*
DNSdns:read dns:write dns:delete dns:*
SSLssl:read ssl:write ssl:*
Backupsbackups:read backups:write backups:restore backups:*
Bandwidthbandwidth:read bandwidth:*
Plansplans:read plans:write plans:*
Webhookswebhooks:read webhooks:write webhooks:delete webhooks:*
Serverserver:read server:*
Servicesservices:read services:start services:stop services:restart services:*
Filesfiles:read files:write files:delete files:*
Licenselicense:read license:*
CloudFlarecloudflare:read cloudflare:write cloudflare:delete cloudflare:*

Scope Presets:

PresetIncludesUse Case
Read OnlyAll :read scopesMonitoring, dashboards, reporting
StandardRead + Write (no delete)Most integrations
Full Access*:*Admin tools, automation
Billing IntegrationAccounts + Plans + BandwidthWHMCS, billing systems
Backup OnlyBackup read/write/restoreBackup automation scripts

---

Rate Limiting​

Each API key has a rate limit tier that controls request frequency:

TierRequests/MinuteRequests/HourBurst SizeBest For
Starter601,00010Development, testing
Professional30010,00050Small to medium apps
Business1,00050,000100High-traffic applications
EnterpriseUnlimitedUnlimited500Enterprise deployments

When you exceed the rate limit, the API returns 429 Too Many Requests with a Retry-After header indicating when you can retry.

Rate limits use a sliding window algorithm --- requests are counted over a rolling time period, not fixed intervals.

---

IP Whitelisting​

You can restrict API key access to specific IP addresses:

  • Add one or more IP addresses when creating or editing a key
  • If the whitelist is empty, all IPs are allowed
  • Requests from non-whitelisted IPs are rejected with 403 Forbidden
  • Useful for server-to-server integrations where the source IP is known

---

Managing API Keys​

Viewing Keys:
The API Keys table shows:
  • Name and description
  • Key prefix (masked: pk_live_a1b2...XXXX)
  • Status badge (Active / Revoked / Expired)
  • Assigned scopes
  • Rate limit tier
  • Expiration date
  • Last used date and IP
  • Total request count
  • Created date

Available Actions:

ActionDescription
EditUpdate name, description, scopes, rate limit tier, IP whitelist, expiry
Regenerate SecretGenerate a new secret (old one immediately invalidated)
RevokeDisable the key (can provide a reason). Revoked keys cannot be reactivated
DeletePermanently remove the key and all its logs
View LogsSee request history (method, path, status code, response time, IP)

---

Analytics Tab​

The Analytics tab provides usage insights:

  • Total Requests --- Overall API call count
  • Success Rate --- Percentage of 2xx responses
  • Average Response Time --- Mean latency in milliseconds
  • Active Keys --- Number of currently active API keys
  • Request Timeline --- Chart showing requests over time
  • Top Endpoints --- Most frequently called endpoints
  • Error Breakdown --- Distribution of error codes (4xx, 5xx)

---

Postman Integration​

Panelica can generate a Postman Collection automatically:

  1. Go to the Quick Start Guide tab
  2. Click Download Postman Collection
  3. Import the JSON file into Postman
  4. Set up environment variables (base_url, api_key, api_secret)
  5. The collection includes a pre-request script that handles HMAC signing automatically

Environment Variables:
Code:
base_url:   https://your-server.com:3002/api/external
api_key:    pk_live_YOUR_API_KEY_HERE
api_secret: sk_live_YOUR_API_SECRET_HERE

---

CLI Access​

Panelica also provides a command-line tool as an alternative to the API:

Code:
# Check server status
panelica server status

# List domains
panelica domain list

# Create a domain
panelica domain create --domain example.com --php 8.4

# Manage services
panelica service restart nginx
panelica service status all

The CLI uses the same backend and provides the same functionality. SSH Commands tab in the API Management page shows all available CLI commands.

---

Security Best Practices​

  1. Use minimum required scopes --- Don't use *:* unless absolutely necessary. Use domains:read instead of domains:* if you only need to list domains
  2. Set expiration dates --- Rotate keys regularly. Set 30/60/90 day expiry
  3. Use IP whitelisting --- Lock keys to known server IPs for production integrations
  4. Separate live and test keys --- Use pk_test_ keys for development
  5. Store secrets securely --- Never hardcode secrets in source code. Use environment variables or secret managers
  6. Monitor usage --- Check the Analytics tab regularly for unusual patterns
  7. Revoke unused keys --- Delete or revoke keys that are no longer needed
  8. Use HTTPS only --- The API enforces HTTPS. Never send requests over plain HTTP

---

Error Responses​

Status CodeMeaningCommon Cause
400Bad RequestInvalid request body or parameters
401UnauthorizedMissing or invalid authentication headers
403ForbiddenInsufficient scopes, IP not whitelisted, or key revoked
404Not FoundResource doesn't exist or not accessible
409ConflictResource already exists or concurrent modification
429Too Many RequestsRate limit exceeded
500Internal Server ErrorServer-side error (check logs)

All error responses include a JSON body:
Code:
{
  "error": "Insufficient permissions",
  "code": "FORBIDDEN",
  "details": "Required scope: domains:write"
}

---

Troubleshooting​

Problem: "Invalid signature" error
  • Check that you're building the string to sign correctly: METHOD + PATH + TIMESTAMP + BODY
  • Ensure the timestamp is current Unix seconds (not milliseconds)
  • Make sure the body in the signature matches the body sent in the request exactly
  • For DELETE requests, exclude the body from the signature
  • Verify your API Secret is correct (regenerate if unsure)

Problem: "Timestamp too old" error
  • Ensure your server's clock is synchronized (use NTP)
  • The timestamp must be within 5 minutes of the server's time
  • Check timezone --- use UTC Unix timestamp, not local time

Problem: "Key revoked" or "Key expired"
  • Check the key status in the API Keys tab
  • Create a new key if the old one has expired
  • Revoked keys cannot be reactivated --- create a new one

Problem: 429 Too Many Requests
  • Check the Retry-After header for when to retry
  • Implement exponential backoff in your client
  • Consider upgrading to a higher rate limit tier
  • Batch operations where possible to reduce request count

---

Related Topics​

 
Last edited:
Back
Top