Skip to main content

HTTP API and Metrics

Start a check on demand, read what the last run did, and feed Outpost into Prometheus, all over plain HTTP with an access token.

Why Use This Feature?

  1. Lets a CI pipeline start a check right after it pushes an image so you don't wait for the schedule.
  2. Returns the overview, containers, and history as JSON so scripts can read them without parsing logs.
  3. Streams live events so a script knows the moment a check finishes.
  4. Exposes Prometheus counters so you can alert on failed or rolled-back updates.
  5. Keeps Watchtower's /v1/update, /v1/status, and /metrics paths so existing clients keep working.

Before You Start

  • A published port — The API listens on port 22601, next to the dashboard. The Quick Start publishes it as 22601:22601, which makes it reachable on every IPv4 and IPv6 address of the Docker host.
  • An access token — Create one on the dashboard's Access tokens page with the scopes the client needs, as shown in Access Tokens.
  • The base URL — The address the dashboard is open at, such as http://192.168.1.50:22601. Every path starts at the root, so a sub-path isn't supported.

Authentication

Send the token in the Authorization header of every request:

bash
# Read the token without showing it or saving it to the shell history.
read -r -s OUTPOST_TOKEN

curl --header "Authorization: Bearer $OUTPOST_TOKEN" http://127.0.0.1:22601/api/v1/overview
  • The word Bearer is matched without regard to letter case. A token is outpost_ followed by 43 URL-safe base64 characters.
  • A missing, mistyped, expired, or revoked token gets 401 with the code authentication_required.
  • A valid token without the route's scope gets 403 with the code scope_required.
  • GET /api/v1/auth/me works with any valid token and returns its name and scopes, which makes it a handy first test.

The /api/v1 data routes also accept a browser that is signed in to the dashboard, and a token in the header wins over the browser's cookie. The Watchtower-style paths take tokens only.

Scopes

ScopeWhat it allowsRoutes
status:readSee the overview, containers, history, and settings.GET /api/v1/overview, GET /api/v1/containers, GET /api/v1/history, GET /api/v1/settings, GET /api/v1/status, GET /api/v1/events, GET /v1/status
updates:runStart checks, and update containers unless Outpost is monitor-only.POST /api/v1/checks, POST /v1/update
metrics:readRead the Prometheus metrics.GET /metrics

Scopes are exact, so none includes another. Changes you make to a token apply to its next request, with no restart.

Endpoints

MethodPathScopeDescription
GET/healthNoneReturns {"status":"ok"}. Used by the image's own healthcheck.
GET/api/v1/overviewstatus:readThe scheduler state, the schedule, the label mode, and whether monitor-only is on.
GET/api/v1/containersstatus:readEvery running container, whether Outpost manages it and why not, its last result, and self, which is true only for Outpost's own container.
GET/api/v1/historystatus:readThe last 50 reports, newest first.
GET/api/v1/settingsstatus:readThe editable runtime settings and read-only container bootstrap details shown on the dashboard's Settings page.
PUT/api/v1/settingsBrowser sessionReplaces the complete runtime settings document. This administrative route requires the signed-in dashboard session and its CSRF token; personal access tokens cannot call it.
GET/api/v1/statusstatus:readThe same body as GET /v1/status.
POST/api/v1/checksupdates:runStarts a cycle, for one container when you send its ID.
GET/api/v1/eventsstatus:readLive updates as server-sent events.
POST/v1/updateupdates:runStarts a full cycle, the Watchtower way.
GET/v1/statusstatus:readThe scheduler state, the latest report, and running totals.
GET/metricsmetrics:readPrometheus metrics in text format.

Lists come back as {"items":[...]}. Opening / in a browser redirects to the web dashboard at /admin/.

Start a Check

bash
curl --request POST \
  --header "Authorization: Bearer $OUTPOST_TOKEN" \
  http://127.0.0.1:22601/api/v1/checks

To check one container, send its 12-character ID from GET /api/v1/containers:

bash
curl --request POST \
  --header "Authorization: Bearer $OUTPOST_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{"containerId":"3f2a9c1b7d4e"}' \
  http://127.0.0.1:22601/api/v1/checks
StatusMeaning
202A cycle started, and the body is {"started":true}.
409A cycle is already running, so none was started. The code is check_running.
422The containerId isn't 12 to 64 hexadecimal characters, or the body has another field. The code is invalid_request.
  • The request returns right away. Watch GET /api/v1/events, or poll GET /api/v1/status until running is false, to read the results.
  • Without Content-Type: application/json, the body isn't read, so Outpost checks every container.
  • An ID of a container that Outpost doesn't manage starts a check that changes nothing.
  • Outpost's own ID, from the row where self is true, checks Outpost itself. If it has a newer image, Outpost restarts, so the event stream closes until it's back, and the check shows in the history once it's done.
  • The dashboard's History page shows API as what started the check.

