PanelicaDocs
panelica.com
Docs / Developer / REST API

REST API

Panelica 1.0.375 Verified 2026-07-24 Developer

Panelica exposes almost everything the panel can do as a REST API, so you can automate provisioning, billing integrations, monitoring and migrations from your own code. This page is the developer's reference: how the API is structured, how to create a key, how request signing works, and complete copy-paste examples in five languages.

Panel locationDeveloper → APIhttps://YOUR-SERVER-IP:8443/developer/api (ROOT only)
 API Management
Endpoints
246
API Keys
3
Categories
14
Status
Online
GET /v1/domains · API Key + Secret · rate-limited

Two API layers, one thing to understand

Panelica actually runs two separate APIs, and this page touches both:

  • The Internal API (port 3001, behind the panel) uses your logged-in session (a JWT). The panel's own UI runs on it, and it is where you manage API keys (create, revoke, list).
  • The External API (port 3002, reached through the panel's HTTPS port at /api/external) uses HMAC signing with the keys you create here. This is the one your scripts and integrations call.

So the workflow is: create a key in the UI (Internal API), then use that key to sign requests to the External API. The page has six tabs, Endpoints (browse + live-test every endpoint), API Keys (create and manage), SSH Commands (a CLI cheat-sheet), Analytics (usage and logs), Quick Start (a signed-request tutorial), and Mobile (pairing).

Base URL: your integrations call https://YOUR-SERVER:8443/api/external/v1/.... The panel's nginx strips the /api/external prefix and forwards to the External API on 3002, so the path you sign is the part after /api/external, for example /v1/domains. This matters, sign the wrong path and the signature will not match.

Creating an API key

Open the API Keys tab and click Generate New Key. The dialog:

  • Key Name (required) and an optional Description.
  • Environment, live or test. This only sets the key prefix (pk_live_ / pk_test_) so you can tell production and staging keys apart.
  • Rate Limit Tier, one of four budgets (below).
  • Expiration, never, 30, 90 or 365 days.
  • Permissions (scopes), tick exactly what this key may do (the full list is below). A key must have at least one scope.
  • IP Whitelist, an optional comma-separated list of IPs allowed to use the key. Empty means any IP.

When you create the key, the panel shows the secret exactly once, copy it now, it is never shown again (only a hash is stored). The dialog even gives you ready-made cURL, Python, Node.js, PHP, Go and Postman snippets with your real key and secret already filled in.

The secret is shown once. Panelica stores only a SHA-256 hash for lookup and an encrypted copy for signature verification, it cannot show you the secret again. If you lose it, use Regenerate Secret on the key (the key ID stays the same, only the secret changes) and update your integration.

The scopes

Scopes are resource:action. Tick only what you need, least privilege keeps a leaked key from doing damage. The picker groups them:

Resource Scopes
Full access *:* (everything, use sparingly)
Accounts accounts:read, accounts:write, accounts:delete
Domains domains:read, domains:write, domains:delete
Databases databases:read, databases:write, databases:delete
DNS dns:read, dns:write, dns:delete
Email email:read, email:write, email:delete
FTP ftp:read, ftp:write, ftp:delete
SSL ssl:read, ssl:write
Backups backups:read, backups:write, backups:restore
Monitoring bandwidth:read, server:read
Plans plans:read, plans:write
Webhooks webhooks:read, webhooks:write, webhooks:delete
Files files:read, files:write, files:delete

A read endpoint needs the :read scope, a create/update needs :write, a delete needs :delete. A key with *:* passes every check. If a key lacks the scope an endpoint needs, the API returns 403 with the required scope named in the body.

The rate-limit tiers

Every key belongs to a tier; the External API enforces it with a Redis sliding window and returns X-RateLimit-* headers on every response (and 429 when you exceed it):

Tier Requests / minute Requests / hour Burst
Starter (default) 60 1,000 10
Professional 300 10,000 50
Business 1,000 50,000 100
Enterprise unlimited unlimited 500

How request signing works

Every External API call carries three headers:

  • X-API-Key, your public key (pk_live_...).
  • X-Timestamp, the current Unix time in seconds.
  • X-Signature, an HMAC-SHA256 hex digest.

The signature is computed over the request method, path, timestamp and body concatenated, keyed with your secret:

signature = HMAC_SHA256( METHOD + PATH + TIMESTAMP + BODY, SECRET )   → hex

Rules that trip people up:

  • PATH is the path you actually hit after /api/external, including the query string, e.g. /v1/domains?limit=10.
  • BODY is the raw request body (the JSON string you send). For a DELETE the body is treated as empty in the signature even if you send one.
  • TIMESTAMP must be within 5 minutes of the server's clock, older or more than a minute in the future is rejected (TIMESTAMP_EXPIRED / TIMESTAMP_FUTURE). Keep your client's clock in sync.
  • The signature is compared in constant time, and it is never logged.

Complete working examples

Each of these is a full, runnable signed request. Set your key, secret and server, then call an endpoint.

cURL (bash) — a helper that signs and calls:

#!/usr/bin/env bash
API_KEY="pk_live_xxxxxxxxxxxxxxxx"
API_SECRET="sk_live_xxxxxxxxxxxxxxxxxxxxxxxx"
BASE="https://YOUR-SERVER:8443/api/external"

call() {
  local method="$1" path="$2" body="${3:-}"
  local ts; ts=$(date +%s)
  local sig; sig=$(printf '%s' "${method}${path}${ts}${body}" \
    | openssl dgst -sha256 -hmac "$API_SECRET" | awk '{print $2}')
  curl -sS -X "$method" "${BASE}${path}" \
    -H "X-API-Key: $API_KEY" \
    -H "X-Timestamp: $ts" \
    -H "X-Signature: $sig" \
    -H "Content-Type: application/json" \
    ${body:+--data "$body"}
}

call GET  /v1/domains
call POST /v1/domains '{"domain_name":"example.com","user_id":"...","php_version":"8.3"}'

PHP:

<?php
class Panelica {
  public function __construct(
    private string $base   = 'https://YOUR-SERVER:8443/api/external',
    private string $key    = 'pk_live_xxxx',
    private string $secret = 'sk_live_xxxx',
  ) {}

  public function call(string $method, string $path, ?array $body = null): array {
    $ts   = (string) time();
    $json = $body === null ? '' : json_encode($body);
    $sig  = hash_hmac('sha256', $method . $path . $ts . $json, $this->secret);
    $ch = curl_init($this->base . $path);
    curl_setopt_array($ch, [
      CURLOPT_CUSTOMREQUEST  => $method,
      CURLOPT_RETURNTRANSFER => true,
      CURLOPT_HTTPHEADER     => [
        "X-API-Key: {$this->key}",
        "X-Timestamp: {$ts}",
        "X-Signature: {$sig}",
        'Content-Type: application/json',
      ],
      CURLOPT_POSTFIELDS => $json ?: null,
    ]);
    return json_decode(curl_exec($ch), true);
  }
}

$api = new Panelica();
print_r($api->call('GET', '/v1/domains'));

Python:

import time, hmac, hashlib, json, requests

BASE   = "https://YOUR-SERVER:8443/api/external"
KEY    = "pk_live_xxxx"
SECRET = "sk_live_xxxx"

def call(method, path, body=None):
    ts   = str(int(time.time()))
    data = json.dumps(body) if body is not None else ""
    sig  = hmac.new(SECRET.encode(), (method + path + ts + data).encode(),
                    hashlib.sha256).hexdigest()
    return requests.request(
        method, BASE + path,
        headers={"X-API-Key": KEY, "X-Timestamp": ts,
                 "X-Signature": sig, "Content-Type": "application/json"},
        data=data or None,
    ).json()

print(call("GET", "/v1/domains"))
print(call("POST", "/v1/email-accounts",
           {"email": "[email protected]", "password": "S3cret!", "quota": 2048}))

Node.js:

import crypto from "node:crypto";

const BASE = "https://YOUR-SERVER:8443/api/external";
const KEY = "pk_live_xxxx", SECRET = "sk_live_xxxx";

async function call(method, path, body = null) {
  const ts = Math.floor(Date.now() / 1000).toString();
  const data = body ? JSON.stringify(body) : "";
  const sig = crypto.createHmac("sha256", SECRET)
    .update(method + path + ts + data).digest("hex");
  const res = await fetch(BASE + path, {
    method,
    headers: {
      "X-API-Key": KEY, "X-Timestamp": ts,
      "X-Signature": sig, "Content-Type": "application/json",
    },
    body: data || undefined,
  });
  return res.json();
}

console.log(await call("GET", "/v1/domains"));

Go:

package main

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

const base, key, secret = "https://YOUR-SERVER:8443/api/external", "pk_live_xxxx", "sk_live_xxxx"

func call(method, path, body string) string {
	ts := strconv.FormatInt(time.Now().Unix(), 10)
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(method + path + ts + body))
	sig := hex.EncodeToString(mac.Sum(nil))

	req, _ := http.NewRequest(method, base+path, bytes.NewBufferString(body))
	req.Header.Set("X-API-Key", key)
	req.Header.Set("X-Timestamp", ts)
	req.Header.Set("X-Signature", sig)
	req.Header.Set("Content-Type", "application/json")
	res, _ := http.DefaultClient.Do(req)
	defer res.Body.Close()
	out, _ := io.ReadAll(res.Body)
	return string(out)
}

