At a glance
| Address | https://<your-server>:8443/api/v1 |
| Specification | GET /api/v1/openapi.json (OpenAPI 3.1, public) and in the panel: API keys › API documentation |
| Authentication | Authorization: Bearer kpk_… — personal API key of an administrator or reseller |
| Format | JSON, UTF-8 |
| Errors | {"error":{"code":"…","message":"…","field":"…","requestId":"…"}} |
| Pagination | ?limit= (1–500) and ?cursor=; the answer carries nextCursor while more pages follow |
| Idempotency | Idempotency-Key header on every POST, 24 hours |
| Limits | 300 requests per minute per key, X-RateLimit-* headers |
| Webhooks | events signed with HMAC-SHA256, retried up to 5 times |
| Compatibility | within /api/v1 fields and operations are only added, never removed or changed |
The API key
In the panel, as an administrator or reseller: API keys › Create key. The key
(kpk_…) is shown once and acts as its account:
- a reseller's key sees and changes only the reseller's clients and packages, within its quota;
- creating resellers needs an administrator's key;
- if the account is suspended or deleted, its keys stop working;
- every operation shows in the Activity log with the name of the key.
What you can do
| Area | Operations |
|---|---|
| Identity | GET /auth/me, POST /auth/login-link |
| Hosting accounts | list, create, read, change (email, package), delete, suspend, unsuspend, password, one-time login link |
| Resellers | list, create with a quota, change the quota |
| Packages | list, create, change, delete |
| Usage | disk used per account |
| Sites | list, create, read, PHP and additional domains, delete |
| Databases | list, create, users, passwords, delete |
| mail domains, mailboxes (create, quota, forwarding, password, delete) | |
| Webhooks | register, change, test, deliveries, delete |
The complete list with every field is in the OpenAPI document of the server.
Examples
curl
export KP=https://server1.example.com:8443/api/v1
export KEY=kpk_... # API keys › Create key
# an account with the password chosen by your system, safe to repeat
curl -sS -X POST "$KP/accounts" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: order-10452" \
-d '{"username":"mario","email":"mario@example.com","package":"pkg_base","passwordMode":"set","password":"A-Strong-Password-2026"}'
# suspension for an overdue invoice, then reactivation
curl -sS -X POST "$KP/accounts/mario/suspend" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" -d '{"reason":"Invoice 2026/118 overdue"}'
curl -sS -X POST "$KP/accounts/mario/unsuspend" -H "Authorization: Bearer $KEY"
# one-time login link (60 seconds) that opens the file manager
curl -sS -X POST "$KP/accounts/mario/login-link" -H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" -d '{"next":"/files"}'
PHP
<?php
$base = 'https://server1.example.com:8443/api/v1';
$key = getenv('KOAPANEL_API_KEY');
function kp(string $method, string $path, ?array $body = null, array $headers = []): array {
global $base, $key;
$ch = curl_init($base . $path);
$h = ['Authorization: Bearer ' . $key, 'Accept: application/json'];
foreach ($headers as $k => $v) { $h[] = "$k: $v"; }
if ($body !== null) { $h[] = 'Content-Type: application/json'; curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body)); }
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => $h, CURLOPT_TIMEOUT => 30]);
$raw = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$data = $raw === '' ? [] : json_decode($raw, true);
if ($code >= 400) { throw new RuntimeException($data['error']['message'] ?? "HTTP $code", $code); }
return $data ?? [];
}
// every account, page by page
$cursor = null;
do {
$page = kp('GET', '/accounts?limit=100' . ($cursor ? '&cursor=' . urlencode($cursor) : ''));
foreach ($page['accounts'] as $a) { echo $a['username'], ' ', $a['usage']['diskBytes'], "\n"; }
$cursor = $page['nextCursor'] ?? null;
} while ($cursor);
// a database for the customer's site
$db = kp('POST', '/databases', ['name' => 'mario_shop', 'site' => 'mario.example.com'], ['Idempotency-Key' => 'order-10452-db']);
echo $db['credentials']['user'], ' / ', $db['credentials']['password'], "\n";
Python
import os, requests
BASE = "https://server1.example.com:8443/api/v1"
S = requests.Session()
S.headers["Authorization"] = "Bearer " + os.environ["KOAPANEL_API_KEY"]
def kp(method, path, **kw):
r = S.request(method, BASE + path, timeout=30, **kw)
if r.status_code == 429:
raise RuntimeError(f"rate limit, retry in {r.headers['Retry-After']} s")
if r.status_code >= 400:
raise RuntimeError(r.json()["error"]["message"])
return r.json() if r.content else None
site = kp("POST", "/sites", json={"domain": "mario.example.com", "owner": "mario"},
headers={"Idempotency-Key": "order-10452-site"})
box = kp("POST", "/mailserver/domains/mario.example.com/mailboxes", json={"local": "info", "quotaMB": 2048})
print(box["address"], box.get("password")) # a generated password is shown once
Webhooks
Register the address of your system (API keys › Webhooks, or POST /webhooks) and
receive a POST request for every event:
| Event | When |
|---|---|
account.created / account.updated / account.deleted |
account created, changed (email, package), deleted |
account.suspended / account.unsuspended |
suspension and reactivation |
account.usage_over_quota |
disk used reaches the package limit (once per crossing) |
reseller.created |
new reseller |
site.created / site.deleted |
site created or deleted |
ping |
test from the panel |
The body is {"id":"evt_…","type":"account.suspended","createdAt":"…","data":{…}} with
the headers X-Koapanel-Event, X-Koapanel-Delivery and
X-Koapanel-Signature: t=<seconds>,v1=<signature>, where the signature is the hex
HMAC-SHA256 of "<t>.<body>" with the webhook secret (shown once, at creation). Answer
2xx within 10 seconds; otherwise the panel retries after 10 s, 1 min, 5 min, 30 min and
2 h. Administrators receive every event, resellers only their clients' events (and only
to public https:// addresses).
Verification in PHP:
$body = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_KOAPANEL_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $sig), $p); // t=…, v1=…
$ok = isset($p['t'], $p['v1']) && abs(time() - (int)$p['t']) < 300
&& hash_equals(hash_hmac('sha256', $p['t'] . '.' . $body, getenv('KOAPANEL_WEBHOOK_SECRET')), $p['v1']);
if (!$ok) { http_response_code(400); exit; }
$event = json_decode($body, true);
Verification in Python:
import hmac, hashlib, time
def verify(secret: str, header: str, body: bytes, tolerance=300) -> bool:
parts = dict(x.split("=", 1) for x in header.split(","))
if abs(time.time() - int(parts.get("t", 0))) > tolerance:
return False
mac = hmac.new(secret.encode(), parts["t"].encode() + b"." + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, parts.get("v1", ""))
Ready-made modules
- WHMCS 8.x and 9.x: official module (accounts, resellers, one-click login, licences for partners) — see the "WHMCS integration" page.
- Planned: modules for Blesta and HostBill, and FOSSBilling. Meanwhile these systems can use the API above (the operations are the same as the WHMCS module's).
Partner API: reselling Koapanel licences
For providers selling Koapanel licences to their own customers, with the WHMCS module
koapanel_license or any other billing system. It is not the panel API: it is the API of
the Koapanel console, with a partner key we give you.
| Base URL | https://console.koapanel.app/api/v1/partner |
| Specification | /api/v1/partner/openapi.json (OpenAPI 3.1, public) |
| Authentication | Authorization: Bearer kpp_… — partner key, shown once; rotation with 24 hours of overlap |
| Errors | {"error":{"code":"…","message":"…"}}, messages in Italian |
| Idempotency | Idempotency-Key on create (7 days); externalId unique among non-revoked licences |
| Limits | 120 requests per minute per key |
| Audit | every operation is logged with the partner, the key and the client (X-Panel-Client) |
| Operation | Request |
|---|---|
| Partner and enabled plans | GET /me, GET /plans |
| List licences | GET /licenses?externalId=&status=&page= |
| Create a licence | POST /licenses with plan, externalId, customer (name, email, country) |
| One licence with its activation code | GET /licenses/{id} |
| Suspend, reactivate, revoke | POST /licenses/{id}/suspend, /reactivate (or /unsuspend), /revoke (or /terminate) |
| Change plan | POST /licenses/{id}/plan with plan |
| Free from its server (to move it) | POST /licenses/{id}/release — at most 3 times in 30 days |
| New code | POST /licenses/{id}/reissue |
| Usage of the month, statements | GET /usage?month=YYYY-MM, GET /statements, GET /statements/{month} |
export KPP=kpp_... # the partner key
curl -sS -X POST https://console.koapanel.app/api/v1/partner/licenses \
-H "Authorization: Bearer $KPP" -H "Content-Type: application/json" \
-H "Idempotency-Key: order-10452" \
-d '{"plan":"pro","externalId":"order-10452","customer":{"name":"Bianchi Ltd","email":"it@bianchi.example","country":"GB"}}'
The answer carries license.id and license.token: the customer pastes the code in
Koapanel › Plan and licence, and the licence binds to that server. Partner licences never
show Koapanel's own plans and checkout.
Wholesale billing. Each licence costs the monthly partner price of its plan, pro rata
for its time in the month (plan changes count from the moment of the change; a suspended
licence is billed, a revoked one until the end of the day of revocation). At the start of
each month you receive the electronic invoice for the previous month, with the detail per
licence in GET /statements/{month}. To become a partner, write to us.
OpenAPI 3.1 specification of the Partner API · Panel OpenAPI on the demo