Read the Status

GET /api/v1/status
json
{
  "running": false,
  "nextRun": "2026-10-25T04:00:00.000Z",
  "lastReport": {
    "startedAt": "2026-10-24T04:00:00.012Z",
    "finishedAt": "2026-10-24T04:01:12.480Z",
    "results": [
      {
        "container": "web",
        "image": "ghcr.io/example/web:latest",
        "status": "updated",
        "detail": "Recreated with ghcr.io/example/web:latest"
      }
    ],
    "trigger": "schedule"
  },
  "totals": {
    "cycles": 3,
    "updated": 1,
    "failed": 0
  }
}
  • running — true while a cycle runs, and startedAt then shows when it began.
  • nextRun — When the schedule starts the next cycle.
  • lastReport — The newest entry in the saved history, so it survives a restart when /data is on a volume. Its trigger names what started it: schedule, startup, api, or dashboard.
  • lastError — Present only when the last cycle failed as a whole, for example because the Docker socket was unreachable.
  • totals — Counted since Outpost started, and reset when it restarts.

GET /api/v1/overview returns the same fields, plus schedule, timeZone, labelMode, and monitorOnly. timeZone is the time zone the schedule runs in, which TZ sets.

Live Events

GET /api/v1/events keeps the connection open and sends server-sent events:

bash
curl --no-buffer \
  --header "Authorization: Bearer $OUTPOST_TOKEN" \
  http://127.0.0.1:22601/api/v1/events
Event stream
text
retry: 3000

event: state
data: {"running":false,"nextRun":"2026-10-25T04:00:00.000Z","totals":{"cycles":0,"updated":0,"failed":0},"schedule":"0 0 4 * * *","timeZone":"UTC","labelMode":"opt-out","monitorOnly":false}

: keep-alive
  • state — The overview object, sent when you connect and whenever a check starts or finishes.
  • check — The report of a check that just finished.
  • Keep-alive — A comment line every 15 seconds keeps proxies from closing the stream. A revoked token or a signed-out browser stops getting events at the next one.

Errors

Failed requests return the same JSON shape, with a stable code for scripts and a message for people. The one exception is POST /v1/update, which answers a running check with {"started":false}.

A failed request
json
{
  "error": {
    "code": "scope_required",
    "message": "This token doesn't have the updates:run scope."
  }
}

A body that fails validation also lists each problem in issues, with the field, a message, and the path:

A body that failed validation
json
{
  "error": {
    "code": "invalid_request",
    "message": "Use a container ID of 12 to 64 hexadecimal characters.",
    "issues": [
      {
        "field": "containerId",
        "message": "Use a container ID of 12 to 64 hexadecimal characters.",
        "path": ["containerId"]
      }
    ]
  }
}
StatusCodeWhen
400invalid_jsonThe body isn't valid JSON.
401authentication_requiredThe token is missing, mistyped, expired, or revoked, or no browser is signed in.
403scope_requiredThe token doesn't have the route's scope.
403session_requiredA token was sent to a route that only the dashboard can use.
403csrf_invalidA signed-in browser sent a change without its CSRF token.
404not_foundNo route matches the method and path, such as GET /v1/update.
409check_runningA check is already running.
413payload_too_largeThe body is larger than 64 KB.
415unsupported_encodingThe body isn't UTF-8, or it uses a compression Outpost can't read.
422invalid_requestA field is missing or breaks a rule, and issues names it.
429rate_limitedThe client sent too many requests.
500internal_errorSomething unexpected went wrong, and the container log has the cause.
502docker_unreachableOutpost can't reach Docker.

Outpost checks a request in this order, so the first problem it finds decides the answer:

  1. The JSON body, for 400, 413, or 415.
  2. The rate limit, for 429.
  3. The token or the browser sign-in, for 401 or 403.
  4. The fields in the body, for 422.

Rate Limits

  • Every request to a path under /api/v1, /v1, or /metrics counts toward 120 requests per minute for each client address. The dashboard's own requests count too.
  • Responses carry the RateLimit-Policy, RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset headers. A 429 also carries Retry-After in seconds.
  • Sign-in has its own limit on failed attempts, described in Web Dashboard.
  • Behind a reverse proxy, the client address comes from X-Forwarded-For, as described in Web Dashboard.

Watchtower-Style Endpoints

