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?
- Lets a CI pipeline start a check right after it pushes an image so you don't wait for the schedule.
- Returns the overview, containers, and history as JSON so scripts can read them without parsing logs.
- Streams live events so a script knows the moment a check finishes.
- Exposes Prometheus counters so you can alert on failed or rolled-back updates.
- Keeps Watchtower's
/v1/update,/v1/status, and/metricspaths 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 as22601: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:
# 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
Beareris matched without regard to letter case. A token isoutpost_followed by 43 URL-safe base64 characters. - A missing, mistyped, expired, or revoked token gets
401with the codeauthentication_required. - A valid token without the route's scope gets
403with the codescope_required. GET /api/v1/auth/meworks 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
| Scope | What it allows | Routes |
|---|---|---|
status:read | See 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:run | Start checks, and update containers unless Outpost is monitor-only. | POST /api/v1/checks, POST /v1/update |
metrics:read | Read 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
| Method | Path | Scope | Description |
|---|---|---|---|
GET | /health | None | Returns {"status":"ok"}. Used by the image's own healthcheck. |
GET | /api/v1/overview | status:read | The scheduler state, the schedule, the label mode, and whether monitor-only is on. |
GET | /api/v1/containers | status:read | Every 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/history | status:read | The last 50 reports, newest first. |
GET | /api/v1/settings | status:read | The editable runtime settings and read-only container bootstrap details shown on the dashboard's Settings page. |
PUT | /api/v1/settings | Browser session | Replaces 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/status | status:read | The same body as GET /v1/status. |
POST | /api/v1/checks | updates:run | Starts a cycle, for one container when you send its ID. |
GET | /api/v1/events | status:read | Live updates as server-sent events. |
POST | /v1/update | updates:run | Starts a full cycle, the Watchtower way. |
GET | /v1/status | status:read | The scheduler state, the latest report, and running totals. |
GET | /metrics | metrics:read | Prometheus metrics in text format. |
Lists come back as {"items":[...]}. Opening / in a browser redirects to the web dashboard at /admin/.
Start a Check
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:
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
| Status | Meaning |
|---|---|
202 | A cycle started, and the body is {"started":true}. |
409 | A cycle is already running, so none was started. The code is check_running. |
422 | The 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 pollGET /api/v1/statusuntilrunningisfalse, 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
selfistrue, 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
{
"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—truewhile a cycle runs, andstartedAtthen 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/datais on a volume. Itstriggernames what started it:schedule,startup,api, ordashboard.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:
curl --no-buffer \
--header "Authorization: Bearer $OUTPOST_TOKEN" \
http://127.0.0.1:22601/api/v1/events
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}.
{
"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:
{
"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"]
}
]
}
}
| Status | Code | When |
|---|---|---|
400 | invalid_json | The body isn't valid JSON. |
401 | authentication_required | The token is missing, mistyped, expired, or revoked, or no browser is signed in. |
403 | scope_required | The token doesn't have the route's scope. |
403 | session_required | A token was sent to a route that only the dashboard can use. |
403 | csrf_invalid | A signed-in browser sent a change without its CSRF token. |
404 | not_found | No route matches the method and path, such as GET /v1/update. |
409 | check_running | A check is already running. |
413 | payload_too_large | The body is larger than 64 KB. |
415 | unsupported_encoding | The body isn't UTF-8, or it uses a compression Outpost can't read. |
422 | invalid_request | A field is missing or breaks a rule, and issues names it. |
429 | rate_limited | The client sent too many requests. |
500 | internal_error | Something unexpected went wrong, and the container log has the cause. |
502 | docker_unreachable | Outpost can't reach Docker. |
Outpost checks a request in this order, so the first problem it finds decides the answer:
- The JSON body, for
400,413, or415. - The rate limit, for
429. - The token or the browser sign-in, for
401or403. - The fields in the body, for
422.
Rate Limits
- Every request to a path under
/api/v1,/v1, or/metricscounts 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, andRateLimit-Resetheaders. A429also carriesRetry-Afterin 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
curl --request POST \
--header "Authorization: Bearer $OUTPOST_TOKEN" \
http://127.0.0.1:22601/v1/update
| Status | Body | Meaning |
|---|---|---|
202 | {"started":true} | A new cycle started. |
409 | {"started":false} | A cycle is already running, so none was started. |
POST /v1/updatealways runs a full cycle and ignores query parameters. To check one container, usePOST /api/v1/checksinstead.- 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:
scrape_configs:
- job_name: outpost
authorization:
# A token with the metrics:read scope.
credentials: outpost_...
static_configs:
- targets:
- outpost:22601
| Metric | Type | Description |
|---|---|---|
outpost_cycles_total | counter | Update cycles finished since start, including single-container checks and cycles that failed as a whole. |
outpost_containers_updated_total | counter | Containers updated since start. |
outpost_containers_failed_total | counter | Container updates that failed or rolled back since start. |
outpost_cycle_running | gauge | 1 while a cycle is running, otherwise 0. |
outpost_last_cycle_timestamp_seconds | gauge | Unix 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.
| Routes | Who can call them | What they do |
|---|---|---|
GET /api/v1/setup/status, POST /api/v1/setup/claim | Anyone | Says whether setup is needed, and creates the account with the setup code. |
POST /api/v1/auth/login, POST /api/v1/auth/login/second-factor | Anyone | Signs 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/verify | Anyone | Signs in with a passkey. |
GET /api/v1/auth/me, POST /api/v1/auth/logout | A browser or a token | Says who is calling, and signs a browser out. |
DELETE /api/v1/auth/removed-secrets | A browser | Dismisses the Saved secrets were removed notice. |
GET /api/v1/auth/security, POST /api/v1/auth/password, POST /api/v1/auth/username | A browser | Loads 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-codes | A browser | Turns 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/:passkeyId | A browser | Adds 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/:tokenId | A browser | Lists, creates, edits, and revokes access tokens. |
GET /api/v1/notifications, GET /api/v1/notifications/events, POST /api/v1/notifications | A browser | Lists 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/test | A browser | Edits, removes, and tests a notification destination. |
- Routes for a browser need one that is signed in to the dashboard. A token gets
403with the codesession_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-Tokenheader, or it gets403with the codecsrf_invalid. - For a browser,
GET /api/v1/auth/mealso returnsremovedSecrets, 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 answers204, empties it.