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.
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
---
Go to Developer > API Management in the panel navigation.
The page has 6 tabs:
| Tab | Description |
|---|---|
| API Endpoints | Browse all available External API endpoints with documentation |
| API Keys | Create and manage your API keys |
| SSH Commands | CLI command examples for terminal-based access |
| Analytics | Usage metrics, charts, success/failure rates |
| Quick Start Guide | Step-by-step integration guide with code examples |
| Mobile Pairing | QR 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.
---
Step 1: Go to the API Keys tab and click Create API Key.
Step 2: Fill in the form:
| Field | Required | Description |
|---|---|---|
| Name | Yes | A descriptive name (e.g., "Monitoring Script", "WHMCS Integration") |
| Description | No | Optional notes about what this key is used for |
| Scopes | Yes | Permission scopes --- what this key can access (minimum 1) |
| Rate Limit Tier | No | Request rate limit: Starter, Professional, Business, or Enterprise |
| IP Whitelist | No | Restrict access to specific IP addresses (empty = allow all) |
| Expires In | No | Days until expiration (0 = never expires) |
| Environment | No | "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.
---
| Credential | Format | Length | Example |
|---|---|---|---|
| API Key | pk_{env}_{random} | ~40 chars | pk_live_a1b2c3d4e5f6g7h8i9j0... |
| API Secret | sk_{env}_{random} | ~56 chars | sk_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
---
Every API request must include 3 headers:
| Header | Value | Example |
|---|---|---|
X-API-Key | Your API key | pk_live_a1b2c3d4... |
X-Timestamp | Current Unix timestamp (seconds) | 1709827200 |
X-Signature | HMAC-SHA256 signature (hex) | a1b2c3d4e5f6... (64 chars) |
How the Signature is Generated:
- Build the string to sign:
METHOD + PATH + TIMESTAMP + BODY - For DELETE requests, the body is excluded
- Compute HMAC-SHA256 using your API Secret as the key
- 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)
---
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.
---
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)
}
---
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)
| Category | Scopes |
|---|---|
| Accounts | accounts:read accounts:write accounts:delete accounts:* |
| Domains | domains:read domains:write domains:delete domains:* |
| Databases | databases:read databases:write databases:delete databases:* |
email:read email:write email:delete email:* | |
| FTP | ftp:read ftp:write ftp:delete ftp:* |
| DNS | dns:read dns:write dns:delete dns:* |
| SSL | ssl:read ssl:write ssl:* |
| Backups | backups:read backups:write backups:restore backups:* |
| Bandwidth | bandwidth:read bandwidth:* |
| Plans | plans:read plans:write plans:* |
| Webhooks | webhooks:read webhooks:write webhooks:delete webhooks:* |
| Server | server:read server:* |
| Services | services:read services:start services:stop services:restart services:* |
| Files | files:read files:write files:delete files:* |
| License | license:read license:* |
| CloudFlare | cloudflare:read cloudflare:write cloudflare:delete cloudflare:* |
Scope Presets:
| Preset | Includes | Use Case |
|---|---|---|
| Read Only | All :read scopes | Monitoring, dashboards, reporting |
| Standard | Read + Write (no delete) | Most integrations |
| Full Access | *:* | Admin tools, automation |
| Billing Integration | Accounts + Plans + Bandwidth | WHMCS, billing systems |
| Backup Only | Backup read/write/restore | Backup automation scripts |
---
Each API key has a rate limit tier that controls request frequency:
| Tier | Requests/Minute | Requests/Hour | Burst Size | Best For |
|---|---|---|---|---|
| Starter | 60 | 1,000 | 10 | Development, testing |
| Professional | 300 | 10,000 | 50 | Small to medium apps |
| Business | 1,000 | 50,000 | 100 | High-traffic applications |
| Enterprise | Unlimited | Unlimited | 500 | Enterprise 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.
---
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
---
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:
| Action | Description |
|---|---|
| Edit | Update name, description, scopes, rate limit tier, IP whitelist, expiry |
| Regenerate Secret | Generate a new secret (old one immediately invalidated) |
| Revoke | Disable the key (can provide a reason). Revoked keys cannot be reactivated |
| Delete | Permanently remove the key and all its logs |
| View Logs | See request history (method, path, status code, response time, IP) |
---
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)
---
Panelica can generate a Postman Collection automatically:
- Go to the Quick Start Guide tab
- Click Download Postman Collection
- Import the JSON file into Postman
- Set up environment variables (
base_url,api_key,api_secret) - 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
---
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.
---
- Use minimum required scopes --- Don't use
*:*unless absolutely necessary. Usedomains:readinstead ofdomains:*if you only need to list domains - Set expiration dates --- Rotate keys regularly. Set 30/60/90 day expiry
- Use IP whitelisting --- Lock keys to known server IPs for production integrations
- Separate live and test keys --- Use
pk_test_keys for development - Store secrets securely --- Never hardcode secrets in source code. Use environment variables or secret managers
- Monitor usage --- Check the Analytics tab regularly for unusual patterns
- Revoke unused keys --- Delete or revoke keys that are no longer needed
- Use HTTPS only --- The API enforces HTTPS. Never send requests over plain HTTP
---
| Status Code | Meaning | Common Cause |
|---|---|---|
| 400 | Bad Request | Invalid request body or parameters |
| 401 | Unauthorized | Missing or invalid authentication headers |
| 403 | Forbidden | Insufficient scopes, IP not whitelisted, or key revoked |
| 404 | Not Found | Resource doesn't exist or not accessible |
| 409 | Conflict | Resource already exists or concurrent modification |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Server-side error (check logs) |
All error responses include a JSON body:
Code:
{
"error": "Insufficient permissions",
"code": "FORBIDDEN",
"details": "Required scope: domains:write"
}
---
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-Afterheader 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
---
Last edited: