Skip to content
🐋 Cetacean

API Guide

Observability and management API for Docker Swarm Mode clusters.

Cetacean runs as a single binary that connects to the Docker socket, caches swarm state in memory, and serves it over HTTP. Read endpoints use GET; write operations use PUT, POST, PATCH, and DELETE gated by operations level. Authentication is pluggable via auth.mode (default: anonymous access).

The machine-readable OpenAPI spec is available at /api (JSON). For an interactive endpoint browser, see the API Reference.

Content Negotiation

Every resource URL serves JSON, HTML (the embedded SPA), or SSE depending on what the client asks for. No /api/v1/ prefix — versioning lives in the media type.

Resolution order

  1. File extension: .json, .html, or .atom appended to any path (the highest priority)
  2. Accept header: standard content negotiation
  3. Default: application/json when */* or no preference

Supported types

Accept valueResult
application/jsonJSON (latest version)
application/vnd.cetacean.v1+jsonJSON pinned to v1
text/htmlSPA
text/event-streamSSE (only on endpoints that support it)
application/atom+xmlAtom feed (resource and history endpoints)

All negotiated responses include Vary: Accept.

Requesting an unsupported type returns 406 Not Acceptable.

GET /services HTTP/1.1
Accept: application/json
curl -H "Accept: application/json" http://localhost:9000/services

Extensions also work — append .json or .atom to any resource path:

GET /services.json HTTP/1.1
curl http://localhost:9000/services.json

Atom Feeds

Resource list endpoints, resource detail endpoints, and the history, search, and recommendations endpoints all support Atom feeds. Request via Accept: application/atom+xml or append .atom to any supported path.

Supported endpoints

All resource list and detail endpoints support Atom:

  • /nodes, /nodes/{id}
  • /services, /services/{id}
  • /tasks, /tasks/{id}
  • /stacks, /stacks/{name}
  • /configs, /configs/{id}
  • /secrets, /secrets/{id}
  • /networks, /networks/{id}
  • /volumes, /volumes/{name}
  • /events, /history, /search, /recommendations

Endpoints that do not produce resource change data (write sub-resources, log streams, metrics, topology) return 406 Not Acceptable.

Pagination

Atom feeds use cursor-based pagination. The feed includes a next link when more entries are available:

ParameterDescription
beforeReturn entries older than this cursor ID
limitNumber of entries per page (default 50, max 200)
# Request an Atom feed
GET /services HTTP/1.1
Accept: application/atom+xml
###

# Request via extension
GET /services.atom HTTP/1.1
###

# Page through history feed
GET /history.atom?limit=50 HTTP/1.1

GET /history.atom?before=<cursor-id>&limit=50 HTTP/1.1
# Request an Atom feed
curl -H "Accept: application/atom+xml" http://localhost:9000/services

# Request via extension
curl http://localhost:9000/services.atom

# Page through history feed
curl "http://localhost:9000/history.atom?limit=50"
curl "http://localhost:9000/history.atom?before=<cursor-id>&limit=50"

Caching

Atom feeds support ETags and conditional requests. Pass If-None-Match with a previous ETag to receive 304 Not Modified when the feed has not changed. Responses include Vary: Accept, Authorization, Cookie so caches differentiate by format and user.

Feed autodiscovery

JSON responses on feed-capable endpoints include a Link: <...>; rel="alternate"; type="application/atom+xml" header. The SPA injects <link rel="alternate" type="application/atom+xml"> in the HTML <head>, so feed readers that support browser-based autodiscovery can find feeds automatically.

Pagination

List endpoints support two pagination mechanisms: query parameters and HTTP Range headers.

Query parameters

ParameterTypeDefaultDescription
limitint50Items per page (1-200)
offsetint0Starting position
sortstringSort field (varies by resource)
dirstringascSort direction: asc or desc
searchstringCase-insensitive substring match on name
filterstringexpr-lang expression (max 512 chars)
# Paginate results
GET /services?limit=10&offset=20 HTTP/1.1
###

# Sort by field
GET /nodes?sort=hostname&dir=desc HTTP/1.1
###

# Search by name
GET /configs?search=nginx HTTP/1.1
###

# Filter with expression
GET /services?filter=name+contains+'web' HTTP/1.1
# Paginate results
curl "http://localhost:9000/services?limit=10&offset=20"

# Sort by field
curl "http://localhost:9000/nodes?sort=hostname&dir=desc"

# Search by name
curl "http://localhost:9000/configs?search=nginx"

# Filter with expression
curl "http://localhost:9000/services?filter=name+contains+'web'"

Range header pagination

List endpoints also accept Range: items 0-24 for HTTP range-based pagination. Returns 206 Partial Content with Content-Range: items 0-24/142. When both query parameters and Range are present, query parameters take precedence.

Sort fields by resource

ResourceSortable fields
Nodeshostname, role, status, availability
Servicesname, mode
Tasksstate, service, node
Stacksname
Configsname, created, updated
Secretsname, created, updated
Networksname, driver, scope
Volumesname, driver, scope

Filter fields by resource

Filter expressions use expr-lang syntax. The result must be boolean. Operators: ==, !=, <, >, <=, >=, contains, startsWith, endsWith, in, not in, &&, ||, !.

Nodes: id, name (hostname), state (ready/down/unknown), role (manager/worker), availability ( active/pause/drain)

Services: id, name, image, mode (replicated/global), stack

Tasks: id, state (new/allocated/pending/activating/running/deactivating/stopping/completed/ failed/rejected), desired_state, image, exit_code, error, service (ID), node (ID), slot (int)

Configs: id, name

Secrets: id, name

Networks: id, name, driver, scope (swarm/local)

Volumes: name, driver, scope

Stacks: name, services (count), configs (count), secrets (count), networks (count), volumes (count)

# Filter ready managers
GET /nodes?filter=role+%3D%3D+%22manager%22+%26%26+state+%3D%3D+%22ready%22 HTTP/1.1
###

# Filter failed tasks
GET /tasks?filter=state+%3D%3D+%22failed%22+%7C%7C+error+!%3D+%22%22 HTTP/1.1
###

# Filter stacks by service count
GET /stacks?filter=services+>+5 HTTP/1.1
# Filter ready managers
curl "http://localhost:9000/nodes?filter=role+%3D%3D+%22manager%22+%26%26+state+%3D%3D+%22ready%22"

# Filter failed tasks
curl "http://localhost:9000/tasks?filter=state+%3D%3D+%22failed%22+%7C%7C+error+!%3D+%22%22"

# Filter stacks by service count
curl "http://localhost:9000/stacks?filter=services+>+5"

Response Format

All responses include JSON-LD annotations (@context, @id, @type) for self-description. Collection responses wrap items in { items, total, limit, offset } with RFC 8288 Link headers for pagination. Detail responses wrap the resource with cross-references (e.g., services using a config, or the service and node for a task).

Errors

Error responses follow RFC 9457 (Problem Details) with Content-Type application/problem+json.

{
  "@context": "/api/context.jsonld",
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "node abc123 not found",
  "instance": "/nodes/abc123",
  "requestId": "a1b2c3d4e5f6"
}

Error codes

Every domain-specific error includes a stable error code in its type URI:

{
  "@context": "/api/context.jsonld",
  "type": "/api/errors/SVC001",
  "title": "Service Version Conflict",
  "status": 409,
  "detail": "service was modified by another client",
  "instance": "/services/abc123/scale",
  "requestId": "a1b2c3d4e5f6"
}

The code is the last path segment of type (e.g. SVC001). Codes use a three-letter domain prefix followed by a three-digit number:

PrefixDomain
APIProtocol and content negotiation
AUTAuthentication
OPSOperations level
FLTFilter expressions
SEASearch
MTRMetrics / Prometheus
LOGLog streaming
ACLAuthorization (RBAC)
SSESSE connections
ENGDocker Engine
SWMSwarm operations
PLGPlugin operations
NODNode operations
SVCService operations
TSKTask operations
STKStack operations
VOLVolume operations
NETNetwork operations
CFGConfig operations
SECSecret operations

Generic HTTP errors (no domain-specific code) use "type": "about:blank".

Browse the error reference interactively at GET /api/errors or look up a single code at GET /api/errors/{code}.

Common error scenarios

Version conflicts (409): All Write endpoints use Docker’s optimistic concurrency. If the resource was modified by another client between your read and write, the server returns 409 Conflict with a SVC001, NOD002, or similar code. Re-read the resource and retry.

Operations level (403): Requests to endpoints above the configured operations level return 403 with code OPS001.

Authorization denied (403): When ACL is active, read access denied returns ACL001 and write access denied returns ACL002. The response includes the resource and permission that was checked.

Unsupported patch type (415): PATCH endpoints validate Content-Type. Sending application/json instead of application/json-patch+json or application/merge-patch+json returns 415 Unsupported Media Type.

Caching

JSON responses include an ETag header (SHA-256 of the response body). Use If-None-Match for conditional requests:

# First request -- note the ETag
curl -v http://localhost:9000/services
# < ETag: "3a7f..."

# Conditional request
curl -H 'If-None-Match: "3a7f..."' http://localhost:9000/services
# < HTTP/1.1 304 Not Modified

Static resources (/api, /api/context.jsonld) return Cache-Control: public, max-age=3600.

SSE and streaming endpoints do not set caching headers.

Detail endpoints also return Last-Modified based on the resource’s update timestamp. Use If-Modified-Since for conditional requests alongside or instead of ETags.

Response Headers

Beyond standard caching headers, Cetacean sets several headers to help clients discover capabilities:

Allow: GET and HEAD responses include an Allow header listing the HTTP methods available for that resource, based on the current operations level and ACL permissions. A client can inspect this before attempting a write operation.

Accept-Patch: Resources that support PATCH include Accept-Patch listing the accepted content types (application/json-patch+json, application/merge-patch+json, or both). Present only when the operations level and ACL permit write operations.

Prefer: return=minimal: Write endpoints honor RFC 7240 Prefer: return=minimal. When set, successful writes return 204 No Content instead of the updated resource. The response includes Preference-Applied: return=minimal.

Standard security headers (X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Content-Security-Policy) are set on all responses. HSTS is added when TLS is enabled.

Real-Time Events (SSE)

Every resource endpoint supports SSE in addition to JSON. Send Accept: text/event-stream to any list or detail URL to open a per-resource event stream.

Per-resource streams

List Endpoints stream events filtered by resource type. Detail Endpoints stream events for a single resource. Stack streams include events for all member resources (services, tasks, configs, secrets, networks, volumes).

# Stream all node events
GET /nodes HTTP/1.1
Accept: text/event-stream
###

# Stream events for a single service
GET /services/abc123 HTTP/1.1
Accept: text/event-stream
###

# Stream events for a stack and all its resources
GET /stacks/myapp HTTP/1.1
Accept: text/event-stream
# Stream all node events
curl -H "Accept: text/event-stream" http://localhost:9000/nodes

# Stream events for a single service
curl -H "Accept: text/event-stream" http://localhost:9000/services/abc123

# Stream events for a stack and all its resources
curl -H "Accept: text/event-stream" http://localhost:9000/stacks/myapp

This is the primary SSE mechanism — the frontend uses per-resource streams for real-time updates on every page.

Global event stream

/events provides a single stream of all resource changes:

GET /events HTTP/1.1
Accept: text/event-stream
curl -H "Accept: text/event-stream" http://localhost:9000/events

Event format

Single events are sent with the resource type as the event name:

id: 1
event: service
data: {"@id":"/services/abc","@type":"Service","type":"service","action":"update","id":"abc","resource":{...}}

When multiple events arrive within the batch interval (default 100ms), they are sent as a batch event:

id: 2
event: batch
data: [{"@id":"/services/abc","@type":"Service","type":"service","action":"update","id":"abc","resource":{...}},...]

Filtering

Use ?types= to subscribe to specific resource types:

GET /events?types=service,node HTTP/1.1
Accept: text/event-stream
curl -H "Accept: text/event-stream" "http://localhost:9000/events?types=service,node"

Valid types: node, service, task, config, secret, network, volume, stack.

Keepalive

The server sends SSE comment lines (:keepalive) on idle connections to prevent proxies and load balancers from closing them. This is transparent to EventSource clients.

Reconnection and Replay

The server assigns incrementing id: values to each event. EventSource clients automatically send Last-Event-ID on reconnect, and the server replays missed events. If the requested ID is too old, the server sends a sync event to tell the client to do a full reload.

Metrics SSE

The /metrics endpoint supports SSE for live-updating charts. Request text/event-stream to receive periodic metric updates instead of a one-shot JSON proxy response.

GET /metrics?query=up&step=15&range=3600 HTTP/1.1
Accept: text/event-stream
curl -H "Accept: text/event-stream" "http://localhost:9000/metrics?query=up&step=15&range=3600"
EventDescription
initialFull range query result on connect (same shape as Prometheus query_range).
pointSingle instant query result appended at each tick.

The stream runs instant queries on each tick interval and pushes new data points. Clients append point events to their existing data to build a rolling window.

Connection Limits

SSE, log stream, and metrics stream connections are capped. When a limit is reached, the server returns 429 Too Many Requests with a Retry-After header.

Endpoints

For the complete endpoint reference with request/response schemas and try-it-out, see the interactive API Reference.

Rate Limits

There is no general rate limiting. The only limits are on concurrent streaming connections:

ResourceLimitExceeded response
SSE event clients (/events and per-resource streams)256429 + Retry-After: 5
Log stream connections128429 + Retry-After: 5
Metrics stream connections (/metrics SSE)64429 + Retry-After: 5

Self-Discovery

Every response (except /-/ meta endpoints) includes RFC 8631 Link headers:

Link: </api>; rel="service-desc", </api/context.jsonld>; rel="describedby"
  • rel="service-desc" points to the OpenAPI spec
  • rel="describedby" points to the JSON-LD context document

Request ID

Every response includes a Request-Id header. Send your own via the Request-Id request header (max 64 chars, ASCII printable); otherwise one is generated. The ID appears in error responses as requestId and in server logs.