The REST API¶
Everything the dashboard does, the REST API does, with the same permissions and the same audit trail. It is at /api/v1 on the admin port, and it is described by an OpenAPI 3.1 document.
- API reference: every operation, rendered from the document this version ships.
https://<admin host>:9443/api/docson your own instance (an interactive reference), and/api/v1/openapi.json(the document).GET /api/v1/versionnames the product version, the API version (v1) and, for a container build, the commit.
Authentication¶
A script authenticates with an API token: Authorization: Bearer pdk_.... Create one under Settings, API tokens.

- A token has a name, an optional expiry and scopes: permission names such as
config:read. The drawer offers Read-only, All you can and None as starting points. - A token never exceeds its owner. Its effective permissions are the intersection of its scopes and the owner's current permissions, checked on every use. Demote the owner and the token shrinks with them.
- The secret is shown once. Paduraksa stores only a hash. A revoked or expired token stops working at once.
- A read-only token is not literally state-free. Every authenticated request records when the token was last used (at most once a minute), as a session records when it was last seen, so the token list can show it. It changes nothing else, and no read writes the config, the audit log or the vault.
- A token cannot manage tokens or sessions. Every
tokensandsessionsendpoint answers a token with403, so a leaked token cannot mint a longer life for itself. - A token's
pdk_prefix and checksum let secret scanners find a leaked one.
| Scope | Allows |
|---|---|
config:read, config:write |
Read or change hosts, routes, upstreams, profiles, rate limits, access lists, rules, sign-in providers, settings, drafts, revisions, import and rollback |
certificates:read, certificates:write |
The certificate inventory; add, issue, renew and delete certificates |
providers:read, providers:write |
ACME accounts, DNS providers and notification channels (never their secrets) |
users:read, users:write |
List users and roles; create, change and delete users |
tokens:own, tokens:all |
Your own tokens; every user's tokens |
sessions:all |
Every user's sessions |
audit:read |
The audit log |
vault:manage |
Secrets in the vault: store, replace, delete, never read |
simulate |
The request simulator |
logs:read |
The log files and the live tail |
metrics:read |
Counters, analytics and the Prometheus endpoint. Every role holds it |
backup:create |
A full backup. The Owner only |
Roles are sets of these. Owner holds everything. Admin holds config, certificates, providers, audit, simulate and the user list. Operator holds config, certificates, provider reads and simulate. Viewer reads and simulates. Auditor reads and holds audit. Only Owner holds users:write, vault:manage, tokens:all, sessions:all and backup:create.
A browser session (the dashboard's cookie) also works, but then every unsafe request must say where it comes from: send Origin. A token needs none of that.
Trust the admin certificate¶
The admin port's certificate is signed by Paduraksa's local CA. Give curl the root:
curl -k -o root.pem https://paduraksa.lan:9443/api/v1/ca/root.pem # once, then check its fingerprint
curl --cacert root.pem -H "Authorization: Bearer $TOKEN" https://paduraksa.lan:9443/api/v1/hosts
Check the fingerprint against GET /api/v1/ca or the Local CA page before you trust a root you fetched with -k.
Conventions¶
- Paths are kebab-case and collections are plural:
/api/v1/rate-limit-policies/{id}. An action on an object isPOST /{collection}/{id}/{verb}, such as/certificates/{id}/renew. - JSON members are camelCase. IDs are typed strings (
host_...,ups_...,rte_...). Times are ISO 8601 in UTC. Enums are strings. - Statuses. A create is
POSTto the collection:201. A change isPUT(replace) orPATCH:200. A delete is204. - Errors are RFC 9457 problem details (
application/problem+json) with atraceIdand a stable dottedcodesuch ashost.name.duplicate. A validation failure is422with anerrorslist of{ path, code, message }.
IDs are yours to choose¶
A create needs an id: a typed string, the prefix and an underscore, then 22 base-62 characters (^host_[0-9A-Za-z]{22}$). Use a time-ordered one, such as a UUIDv7 written in base 62 (the digits 0-9A-Za-z, in that order). This script makes one:
import os, sys, time
ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
def new_id(prefix):
ms = int(time.time() * 1000)
n = (ms << 80) | (0x7 << 76) | ((int.from_bytes(os.urandom(2)) & 0xFFF) << 64) | (0b10 << 62) | (int.from_bytes(os.urandom(8)) & ((1 << 62) - 1))
digits = ""
while n:
n, r = divmod(n, 62)
digits = ALPHABET[r] + digits
return f"{prefix}_{digits.rjust(22, '0')}"
print(new_id(sys.argv[1])) # python3 new-id.py host -> host_034wzZoNpxm3a53aJxIOBE
When you only want to describe the config and not mint IDs, use YAML import: an object without an id is matched by name or created.
Pagination¶
Every collection answers { "items": [...], "nextCursor": "..." } and takes limit (1 to 200, default 50) and cursor (what the previous page gave, opaque). nextCursor is null on the last page.
- Config and provider collections are ascending by ID.
- History (revisions, audit, notification history) is newest first.
- The exceptions are the log files (a directory listing), the recent access log (a bounded snapshot with
limitup to 1000 and no cursor) and the upstreams' live health.
curl --cacert root.pem -H "Authorization: Bearer $TOKEN" "https://paduraksa.lan:9443/api/v1/hosts?limit=50"
Changing config: If-Match, dryRun and message¶
Config objects (hosts, routes, upstreams, profiles, rate limits, access lists, rule sets, sign-in providers, the settings) carry a version.
- A
GETanswers withETag: "<version>". A change or delete of that object sends it back inIf-Match, as one quoted tag. Without it the answer is428. A stale tag is412, and the problem'sconflictsays what is current. A malformed tag, a list or*is400. A create needs none. - A route belongs to its host: changing or deleting a route takes the host's version in
If-Match. POST /revisions/{id}/rollbackandPOST /config/importtake the latest revision's ID the same way.?dryRun=trueruns the whole command and returns the plan (status: "dryRun", the warnings and the changes) without committing.?message=is the revision's note, up to 1000 characters.?force=trueoverrides the lock-out guard for the admin host.- A change that was stored but could not go live is
500withcode: config.publishFailed, the storedrevisionand the newETag. - Certificates, ACME accounts, DNS providers, notification channels, users and tokens carry no version: a change overwrites, and last write wins.
A change answers with its result: status (committed or dryRun), the revision, the publish outcome, the new version, warnings and changes.
Example: a service behind a host¶
A=https://paduraksa.lan:9443/api/v1
H="Authorization: Bearer $TOKEN"
UPS=$(python3 new-id.py ups); DST=$(python3 new-id.py dst); HOST=$(python3 new-id.py host); RTE=$(python3 new-id.py rte)
curl --cacert root.pem -H "$H" -H 'Content-Type: application/json' -X POST "$A/upstreams" -d "{
\"id\": \"$UPS\", \"name\": \"jellyfin\",
\"destinations\": [{ \"id\": \"$DST\", \"address\": \"http://jellyfin:8096\" }]
}"
BODY="{
\"id\": \"$HOST\", \"names\": [\"jellyfin.example.com\"],
\"routes\": [{ \"id\": \"$RTE\", \"upstreamId\": \"$UPS\", \"match\": \"prefix\", \"path\": \"/\" }]
}"
curl --cacert root.pem -H "$H" -H 'Content-Type: application/json' -X POST "$A/hosts?dryRun=true" -d "$BODY" # the plan
curl --cacert root.pem -H "$H" -H 'Content-Type: application/json' -X POST "$A/hosts?message=add+jellyfin" -d "$BODY" # for real
To change it later, read it, send it back with the version:
curl --cacert root.pem -D - -H "$H" "$A/hosts/$HOST" -o host.json # ETag: "2"
# edit host.json ...
curl --cacert root.pem -H "$H" -H 'If-Match: "2"' -H 'Content-Type: application/json' -X PUT "$A/hosts/$HOST" -d @host.json
Request bodies¶
JSON bodies are limited to 64 KiB by default (Paduraksa__Admin__MaxRequestBodyBytes). A certificate upload and a config import raise their own limits. Bodies are strict: an unknown member is an error.
Drafts¶
/api/v1/drafts is the draft the dashboard shows. A token acts on its owner's draft: add entries with POST /drafts/{id}/entries, review with POST /drafts/{id}/review and apply with POST /drafts/{id}/apply with the review's token in If-Match. If the config changed since, apply is refused with 412 and draft.changedSinceReview, and the fresh review comes back. See Hosts and routes.
Streams and files¶
GET /api/v1/logs/tailstreams requests as they end as Server-Sent Events:curl -N -H "$H" "$A/logs/tail?statusClass=5xx&backlog=50".GET /api/v1/logs/{name}downloads a log file, andPOST /api/v1/backupmakes a backup.GET /metrics(the same as/api/v1/metrics) is the Prometheus text format: see Metrics.