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>'
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.
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.
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.
| Scope | Covers | Reads only |
|---|---|---|
read | Every operation that changes nothing | Yes |
sites | Sites, their domains, redirects and DNS records, scheduled tasks, and WordPress | No |
files | The file manager | No |
databases | Databases, their users, exports and imports | No |
backups | Backups and server recovery | No |
deploy | Application deploys and restarts, and WordPress staging | No |
migrate | Imports, the .htaccess analyser and the verification report | No |
jobs | Reading jobs and health | Yes |
admin | Everything the token's owner can do | No |
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.
- queuedwaiting for a slotbackups, migrations and system jobs run one at a time; site jobs three
- runninga step is executing
- succeededevery step finished
- failedthe failing step carries the errorevery later step is recorded as cancelled, not omitted
- 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-Signaturet=<unix>,v1=<hex>X-Klyrn-Event- The event name.
X-Klyrn-Delivery- A unique id. Use it for idempotency.
User-AgentKLYRN-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.
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.
| Area | Endpoints | Scope |
|---|---|---|
| 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.
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.
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.
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.
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.