REST API
On this page
goethite has a REST API at /api/v1 for everything the config file does not cover: filter lists
(and their refresh, recommended catalog and FilterLists directory), custom rules, local DNS
records, clients, groups, schedules, blocked services, the filtering settings and access lists,
pausing, the query log, statistics, the audit log, the cache and the cluster. The
terminal UI and the web UI both use it. The API reference lists
every endpoint.
Access
Section titled “Access”By default the API listens on 127.0.0.1:8053 and answers without authentication, which only
local programs can reach:
curl http://127.0.0.1:8053/api/v1/statusGET /api/v1/health never needs a token or session; sign-in and reset redemption work without
one too. Once a user account exists, the other endpoints need a token or a session even on
loopback.
To use it from other machines, create an admin token:
goethite tokenIt prints a token (gth_ followed by 64 hex digits) and a line for the config file. Only that
hash goes into the config, so the config file gives nothing away:
[api]
listen = ["0.0.0.0:8053", "[::]:8053"]
token_sha256 = "…"
tls_cert = "/etc/goethite/api.crt" # recommended beyond loopback
tls_key = "/etc/goethite/api.key"Restart goethite. From then on every request, from anywhere, needs the token (or a signed-in
session; /api/v1/health, sign-in and reset redemption stay open):
curl -H "Authorization: Bearer $GOETHITE_TOKEN" https://dns.example.lan:8053/api/v1/statusgoethite refuses to listen beyond loopback without a token. Without TLS, the token crosses the network in clear text, and goethite logs a warning. Keep the API off the internet either way.
Users and sign-in
Section titled “Users and sign-in”The token is for programs. For people, goethite keeps users in its store, with an Argon2id password hash and, optionally, a TOTP second factor. Create the first one on the node (goethite must not be running, since the store file is locked):
goethite user add --config /etc/goethite/goethite.toml admin # prints a generated password once
goethite user passwd admin # set another one
goethite user reset admin # one-time link for the web UI, valid an hourUsers sign in with POST /api/v1/auth/login (name, password, and code when a second factor
is on) and get an HttpOnly session cookie back; the salt is never echoed anywhere, and password
hashes, TOTP secrets and recovery codes never leave the node through the API. Roles are admin
(everything) and viewer (reads and its own account). Admin token and sessions work side by side:
scripts keep the token, browsers the cookie.
curl -c jar -X POST http://127.0.0.1:8053/api/v1/auth/login \
-H 'content-type: application/json' -d '{"name": "admin", "password": "…"}'
curl -b jar http://127.0.0.1:8053/api/v1/statusChanging a user's password, role, second factor or disabled state ends every session of that user
on every node: each session names the revision it was made at. A user who lost their password
gets a reset link (POST /api/v1/users/{id}/reset, admin only), or one from goethite user reset; it works once, for an hour. A user who lost their authenticator has it cleared by an
admin (POST /api/v1/users/{id}/otp/disable). Sign-in attempts are rate-limited per address and
per user name.
Browsers
Section titled “Browsers”So that web pages cannot use your browser against the API, goethite refuses two kinds of request:
- Without a token, other names. Requests must be for a loopback address (
127.0.0.1,[::1]) or alocalhostname (with any port). This stops DNS rebinding, where a hostile site makes its own name resolve to 127.0.0.1. With a token any name works, since such a page does not have the token. - Requests from other sites. A request with an
Originheader must come from the API's own address. Browsers addOriginto anything that can change something; curl, scripts and the TUI send none and are not affected.
Both are answered with 403 forbidden.
Resources
Section titled “Resources”Lists, rules, local records, groups, clients and schedules all work the same way. POST a spec to the
collection to create one, then GET, PUT or DELETE it by ID:
api() { curl -s -H "Authorization: Bearer $GOETHITE_TOKEN" -H 'content-type: application/json' "$@"; }
api -X POST http://127.0.0.1:8053/api/v1/rules -d '{"rule": "||ads.example^"}'{
"id": "ru_6e687cfb77741ac2",
"revision": 1,
"created_at": "2026-10-08T07:20:40.011282Z",
"updated_at": "2026-10-08T07:20:40.011282Z",
"spec": { "rule": "||ads.example^", "enabled": true, "comment": "", "managed_by": "api" }
}A PUT replaces the whole spec. Every resource has a revision, also sent as its ETag. Send it
back in If-Match and the update or delete fails with 412 if someone changed the resource in
the meantime. Changes apply at once: a new rule blocks from the next query on.
Specs reject unknown fields. Errors are JSON with a stable code:
{ "error": { "code": "conflict", "message": "conflict: groups[default].lists[0]: there is no list \"li_x\"" } }| Status | Code | When |
|---|---|---|
| 400 | bad_request |
The request is malformed (bad JSON, bad query string, bad If-Match). |
| 401 | unauthorized |
No session and the token is missing or wrong, a wrong password or code, or an expired reset link. |
| 401 | otp_required |
The sign-in needs a code from the authenticator app. |
| 403 | forbidden |
A viewer tried to change something, or the account is disabled. |
| 404 | not_found |
No such resource or endpoint. |
| 405 | Wrong method for an endpoint; the HTTP layer answers with an empty body. | |
| 409 | conflict |
It refers to something missing, or something still refers to it. |
| 412 | revision_mismatch |
It changed since the revision in If-Match. |
| 413 | bad_request |
The body is larger than 1 MiB. |
| 415 | bad_request |
A body was sent with the wrong content type. |
| 422 | invalid |
A value is not valid, or a field is unknown. |
| 429 | too_many_requests |
Too many failed sign-ins or reset attempts; wait five minutes. |
| 500 | internal |
goethite failed to do the work. |
| 503 | unavailable |
A dependency (the store) is unavailable; retry. |
Pausing filtering
Section titled “Pausing filtering”api -X PUT http://127.0.0.1:8053/api/v1/pause -d '{"seconds": 600}' # for 10 minutes
api -X DELETE http://127.0.0.1:8053/api/v1/pause # resume nowA pause ends by itself, and on a restart. It is per node: in a
cluster pause each member, or a device that reaches another one stays filtered. The
settings' protection switch turns filtering off for good.
Settings, cache and updates
Section titled “Settings, cache and updates”GET and PUT /api/v1/settings read and change the replicated filtering settings: the
protection switch, how blocked names are answered (null IP, NXDOMAIN, REFUSED or a custom
address), the blocked-answer TTL, the list update interval and the allowed and disallowed clients.
api -X POST http://127.0.0.1:8053/api/v1/cache/flush # names resolve again on their next query
api -X POST http://127.0.0.1:8053/api/v1/update/check # is a newer release out?POST /api/v1/lists/refresh downloads every URL list now, GET /api/v1/services lists the
blocked-services catalog, and the cluster endpoints (GET /api/v1/cluster, promote, demote and
member removal) are in High availability.
DNS leak tests
Section titled “DNS leak tests”A DNS leak test is a set of names only goethite answers. POST makes one; the
device being tested looks its names up; GET shows which reached this node, from where, over
which protocol and as which client:
api -X POST http://127.0.0.1:8053/api/v1/leak-tests # {"id": "…", "names": [8 names], …}
api http://127.0.0.1:8053/api/v1/leak-tests/<id> # {"reached": 8, "lookups": [...], …}
api http://127.0.0.1:8053/api/v1/leak-tests # every test of the last hourTests are kept in memory on the node that made them for an hour, and are not configuration: a cluster member does not forward them to the leader, and the audit log does not list them.
Query log and statistics
Section titled “Query log and statistics”api 'http://127.0.0.1:8053/api/v1/querylog?limit=50&outcome=blocked'
api 'http://127.0.0.1:8053/api/v1/querylog?client=192.168.1.23&name=example'
api 'http://127.0.0.1:8053/api/v1/stats?hours=24'The query log is newest first. To page, pass the response's next as before. The statistics
cover the last 1 to 720 hours: counts per hour by outcome, and the names, blocked names and clients
asked most.
Audit log
Section titled “Audit log”Every change, and every pause, resume and list refresh, is in the audit log with the time, who
made it (token, user, unauthenticated, cli, replication or system), their address, and
the resource before and after. User changes are logged with the name, role and disabled state;
password hashes, TOTP secrets and recovery codes are never written to the log:
api 'http://127.0.0.1:8053/api/v1/audit?limit=20'Metrics
Section titled “Metrics”/metrics serves Prometheus metrics: queries by outcome and protocol (udp, tcp, dot,
doh, doq, odoh), a latency histogram, rate-limited and refused queries, failed TLS handshakes and rejected
DNS over HTTPS requests, the cache, upstream health, the filter and the query log. It
needs the token once one is configured:
scrape_configs:
- job_name: goethite
authorization:
credentials_file: /etc/prometheus/goethite-token
static_configs:
- targets: ["dns.example.lan:8053"]goethite keeps these metrics with the OpenTelemetry SDK and serves them through its Prometheus
reader, under the same names as before. It can also send them to a collector over OTLP; see
Metrics and telemetry. An OpenTelemetry Collector can instead read
/metrics with its prometheus receiver and the same scrape configuration:
receivers:
prometheus:
config:
scrape_configs:
- job_name: goethite
authorization:
credentials_file: /etc/otelcol/goethite-token
static_configs:
- targets: ["dns.example.lan:8053"]The OpenAPI document
Section titled “The OpenAPI document”goethite openapi prints the OpenAPI 3.1 document, which /api/v1/openapi.json also serves.
Generate a client from it, or browse it in the API reference.