Developers

One registry. Three ways in.

Every action the panel can take is a typed operation in a closed registry: its own request type, its own validator, its own authoriser, its own audit entry. The CLI and the HTTP API call that same registry. Nothing the interface can do is unavailable to a script, and nothing a script can do skips a check the interface performs.

Authentication

A bearer token, narrowed to what the script needs.

klyrn user token deploy@example.com --name "CI deploys" --days 90 --scopes deploy,jobs

The token is printed once. --days 0 means it never expires. Omitting --scopes gives the token its owner's full authority.

curl -sS https://panel.example.com:7443/api/v1/sites \
  -H 'Authorization: Bearer klyrn_<token>'
transport

HTTPS on 7443, TLS 1.2 or better

The panel has its own listener and never shares one with a customer site. Every API path is under /api/v1/. Responses are JSON with Cache-Control: no-store.

strictness

An unknown field is a 400

Request bodies are decoded with unknown fields disallowed and capped at 1 MB, so a typo in a key name is an error rather than something silently ignored. The site id in the path always wins over one in the body: a body cannot redirect a write to another site.

errors

A code, a message and a hint

Failures are {"error": {…}} with a machine code (invalid, unauthenticated, forbidden, not_found, conflict, rate_limited, unavailable) mapped to 400, 401, 403, 404, 409, 429 and 503.

Scopes

Enforced in core, not in the interface.

A scope is checked in the privileged process before the role is, on every call. The web tier's opinion is never trusted. A scope matches by operation-name prefix, and a read-only scope matches only operations registered as non-mutating.

The nine scopes
ScopeCoversReads only
readEvery operation that changes nothingYes
sitesSites, their domains, redirects and DNS records, scheduled tasks, and WordPressNo
filesThe file managerNo
databasesDatabases, their users, exports and importsNo
backupsBackups and server recoveryNo
deployApplication deploys and restarts, and WordPress stagingNo
migrateImports, the .htaccess analyser and the verification reportNo
jobsReading jobs and healthYes
adminEverything the token's owner can doNo

A token with no scopes carries its owner's full authority. That is the behaviour for tokens created before scopes existed, and it is why the CLI prints the scope list when you create one. A scope never widens what the owner may do: it can only narrow it. A customer's token with admin is still a customer.

The job model

Anything slow answers with a job.

Creating a backup, restoring one, deploying, pushing staging, importing a database, deleting an account, recovering a server: each returns a job immediately, with an ordered list of steps that was written down before the first one ran.

POST /api/v1/sites/12/backups        → 200 { "id": 118, "status": "queued", … }

GET  /api/v1/jobs/118                → status, progress, steps
GET  /api/v1/jobs/118/log?after=42   → log lines after sequence 42

A job carries id, kind, title, status, progress from 0 to 100, its steps, and an error if it failed. Log lines carry a monotonic seq, so polling with after never repeats a line and never skips one.

  1. queuedwaiting for a slotbackups, migrations and system jobs run one at a time; site jobs three
  2. runninga step is executing
  3. succeededevery step finished
  4. failedthe failing step carries the errorevery later step is recorded as cancelled, not omitted
  5. cancelledthe step did not run

A restart of klyrn-core marks anything still running as failed with "interrupted: klyrn-core restarted". Resumable jobs are a later milestone; being honest about interruption is the behaviour today.

Webhooks

Signed, so a system finds out and not just a person.

An endpoint must be https://: the signature protects the body, not the network. Private, loopback and cloud-metadata addresses are refused, and the resolved address is checked again at connect time so a name that resolves to something internal later is still refused.

Every delivery carries

X-Klyrn-Signature
t=<unix>,v1=<hex>
X-Klyrn-Event
The event name.
X-Klyrn-Delivery
A unique id. Use it for idempotency.
User-Agent
KLYRN-Webhook/1
Method and type
Always POST, application/json.

The signed string is <t>.<body>: the same decimal timestamp that appears in the header, a literal dot, then the exact bytes of the request body. HMAC-SHA256, keyed with the endpoint secret including its whsec_ prefix, hex encoded in lower case.

verify.js Node, no dependencies
import { createHmac, timingSafeEqual } from 'node:crypto'

export function verify(secret, header, rawBody, toleranceSec = 300) {
  const parts = Object.fromEntries(
    header.split(',').map((p) => p.trim().split('=')),
  )
  const t = Number(parts.t)
  if (!Number.isFinite(t) || !parts.v1) return false

  // Reject a replay before spending a hash on it.
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false

  const want = createHmac('sha256', secret)
    .update(parts.t + '.')
    .update(rawBody)
    .digest('hex')

  const a = Buffer.from(parts.v1, 'utf8')
  const b = Buffer.from(want, 'utf8')
  return a.length === b.length && timingSafeEqual(a, b)
}
rawBody must be the exact bytes received. Re-serialising the parsed JSON changes them.

The thirteen events

backup.completed · backup.failed · backup.remote_failed
ssl.issued · ssl.failed
health.critical
site.created · site.suspended · site.unsuspended
update.installed
job.failed
migration.completed · recovery.completed

