Last updated: 2026-07-30 · Applies to: OpenWatch v0.8.0 (Eyrie)
Most operators use the web UI for daily work: managing hosts, viewing fleet health, reading compliance state, and triaging alerts. This guide is for automation: scripting repetitive tasks, integrating with CI/CD, or building tooling on top of OpenWatch.
OpenWatch is a single Go binary that serves both the REST API and the embedded
React UI over HTTPS on port 8443. All API paths live under /api/v1. The
running binary serves its own OpenAPI document as the contract source of truth,
and GET /api/v1/version reports the build it came from.
The compliance
surface (scan execution + results, remediation, exceptions, posture/drift, audit
export, the rule browser) IS exposed over /api/v1. See the compliance API surface
(now live). The genuinely-absent pieces (a
Prometheus /metrics endpoint, /security-info) are listed under what is
genuinely not in the API yet.
When the OpenAPI document and this guide disagree, the OpenAPI document wins.
- Base URL is
https://<host>:8443. The server is HTTPS-only. In a default install the certificate at/etc/openwatch/tls/cert.pemis self-signed, so add--cacert /etc/openwatch/tls/cert.pem(or, for a throwaway lab box only,-k) to yourcurlcalls. In production, point--cacertat your own CA bundle instead. - Resource identifiers are UUIDs.
- Timestamps are ISO 8601 / RFC 3339 (for example
2026-06-10T14:30:00Z). - Mutating endpoints that exist to be retried safely take a required
Idempotency-Keyheader (a unique string per logical operation). Replaying the same key with the same body returns the original result; replaying it with a different body returns409. - An optional
X-Correlation-Idheader is propagated through logs and audit events. If you omit it, the server generates one and returns it in the response. - Paginated lists (
/api/v1/audit/events,/api/v1/scans,/api/v1/alerts,/api/v1/intelligence/events,/api/v1/activity) takelimitand an opaquecursor. Each page carriesnext_cursor; pass its value as the next request'scursor, and stop when it is absent or null. Other lists takelimitonly, andGET /api/v1/hostsreturns the whole fleet.
The API accepts two credential types. Both resolve to the same identity and permission set:
- A
Bearervalue in theAuthorizationheader. Two kinds exist. An API token (owk_prefix) is the credential for scripts and CI: it is bound to one role, can carry an expiry, is stored only as a hash, and is revoked at once byDELETE /api/v1/tokens/{id}. It works only while the user who created it may sign in: it is refused while that user is disabled or deleted, and works again if they are re-enabled. A token with no owner is refused. An access token is the short-lived JWT thatPOST /api/v1/auth/loginreturns for interactive use and first-time setup; it expires 30 minutes after issue. - The browser session cookie (
openwatch_session), used by the web UI. Cookie rotation and the on-401 refresh flow are UI concerns and are not covered here. A request that presents the session cookie must also echo theXSRF-TOKENcookie in anX-CSRF-Tokenheader on every mutation or it gets403; Bearer requests are exempt from that check.
The contract declares five operations credential-free: GET /api/v1/health,
GET /api/v1/version, POST /api/v1/auth/login, POST /api/v1/auth/refresh
and POST /api/v1/auth/refresh-cookie. Three more answer an anonymous caller
by design and return only what a login page or a client needs before it has
an identity: GET /api/v1/capabilities (which capabilities this deployment
has, with no license detail), GET /api/v1/license (only tier, status
and features until the caller is authenticated), and
GET /api/v1/sso/providers/enabled (provider id and name only), together
with the SSO redirect pair GET /api/v1/auth/sso/{id}/login and
/callback. Two introspection reads are anonymous as well.
GET /api/v1/auth/permissions:registry returns the static permission
registry (permission ids, descriptions, categories and the built-in role
bundles), which this repository publishes in User roles;
it never returns users, role assignments, custom roles or deployment
settings. GET /api/v1/auth/me/permissions tells a caller what it is: an
anonymous caller gets is_anonymous: true and an empty permission list.
Everything else requires a valid identity. An anonymous caller gets 401 auth.required; an authenticated
caller without the permission gets 403 authz.permission_denied.
GET /api/v1/capabilities reports every capability this deployment has, with
whether it is available here, so a client can present a locked control rather
than discovering the gate from a 402. It exposes no customer identity or
license detail; GET /api/v1/license returns those to an authenticated caller
and only tier, status and features to an anonymous one.
Do this once, interactively, with an identity that holds token:write. Pick
the narrowest built-in or custom role the job needs and set an expiry; the
secret is returned once and never again.
curl --fail-with-body -s --cacert /etc/openwatch/tls/cert.pem \
-X POST https://localhost:8443/api/v1/auth/login \
-H "Content-Type: application/json" \
-d @login.json | jq -r '.access_token' > /tmp/ow-access
# login.json holds {"username": "...", "password": "..."} and is deleted after use;
# a password on the command line lands in shell history and in `ps`.
curl --fail-with-body -s --cacert /etc/openwatch/tls/cert.pem \
-X POST https://localhost:8443/api/v1/tokens \
-H "Authorization: Bearer $(cat /tmp/ow-access)" \
-H "Content-Type: application/json" \
-d '{"name":"ci-scanner","role_id":"viewer","expires_at":"2027-01-01T00:00:00Z"}' \
| jq -r '.token' > ci-token # store it in your secret manager, not in the repo--fail-with-body (curl 7.76 and later) makes a 401 or 403 exit
non-zero with the error envelope printed, instead of writing the string
null into your token file and failing later with a misleading 401. On
older curl, check $? and the envelope yourself. expires_at is optional
in the contract; set it for every automation credential and rotate it before
it lapses. Revoke a token with DELETE /api/v1/tokens/{id} (token:delete);
GET /api/v1/tokens lists metadata only.
The password login is for interactive use and for minting the first API
token. The request body is {username, password} with an optional otp
(6 digits) when the account has TOTP MFA enrolled. The response is:
{
"access_token": "…",
"refresh_token": "…",
"user": {"id": "…", "username": "admin", "email": "…", "role": "admin"}
}All later examples assume -H "Authorization: Bearer $TOKEN", where
$TOKEN is an API token or an access token.
| Method | Path | Purpose |
|---|---|---|
POST |
/api/v1/auth/refresh |
Rotate the refresh token; returns a new access + refresh pair. Body: {refresh_token}. |
GET |
/api/v1/auth/me |
Return the calling identity (id, username, email, role). |
GET |
/api/v1/auth/me/permissions |
Return the caller's effective permission strings. |
POST |
/api/v1/auth/logout |
Revoke the session and refresh token presented as cookies (204). A caller that presents only a Bearer value has nothing this route revokes today: an access token stays valid until it expires (30 minutes), and a refresh token returned in the login body has no revoke route until CP bugs/OW-062 is resolved. Revoke an API token with DELETE /api/v1/tokens/{id}. |
POST |
/api/v1/auth/password:change |
Change the caller's password. Body: {current_password, new_password}. |
POST |
/api/v1/auth/mfa:enroll |
Begin TOTP enrollment; returns a provisioning_uri. |
POST |
/api/v1/auth/mfa:verify |
Confirm an enrolled secret. Body: {otp}. |
Authorization is permission-based, not role-based, at the endpoint level. Each
protected endpoint declares the permission it requires (visible in the served
OpenAPI document, for example host:read or host:write). Built-in roles
bundle permission sets:
| Role | Intent |
|---|---|
viewer |
Read-only access |
auditor |
Read plus audit/compliance review |
ops_lead |
Host + scan + remediation operations |
security_admin |
Security configuration |
admin |
Full system administration |
A caller missing the required permission receives 403. The full permission and
role registry is the source of truth at
User roles; the running service exposes it at
GET /api/v1/auth/permissions:registry.
| Method | Path | Permission | Purpose |
|---|---|---|---|
GET |
/api/v1/hosts |
host:read |
List hosts. Query: environment, tag. |
POST |
/api/v1/hosts |
host:write |
Create a host. |
GET |
/api/v1/hosts/{id} |
host:read |
Host detail with liveness and compliance summary. Query: framework. |
PATCH |
/api/v1/hosts/{id} |
host:write |
Update mutable host fields. |
DELETE |
/api/v1/hosts/{id} |
host:delete |
Soft-delete a host (204; sets deleted_at). |
GET |
/api/v1/hosts/{host_id}/monitoring/history |
host:read |
Monitoring history. |
PUT |
/api/v1/hosts/{host_id}/maintenance |
host:write |
Pause or resume liveness probes for the host (maintenance mode). |
POST |
/api/v1/hosts/{id}/connectivity:check |
host:connectivity_check |
Run a connectivity check (idempotent). |
GET |
/api/v1/hosts/{id}/system-info |
host:read |
Latest collected system intelligence. |
POST |
/api/v1/hosts/{id}/discovery:run |
host:write |
Run host discovery (idempotent). |
POST |
/api/v1/hosts/{host_id}/credentials:resolve |
credential:read |
Resolve the effective credential for a host. |
curl -s --cacert /etc/openwatch/tls/cert.pem \
-X POST https://localhost:8443/api/v1/hosts \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"hostname": "rhel9-web01.example.com",
"ip_address": "10.0.1.50",
"port": 22,
"environment": "production",
"tags": ["web", "rhel9"]
}'hostname and ip_address are required; port, display_name, description,
environment, tags, group_id, and username are optional. A successful
create returns 201; a duplicate hostname in the same environment returns 409.
SSH credentials are stored separately from hosts and scoped either to the whole
system (scope: system) or to one host (scope: host). Secret material is
encrypted at rest and never returned in responses.
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/credentials |
List credentials (secrets redacted). |
POST |
/api/v1/credentials |
Create a credential. |
GET |
/api/v1/credentials/{id} |
Get one credential. |
PATCH |
/api/v1/credentials/{id} |
Update a credential. |
DELETE |
/api/v1/credentials/{id} |
Delete a credential. |
POST |
/api/v1/credentials/{id}:clone |
Clone to a new scope (secret inherited; no plaintext on the wire). |
A create body requires scope, name, username, and auth_method (one of
ssh_key, password, both). scope_id is the host's UUID and is required
when scope is host; it must be absent when scope is system. Either
mismatch returns 400 credentials.invalid_scope, and a scope_id that
names no active host returns 400 credentials.host_not_found. Provide
private_key (and optional private_key_passphrase) and/or password to
match the chosen method.
These endpoints back the dashboard and require read access.
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/fleet/score |
Aggregate fleet compliance score. |
GET |
/api/v1/fleet/liveness |
Fleet liveness breakdown. |
GET |
/api/v1/fleet/top-failing-rules |
Rules failing across the most hosts. |
GET |
/api/v1/fleet/top-failing-hosts |
Hosts with the most failing rules. |
GET |
/api/v1/fleet/recent-changes |
Recent compliance state transitions. |
GET |
/api/v1/fleet/connectivity/breakdown |
Connectivity status counts. |
score_pct is nullable on every surface that carries it, and a client has
to handle the null.
| Value | Meaning |
|---|---|
null |
No rule produced a pass or a fail. Nothing was measured. |
0 |
Rules produced verdicts and all of them failed. |
Do not coerce null to 0. They are different answers, and a client that
merges them reports an unscanned host as a totally failing one.
The score counts pass and fail only. Skipped, not-applicable and errored rules
change neither the numerator nor the denominator. Aggregates are the mean of
the scored hosts, each host counting once, with unscored hosts excluded from
the mean and still reported in hosts_total, hosts_scored and
hosts_without_score.
coverage_status travels beside the score and is one of available,
unavailable_unclassified_skips or unavailable_no_outcomes. A coverage
percentage is present only when the status is available.
See Scanning and compliance for what the numbers mean, and the OpenAPI contract for the complete envelope including the provenance fields.
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/alerts |
List alerts. |
GET |
/api/v1/alerts/{id} |
Alert detail. |
POST |
/api/v1/alerts/{id}:acknowledge |
Acknowledge an alert. |
POST |
/api/v1/alerts/{id}:silence |
Silence an alert. |
POST |
/api/v1/alerts/{id}:resolve |
Resolve an alert. |
POST |
/api/v1/alerts/{id}:dismiss |
Dismiss an alert. |
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/intelligence/events |
Stream of intelligence-collection events. |
GET |
/api/v1/intelligence/state/{host_id} |
Latest intelligence state for a host. |
GET |
/api/v1/activity |
Unified recent-activity feed. |
Connectivity, intelligence-collection, and discovery behavior are configured through the API. These are admin-level controls.
| Method | Path | Purpose |
|---|---|---|
GET / PUT |
/api/v1/system/connectivity/config |
Connectivity polling config. |
GET |
/api/v1/system/connectivity/status |
Connectivity worker status. |
GET / PUT |
/api/v1/system/intelligence/config |
Intelligence-collection config. |
GET / PUT |
/api/v1/system/discovery/config |
Discovery config. |
POST |
/api/v1/system/discovery/sweep |
Trigger a discovery sweep (idempotent). |
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/users |
List users. |
POST |
/api/v1/users |
Create a user. Body: {username, email, password}. |
GET |
/api/v1/users/{id} |
Get a user. |
PATCH |
/api/v1/users/{id} |
Update a user. |
DELETE |
/api/v1/users/{id} |
Delete a user. |
POST |
/api/v1/users/{id}/roles:assign |
Assign a role. Body: {role_id}. |
POST |
/api/v1/users/{id}/roles:unassign |
Remove a role. |
GET |
/api/v1/roles |
List roles (built-in roles only). |
POST |
/api/v1/roles:create |
Create a custom role. |
For first-admin bootstrap, prefer the CLI (openwatch create-admin) over the
API; see Operations.
OpenWatch has two tiers: Community, which reports free, and Enterprise, which
reports enterprise. Community needs no license file, and a deployment without
one reports tier: free with status: no_license. The tiers differ in the scope
of an action, not in capability: what one host can do is Community, and the same
vocabulary across a fleet is Enterprise. There are no quotas or caps on hosts,
scans, users, or retention. An Enterprise-scoped endpoint returns 402 when the
active tier lacks the feature.
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/license |
Current license tier, status, and features. |
POST |
/api/v1/admin/license:verify |
Dry-run validate a license JWT without installing it. |
Every meaningful state change writes an audit event. The log is queryable and cursor-paginated, newest first.
| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/audit/events |
List audit events. |
GET |
/api/v1/audit/events/export |
Download the filtered trail as CSV (default) or JSON (format=json). Requires audit:export. |
List query parameters: action, correlation_id, actor_type,
resource_type, resource_id, since, until (both RFC 3339), cursor,
and limit (1 to 200, default 50). Each page carries next_cursor; pass it
as the next request's cursor.
The export takes the same seven filters, returns the whole filtered set
newest first, and stops at 10,000 rows; a capped export carries an
X-OpenWatch-Export-Truncated header. A query parameter the export does
not declare is refused with 400 request.unknown_parameter naming it, so
a misspelled filter cannot silently widen an export you will file; the list
endpoint ignores unknown parameters as before.
These two endpoints are anonymous and are what monitoring should poll.
curl -s --cacert /etc/openwatch/tls/cert.pem https://localhost:8443/api/v1/health | jq{"status": "healthy", "db_connected": true, "version": "<release version>"}A healthy response is always status: "healthy", db_connected: true. When
the database is unreachable, the endpoint does not return a degraded status
body. It returns 503 with the standard ErrorEnvelope (code
server.unavailable) instead. GET /api/v1/version returns build metadata
(openwatch, kensa, go, commit, build_time).
Errors use a single envelope shape, not field-level validation detail:
{
"error": {
"code": "hosts.invalid_input",
"fault": "client",
"retryable": false,
"human_message": "ip_address is required",
"correlation_id": "…"
}
}fault is one of client, server, policy, or external. Status codes you
will encounter:
| Code | Meaning |
|---|---|
400 |
Bad request: invalid input or a violated business rule |
401 |
Unauthorized: missing, expired, or invalid credential |
402 |
Payment required: the license tier lacks this feature |
403 |
Forbidden: the caller lacks the required permission |
404 |
Not found |
405 |
Method not allowed |
409 |
Conflict: duplicate resource, or a reused Idempotency-Key with a different body |
429 |
Too many requests: /auth/login or /auth/mfa:verify rate limit exceeded; retry after Retry-After seconds |
502 |
Bad gateway: an external dependency failed |
503 |
Service unavailable: the service is degraded |
There is no general per-route API rate limiting in this release. POST /api/v1/auth/login and /api/v1/auth/mfa:verify are the exceptions: they are
rate-limited per client IP and return 429 with a Retry-After header over
the limit. There is no 422 validation status: validation failures return
400 with the envelope above.
Every error the OpenWatch process generates carries the envelope, including
a 404 for an /api/ path that does not exist, a 405, and a 400 for a
query parameter that is missing or fails to parse; the message names the
parameter and never repeats the rejected value. A proxy or load balancer in
front of OpenWatch produces its own 502, 503 or 504 bodies, so a client
should treat any non-2xx whose body is not JSON as an infrastructure error
rather than fail on the parse.
Automation that manages the deployment itself (rather than calling the API) uses
the openwatch binary and systemd, not Docker. The subcommands are listed
once, in the environment reference.
Day-to-day lifecycle:
systemctl status openwatch
systemctl restart openwatch
journalctl -u openwatch -fConfiguration lives in /etc/openwatch/openwatch.toml, with a fixed set of
environment overrides named OPENWATCH_<SECTION>_<KEY> (the loader
recognizes only the variables listed in the
environment reference and ignores any other) and
the database DSN in /etc/openwatch/secrets.env (OPENWATCH_DATABASE_DSN). For full install and
configuration steps, see
docs/guides/INSTALLATION.md.
As of v0.2.0, the compliance workflow IS exposed over api/v1 (it is no
longer worker-internal only):
- Scans: trigger with
POST /api/v1/hosts/{id}/scans; browse durable per-scan history + per-rule evidence + OSCAL export under/api/v1/scansand/api/v1/scans/{id}(scan:read). - Remediation: request/approve/reject + execute/rollback under
/api/v1/remediation/requests(sub-actions:approve,:dry-run,:execute,:reject,:rollback). - Compliance exceptions: request via
/api/v1/hosts/{id}/exceptions, browse the fleet queue via/api/v1/compliance/exceptions, then mutate with/api/v1/exceptions/{xid}:approve,:reject, or:revoke. - Posture + drift: per-host
/api/v1/hosts/{id}/complianceand/api/v1/hosts/{id}/compliance/trend; fleet/api/v1/fleet/score. - Audit export:
GET /api/v1/audit/events(audit:read; filterable via query parameters, cursor-paginated) andGET /api/v1/audit/events/export(audit:export: auditor, security_admin, admin), a synchronous CSV/JSON download of the same filtered set (capped at 10,000 rows, newest-first). - Rule browser:
/api/v1/rules(the Kensa rule-library read model).
- A Prometheus
/metricsendpoint and a/security-infoendpoint: both are roadmap items (useGET /api/v1/healthfor liveness today). Do not script against them until they appear in the served OpenAPI document.
Kensa is the SSH-based compliance scanning engine OpenWatch invokes to run scans; see Scanning and compliance for how it integrates.
- Install guide: install, configure, and run the service.
- User roles: permission and role reference.
- Scanning and compliance: how scanning works.
- The served OpenAPI document: the authoritative, always-current API contract.