func main() { fmt.Println(call("GET", "/v1/domains", "")) }

Browsing and testing endpoints

The Endpoints tab loads the live API specification straight from the server (no two docs to drift apart) and lists every endpoint grouped by category, each with its method, path, description, required auth, rate limit and parameters. A search box and a category filter narrow it down, and there is a role simulator so ROOT/ADMIN can preview which endpoints each role would see.

Every endpoint row has a Test button that opens a live playground: it pre-fills a temporary signed credential, lets you set path/query parameters and a JSON body, sends the real request, and shows the status, response body and round-trip time. It is the fastest way to confirm a call before you write the code.

Postman: the create-key dialog and Quick Start tab both emit a Postman pre-request script that signs requests for you. Remember Postman must sign the path without the /api/external prefix, the same path the server sees after nginx rewrites it.

Managing keys

Each key in the list shows its name, tier, masked key, status, scopes, usage count and last-used time, created and expiry dates. Per key you can:

  • Copy the public key.
  • Edit, change the name, description, tier, scopes and IP whitelist.
  • Regenerate Secret, issue a new secret (the key ID is unchanged); the new secret is shown once.
  • Revoke, disable the key immediately (a soft state, reversible in intent but the key stops working).
  • Delete, remove it permanently.

The Analytics tab charts request volume in two-hour buckets and lists recent API requests (filterable to external calls), with a per-request detail view (IP, user-agent, status, response time). Usage is tracked per key and both successful and failed authentications are recorded.

WebSocket endpoints

Two streaming endpoints can't carry HMAC headers in a browser WebSocket handshake, so they use a short-lived ticket instead: POST /v1/metrics/ws-ticket (or /v1/terminal/ws-ticket) with a normal signed request returns a ticket you then pass to the wss://.../v1/metrics/ws connection. The terminal socket additionally requires a full-access (*:*) key.

Access

The API Management page is a licensed feature (api_access, PRO and up) and is ROOT-only; if High Security Mode is on, it also asks for 2FA. Key management (create/revoke/list) respects the role chain, ROOT and ADMIN see all keys, other roles only their own. The keys you create then carry their own scopes, which are what actually gate each External API call.

Panelica Documentation · Written and verified against the live panel. · Last verified 2026-07-24 on Panelica 1.0.375