Subscribing to * expands to all of them. A name that is not on this list is refused when you create the endpoint, rather than accepted and never delivered.

What a payload holds

id, event, at, a server block with the hostname, install id and version, an optional subject, and a flat data map. No customer content is ever included: a webhook says a backup failed, not what was in it.

Delivery and retries

Success
Any 2xx. Timeout is 15 seconds.
Retried
A transport error, 408, 429, or any 5xx.
Schedule
Six attempts: after 30 s, 2 min, 10 min, 30 min and 1 h, then it stops.
Statuses
pending, delivered, retrying, failed.
History
14 days or 500 deliveries, whichever comes first.
Endpoints
Up to 20 per server.

The secret is shown once when you create the endpoint and once when you rotate it; after that only its last four characters. Rotating invalidates the old secret immediately. There is a test delivery, so you can prove your verification works before you need it.

Choose your own replay tolerance. KLYRN signs with a timestamp and does not tell you how old is too old, because that is the receiver's decision. Five minutes is the value our own tests use. Reject anything outside your window before you verify the hash.

The surface

What the API reaches.

A representative slice, not the whole list. klyrn ops on the server prints the registry itself, which is the one index that cannot drift from what the server can actually do.

Endpoints under https://<panel>:7443/api/v1/
AreaEndpointsScope
Sites GET /sites · POST /sites · GET /sites/{id} · PATCH /sites/{id} · DELETE /sites/{id} · POST /sites/{id}/suspend · POST /sites/{id}/ssl · GET /sites/{id}/logs sites
Diagnostics GET /sites/{id}/diagnose · POST /sites/{id}/repair sites
Domains and DNS GET /sites/{id}/domains · POST /sites/{id}/domains · PATCH /domains/{id} · GET /sites/{id}/dns · POST /sites/{id}/dns/records · POST /sites/{id}/dns/point sites
Staging GET /sites/{id}/staging · POST /sites/{id}/staging · POST /sites/{id}/staging/refresh · POST /sites/{id}/staging/push deploy
Applications POST /sites/{id}/deploy · POST /sites/{id}/rollback · PUT /sites/{id}/env · POST /sites/{id}/app/{action} · GET /sites/{id}/app/logs deploy
Files GET /sites/{id}/files · PUT /sites/{id}/files/content · POST /sites/{id}/files/{mkdir,rename,delete,chmod,extract,copy,move,compress} · POST /sites/{id}/files/upload files
Databases GET /databases · POST /databases · POST /databases/{id}/export · POST /databases/{id}/import · GET /databases/exports databases
Backups GET /backups · POST /sites/{id}/backups · PUT /sites/{id}/backups/schedule · POST /backups/{id}/restore backups
Recovery GET /recovery · POST /recovery/run · POST /recovery/manifest · GET /backups/keys backups, administrator
Migration POST /imports/upload · POST /imports/ssh · POST /imports/{id}/apply · GET /sites/{id}/htaccess · GET /sites/{id}/migration/verify migrate
Logs GET /sites/{id}/logs/sources · GET /sites/{id}/logs/read · GET /server/logs/read (administrator) read
Jobs GET /jobs · GET /jobs/{id} · GET /jobs/{id}/log jobs or read
Webhooks GET /webhooks · POST /webhooks · POST /webhooks/{id}/test · POST /webhooks/{id}/rotate · GET /webhooks/{id}/deliveries administrator

Administrator-only areas (health, updates, licensing, PHP extensions, backup keys, recovery, webhooks and the audit log) need a token whose owner is an administrator. A scope cannot grant what the owner does not have.

On the server

The CLI, when the panel is the thing that is broken.

klyrn talks to the privileged core over the local socket, so it works whether or not the web tier is running, which is exactly when you want it.

The full CLI reference

klyrn status
klyrn health --events
klyrn ops                       # the operation registry itself

klyrn site create example.com --type wordpress --admin-email you@example.com
klyrn site diagnose example.com --repair php.pool.restart
klyrn site staging push example.com --confirm example.com

klyrn backup create example.com
klyrn recover discover
klyrn dns point example.com

Why it is shaped like this

There is no endpoint that runs a command.

closed registry

Operations, not commands

No operation takes a command string or an unconstrained path. Programs get argument lists, never a shell, and a CI check forbids process execution anywhere else in the codebase. A repair has no field that could carry an argument, which is why there are exactly six repairs and not a text box.

one door

The panel is a client

The interface holds no privileged path of its own. It signs in, gets a session, and calls the same operations your script calls. That is why an API can be complete without a second implementation drifting behind the UI.

audited

Every write, and some reads

Actor, address, operation, target and outcome. Reads are audited too when what they return is a secret: revealing a site's credentials, exporting a backup key and downloading a file are all recorded, because the interesting question about those is who asked.

Beta

One email when the paid editions go on sale.

Every endpoint, scope and signature on this page is in the beta you can install today, free. This list is for the paid editions and the first stable release.

One email when pricing and the first release are announced. Nothing else.