POST /v1/update, GET /v1/status, and GET /metrics keep the bodies that Watchtower-style clients expect. They take an access token only, never a browser sign-in.

Trigger an Update

bash
curl --request POST \
  --header "Authorization: Bearer $OUTPOST_TOKEN" \
  http://127.0.0.1:22601/v1/update
StatusBodyMeaning
202{"started":true}A new cycle started.
409{"started":false}A cycle is already running, so none was started.
  • POST /v1/update always runs a full cycle and ignores query parameters. To check one container, use POST /api/v1/checks instead.
  • Other failures, such as a missing token, use the error shape described in Errors.

Status and Metrics

GET /v1/status returns the same body as GET /api/v1/status, described in Read the Status. GET /metrics returns the Prometheus text described in Metrics.

Metrics

Point Prometheus at /metrics with a token that has the metrics:read scope:

prometheus.yml
yaml
scrape_configs:
  - job_name: outpost
    authorization:
      # A token with the metrics:read scope.
      credentials: outpost_...
    static_configs:
      - targets:
          - outpost:22601
MetricTypeDescription
outpost_cycles_totalcounterUpdate cycles finished since start, including single-container checks and cycles that failed as a whole.
outpost_containers_updated_totalcounterContainers updated since start.
outpost_containers_failed_totalcounterContainer updates that failed or rolled back since start.
outpost_cycle_runninggauge1 while a cycle is running, otherwise 0.
outpost_last_cycle_timestamp_secondsgaugeUnix time the last cycle finished. Missing until one has.

A useful alert is increase(outpost_containers_failed_total[1d]) > 0, which fires when any update failed or rolled back in the last day.

Dashboard Routes

The dashboard itself calls these routes. Their errors use the same shape, and the dashboard shows each message next to the field it belongs to.

RoutesWho can call themWhat they do
GET /api/v1/setup/status, POST /api/v1/setup/claimAnyoneSays whether setup is needed, and creates the account with the setup code.
POST /api/v1/auth/login, POST /api/v1/auth/login/second-factorAnyoneSigns in with a password, then an authenticator or recovery code when one is needed.
POST /api/v1/auth/passkeys/login/options, POST /api/v1/auth/passkeys/login/verifyAnyoneSigns in with a passkey.
GET /api/v1/auth/me, POST /api/v1/auth/logoutA browser or a tokenSays who is calling, and signs a browser out.
DELETE /api/v1/auth/removed-secretsA browserDismisses the Saved secrets were removed notice.
GET /api/v1/auth/security, POST /api/v1/auth/password, POST /api/v1/auth/usernameA browserLoads the Security page, and changes the password or username.
POST /api/v1/auth/totp/setup, POST /api/v1/auth/totp/enable, POST /api/v1/auth/totp/disable, POST /api/v1/auth/recovery-codesA browserTurns the authenticator app on or off, and makes new recovery codes.
POST /api/v1/auth/passkeys/register/options, POST /api/v1/auth/passkeys/register/verify, DELETE /api/v1/auth/passkeys/:passkeyIdA browserAdds or removes a passkey.
GET /api/v1/auth/tokens, POST /api/v1/auth/tokens, PUT /api/v1/auth/tokens/:tokenId, DELETE /api/v1/auth/tokens/:tokenIdA browserLists, creates, edits, and revokes access tokens.
GET /api/v1/notifications, GET /api/v1/notifications/events, POST /api/v1/notificationsA browserLists and adds notification destinations, and lists the events they can choose, as described in Notifications.
PUT /api/v1/notifications/:notificationId, DELETE /api/v1/notifications/:notificationId, POST /api/v1/notifications/:notificationId/testA browserEdits, removes, and tests a notification destination.
  • Routes for a browser need one that is signed in to the dashboard. A token gets 403 with the code session_required, so a token can't change sign-in settings, make more tokens, or read or change notification destinations.
  • Every change from a browser also needs the sign-in's CSRF token in the X-CSRF-Token header, or it gets 403 with the code csrf_invalid.
  • For a browser, GET /api/v1/auth/me also returns removedSecrets, an empty list normally. After a lost encryption key, it holds {"kind":"authenticator","removedAt":"2026-10-25T08:30:00.000Z"} for the authenticator app, and {"kind":"notification","name":"Homelab chat","notificationId":"ntf-6a8c0e2b4d6f8a13","removedAt":"2026-10-25T08:30:00.000Z"} for each destination that lost secrets.
  • An entry leaves the list when its secret is entered again, and DELETE /api/v1/auth/removed-secrets, which answers 204, empties it.