Skip to content

Script the API

Everything the Console UI does is an HTTP call, and the automation subset of it is published as /api/v1: a stable, additive-only surface for your own scripts and pipelines. (Since v0.4.1.)

  • Base URL: your Console, for example https://console.dednets.com.
  • Auth: Authorization: Bearer dak_… with a scoped token (API tokens and scopes).
  • Format: JSON in. Reads answer JSON; some writes answer a short plain-text confirmation (exported, assigned, revoked, ok) rather than a JSON body, so do not pipe every response into a JSON parser. Every error is the JSON envelope described below.
Terminal window
curl -fsS -H "Authorization: Bearer dak_your_token_here" \
https://console.dednets.com/api/v1/topology

Paths below are relative to /api/v1.

Method + path Purpose Scope
GET /ca the Console’s CA certificate (public, no auth) none
GET /info bootstrap document: version, control port, domain, CA fingerprint read, hosts:manage, services:manage, watchdogs:manage
GET /limits your quota limits and current usage read, hosts:manage, services:manage, watchdogs:manage
GET /topology the whole account view: hosts, apps, proxy ports, gateways, routes, exposures read, hosts:manage, services:manage
GET /services/uptime bucketed availability per service read, services:manage, watchdogs:manage

A token carrying only the Notifications (notify) scope reaches none of the authenticated routes above: it is for the notifications API, and /api/v1 answers it with scope_denied.

Method + path Purpose Scope
POST /tokens mint a single-use join token for a new or existing host hosts:manage
DELETE /daemons?daemon= revoke a host and everything it published hosts:manage
POST /daemons/update pin a host’s version, opt it out, or apply an update hosts:manage
Method + path Purpose Scope
GET /exports published apps read, services:manage
POST / DELETE /exports publish or unpublish an app services:manage
POST / DELETE /proxy-ports a local port on one host reaching an app on another services:manage
GET /gateways, POST / DELETE /gateways publish on a host you own read for the list, services:manage to change
GET /exposures, POST / DELETE /exposures raw TCP/UDP ports on the DedNets edge read for the list, services:manage to change
GET /health-checks, POST / DELETE /health-checks per-app health-check overrides read for the list, services:manage to change
Method + path Purpose Scope
GET /watchdogs list monitors with their derived status read, watchdogs:manage
POST /watchdogs create a monitor watchdogs:manage
PATCH / DELETE /watchdogs/{id} edit or delete watchdogs:manage
POST /watchdogs/{id}/pause, .../resume pause or resume watchdogs:manage
GET /watchdogs/{id}/history uptime buckets and the outage list read, watchdogs:manage
Method + path Purpose Scope
GET / POST / DELETE /apitokens manage API tokens signed-in session only, not available to API tokens
Terminal window
curl -fsS -X POST -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"daemon":"vm1","ttl_seconds":3600}' \
https://console.dednets.com/api/v1/tokens
# -> {"token":"...","daemon":"alice-vm1"}

daemon in the reply is the name the token actually binds: for a new host the Console derives <username>-<host>. The token is single-use. Then run the install command on the machine itself: see Deploy a daemon, or let an agent hand you the exact command (Let an AI agent run DedNets).

To publish a container the host has discovered, name it in container:

Terminal window
curl -fsS -X POST -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"daemon":"alice/vm1","container":"my-app","name":"demo","port":8080,"proto":"http","public":true}' \
https://console.dednets.com/api/v1/exports

Omit container and you get a host-port export instead: the daemon dials its own host (127.0.0.1 unless you set host), which is what you want for a service running directly on the machine rather than in a container. name is then required, since there is no container name to fall back on.

Terminal window
curl -fsS -X POST -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"daemon":"alice/vm1","name":"demo","port":8080,"proto":"http","public":true}' \
https://console.dednets.com/api/v1/exports

Either way, read the assigned hostname from the topology afterwards: the app’s public_host field under its host’s apps list.

Terminal window
curl -fsS -H "Authorization: Bearer $TOKEN" \
https://console.dednets.com/api/v1/topology

To unpublish: DELETE /api/v1/exports?daemon=alice/vm1&name=demo.

Terminal window
curl -fsS -X POST -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"name":"nightly-backup","period":86400,"grace":3600,"group":"backups"}' \
https://console.dednets.com/api/v1/watchdogs

period and grace are seconds. The reply carries ping_url; have the job curl it after every successful run. That ping URL is a capability: it lives at /api/v0/wd/<address> and stays there unversioned, because it is baked into cron jobs. See Get alerted when a cron job stops running.

group is optional and purely for display: it buckets related monitors together in the Console and in listings. Same charset as a name (letters, digits, - and _, 1 to 64 characters). There is nothing to create first - a group exists as soon as a watchdog names it, and disappears when the last one leaves. Omit it for an ungrouped watchdog; responses leave the field out entirely when a watchdog has no group. (Since v0.4.1.)

PATCH only touches the fields you send, so an absent group leaves the watchdog where it is. An empty string is the way to ungroup one:

Terminal window
# file it under a group
curl -fsS -X PATCH -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"group":"db-prod"}' \
https://console.dednets.com/api/v1/watchdogs/12
# remove it from its group
curl -fsS -X PATCH -H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{"group":""}' \
https://console.dednets.com/api/v1/watchdogs/12
Terminal window
curl -fsS -H "Authorization: Bearer $TOKEN" \
"https://console.dednets.com/api/v1/services/uptime?window=day"

window is hour, day or week; add &target=alice/vm1/demo for one service.

Every /api/v1 failure is one JSON envelope:

{"error":{"code":"not_found","message":"...","status":404}}

Codes: invalid_request, unauthorized, forbidden, scope_denied, quota_exceeded, not_found, method_not_allowed, conflict, too_large, rate_limited, internal, and error (the fallback for any status the Console does not map). Branch on code, never on the message text.

  • Routes and response fields may be added, never removed or renamed.
  • Error codes may be added, never renamed.
  • Capability URLs (/api/v0/wd/…, /install.sh, /dist/…) are deliberately unversioned and stay put.

And the honest boundary: /api/v0 is what the web UI uses. It is not a contract, it can change without notice, and your scripts should use /api/v1.

  • Check the content type. A Console older than v0.4.1 does not know /api/v1 and answers a GET with the web UI’s HTML page and a 200. If your client suddenly fails to parse JSON, confirm the response really is Content-Type: application/json, then upgrade the Console.
  • Watchdog ping URLs can come back relative. On a Console with no public hostname configured, ping_url is a path such as /api/v0/wd/…. Join it to your Console base URL before handing it to a job on another machine.
  • There is no GET /api/v1/daemons and no GET /api/v1/proxy-ports. Both lists come from GET /api/v1/topology.
  • Install commands assume HTTPS on port 443. A self-hosted Console served on another port emits an install command whose download URL does not resolve.