Every endpoint the server exposes. The API is served under the /v1 base path. The web UI at
/ui/, along with /healthz, /readyz, /metrics, the OpenID Connect and SAML sign-in routes, the
webhook /hooks path, and the /relay worker path, is unversioned. The root redirects to the UI.
| Method | Path | What |
|---|---|---|
| POST | /v1/runs |
Submit a run. shards of two or more splits it. |
| GET | /v1/runs |
Run history, newest first. |
| GET | /v1/runs/{id} |
One run. |
| POST | /v1/runs/{id}/cancel |
Cancel a pending or running run. |
| POST | /v1/runs/{id}/retry |
New split from only the failed shards of a finished one. |
| POST | /v1/runs/{id}/relaunch-failed |
Re-run only the hosts a finished run left failed or unreachable. |
| POST | /v1/runs/{id}/approve |
Release a run held for approval so it runs. |
| POST | /v1/runs/{id}/reject |
Deny a run held for approval. |
| GET | /v1/runs/{id}/shards |
Shard runs of a split. |
| GET | /v1/runs/{id}/steps |
Step runs of a pipeline. |
| GET | /v1/runs/{id}/logs |
Captured output as plain text. |
| GET | /v1/runs/{id}/events |
Structured events as JSON. |
| GET | /v1/runs/{id}/compare |
What changed against a baseline run: host verdicts, task timing, duration. with= names the baseline or prev for the previous run of the same source. |
| GET | /v1/runs/{id}/stream |
Live events and log over Server-Sent Events. |
| POST | /v1/runs/{id}/explain |
Advisory AI explanation of a run, when a provider is configured. |
| POST | /v1/ai/draft |
Advisory AI draft of a bash, python, powershell, or go step script from a description. Operator role. |
| POST | /v1/ai/ask |
Advisory AI answer to a fleet question, from run, health, and drift metadata. Rate limited. |
| POST | /v1/ai/propose-run |
Turn a plain-language request into a run proposal, validated and held for approval. Operator role. |
| POST | /v1/drift/reconcile |
Build a reconcile proposal for a drifted host, held for approval. Operator role. |
| POST | /v1/pipelines |
Submit ordered playbook steps as one pipeline. |
| POST | /v1/schedules |
Cron schedule for a run, split, pipeline, or template. |
| GET | /v1/schedules |
List schedules. |
| GET | /v1/schedules/{id} |
One schedule. |
| PUT | /v1/schedules/{id} |
Update a schedule. |
| DELETE | /v1/schedules/{id} |
Delete a schedule. |
| GET | /v1/fleet |
Hosts ranked by failures over recent runs, flaky flags. |
| GET | /v1/hosts/{host}/runs |
One host's recent per-run outcomes. |
| GET | /v1/tasks |
Per-task duration trends over recent runs. |
| GET | /v1/drift |
Resources drifting from desired state, from dry runs. |
| POST | /v1/projects |
Register a git project. Runs record their commit. |
| GET | /v1/projects |
List projects. |
| PUT | /v1/projects/{id} |
Update a project. |
| DELETE | /v1/projects/{id} |
Delete a project. 409 while a template or source uses it. |
| POST | /v1/templates |
Save a launch preset. |
| GET | /v1/templates |
List templates. |
| POST | /v1/templates/{id}/launch |
Launch a template, answering its survey and choosing selectable credentials if it has them. |
| PUT | /v1/templates/{id} |
Update a template. |
| DELETE | /v1/templates/{id} |
Delete a template. |
| POST | /v1/triggers |
Create a webhook trigger, returns a signing secret once. |
| PUT | /v1/triggers/{id} |
Rename a trigger or toggle signature enforcement. |
| POST | /v1/triggers/{id}/rotate-secret |
Rotate the signing secret, shown once. |
| GET | /v1/triggers |
List webhook triggers. |
| DELETE | /v1/triggers/{id} |
Delete a trigger, revoking its webhook. |
| POST | /hooks/{token} |
Fire a trigger from a git push. A required HMAC signature is checked first. |
| POST | /v1/credentials |
Store a credential, encrypted at rest. Fourteen built-in kinds, or a custom type via type_id and fields. Non-secret settings ride beside the secret and return from the API. |
| GET | /v1/credentials |
List credentials, secrets never included. |
| POST | /v1/credential-types |
Define a custom credential type: fields and how they inject. Admin only. |
| GET | /v1/credential-types |
List custom credential types. Admin only. |
| PUT | /v1/credential-types/{id} |
Replace a custom credential type. Admin only. |
| DELETE | /v1/credential-types/{id} |
Delete a custom credential type. Admin only. |
| PUT | /v1/credentials/{id} |
Update a credential. |
| DELETE | /v1/credentials/{id} |
Delete a credential. 409 while an object still uses it. |
| POST | /v1/auth/login |
Sign in with username and password, returns a token. |
| POST | /v1/auth/check |
Verify an API token. |
| GET | /v1/auth/me |
Who the server resolved the caller to be. |
| POST | /v1/auth/logout |
End the caller's own session, revoking its token. |
| POST | /v1/tokens |
Mint a token bound to an account. Returns it once. |
| GET | /v1/tokens |
List tokens without secrets. Admin only. |
| DELETE | /v1/tokens/{id} |
Revoke a token everywhere at once. Admin only. |
| GET | /auth/oidc/login |
Start the OpenID Connect sign-in handshake. |
| GET | /auth/oidc/callback |
Complete the OIDC handshake and issue a token. |
| GET | /auth/saml/login |
Start the SAML sign-in handshake. |
| POST | /auth/saml/acs |
Consume the IdP assertion and issue a token. |
| GET | /auth/saml/metadata |
Service provider metadata for IdP registration. |
| POST | /v1/users |
Create an account with a role and an optional profile. |
| GET | /v1/users |
List accounts with their profiles, admin only. |
| PUT | /v1/users/{id} |
Update an account's role, password, or profile. |
| DELETE | /v1/users/{id} |
Delete an account. Its tokens stop working. |
| POST | /v1/teams |
Create a team of users. |
| GET | /v1/teams |
List teams. |
| DELETE | /v1/teams/{id} |
Delete a team and its memberships. |
| POST | /v1/teams/{id}/members |
Add a user to a team. |
| GET | /v1/teams/{id}/members |
List a team's members. |
| DELETE | /v1/teams/{id}/members/{userID} |
Remove a user from a team. |
| POST | /v1/orgs |
Create an organization. |
| GET | /v1/orgs |
List organizations. |
| DELETE | /v1/orgs/{id} |
Delete an organization and its memberships. |
| POST | /v1/orgs/{id}/members |
Add a user to an organization with an organization role. |
| GET | /v1/orgs/{id}/members |
List an organization's members and their roles. |
| DELETE | /v1/orgs/{id}/members/{userID} |
Remove a user from an organization. |
| POST | /v1/grants |
Grant a user or team read, use, or manage on an object. |
| GET | /v1/grants |
List access grants. |
| DELETE | /v1/grants/{id} |
Delete an access grant. |
| GET | /v1/workers |
The executor fleet with lease freshness. |
| POST | /v1/inventory-sources |
Register a dynamic inventory source. |
| GET | /v1/inventory-sources |
List inventory sources. |
| POST | /v1/inventory-sources/{id}/refresh |
Refresh a source into its inventory now. |
| PUT | /v1/inventory-sources/{id} |
Update an inventory source. |
| DELETE | /v1/inventory-sources/{id} |
Delete an inventory source. |
| POST | /v1/inventories |
Store an inventory. Runs reference it by id anywhere. |
| GET | /v1/inventories |
List stored inventories. |
| PUT | /v1/inventories/{id} |
Update a stored inventory. |
| DELETE | /v1/inventories/{id} |
Delete a stored inventory. |
| POST | /v1/policies |
Create a policy that holds or denies matching runs. |
| GET | /v1/policies |
List approval policies. |
| PUT | /v1/policies/{id} |
Update an approval policy. |
| DELETE | /v1/policies/{id} |
Delete an approval policy. |
| POST | /v1/import/{format} |
Import an AWX, Semaphore, or Rundeck export. Format is awx, semaphore, or rundeck. Rundeck takes ?inventory= to say which hosts its jobs target. |
| GET | /v1/audit |
A page of the mutation trail, admin only. ?limit= up to 1000, default 100; has_more reports whether older entries remain. |
| GET | /v1/audit/verify |
Verify the audit hash chain is intact. |
| GET | /v1/audit/bundle |
The audit chain as a signed LoomSeal bundle, verifiable offline or on the /verify page. |
| GET | /metrics |
Prometheus series: run, fleet, queue-depth, and worker gauges, plus a run-duration histogram. |
| GET | /healthz |
Liveness. |
| GET | /readyz |
Readiness: 200 once the store answers, 503 while it does not. |
A streamed export whose status line has already been sent cannot report a later failure with a
status code. The run event NDJSON download and the run log download therefore end with a
{"export_incomplete":true,"reason":"..."} line when they stop early, so a short file is never
mistaken for a whole one.
An account carries an optional profile alongside its role: full_name, email, phone, title,
links, and notes. All of them are optional, so an account created by the CLI or provisioned over
single sign-on stays valid with none of them set. title is descriptive and grants nothing; role
alone decides what an account may do.
curl -X PUT https://switchtender.example.com/v1/users/user_9f2c \
-H "Authorization: Bearer $SWITCHTENDER_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"username": "ada",
"role": "operator",
"full_name": "Ada Lovelace",
"email": "ada@example.com",
"phone": "+1 555 0100",
"title": "Platform Engineer",
"links": ["https://wiki.example.com/people/ada"],
"notes": "review each quarter"
}'The profile is replaced wholesale on update, so send the profile you want to end up with rather than only the parts that changed. An omitted field clears.
A profile is personal data and is treated as such. Only an admin may read it. /v1/users requires
the admin role and is not delegable by a manage grant. The values are never written to the logs, and
a rejection names the offending field without echoing it. Each single-line field is capped at 320
characters, notes at 2000, and an account may carry at most eight links. A link must be an http
or https address; any other scheme is refused, because the admin page renders links as anchors.
A template may cap how long its launches are allowed to execute with timeout, a whole number of
seconds. It is accepted on create and update, and every launch of the template carries it onto the
run, whether the launch came from the API, a schedule, or a webhook trigger.
curl -X POST https://switchtender.example.com/v1/templates \
-H "Authorization: Bearer $SWITCHTENDER_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "nightly database backup",
"playbook": "plays/backup.yml",
"timeout": 5400
}'Zero, or the field omitted, leaves launches on the server default set by --run-timeout, so a
template saved before this field existed is unchanged. A run that exceeds its timeout is canceled
and finalized as failed. A launch cannot raise the cap; the template's value is what applies.
A run submission and a template both accept the Ansible controls that used to require a hand-built command. They ride onto a run the same way from the API, a schedule, or a webhook trigger, and a retry keeps them.
| Field | Type | What it does |
|---|---|---|
limit |
string | Narrows the run to the hosts matching this pattern. Becomes --limit. Empty targets the whole inventory. |
tags |
list of strings | Runs only the plays and tasks carrying one of these tags. Becomes --tags. |
skip_tags |
list of strings | Skips the plays and tasks carrying one of these tags. Becomes --skip-tags. |
forks |
integer | How many hosts Ansible addresses at once. Zero leaves the Ansible default. Becomes --forks. |
verbosity |
integer 0 to 4 | Raises Ansible logging. One through four becomes -v through -vvvv; a higher number is clamped to four. |
diff_mode |
boolean | Shows the before and after of every changed file and template. Becomes --diff. |
curl -X POST https://switchtender.example.com/v1/runs \
-H "Authorization: Bearer $SWITCHTENDER_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"playbook": "plays/deploy.yml",
"inventory": "prod",
"limit": "canary01",
"tags": ["web", "config"],
"skip_tags": ["reboot"],
"forks": 25,
"verbosity": 2,
"diff_mode": true
}'These apply to the Ansible tool. The other tools ignore them, so a Bash or Terraform template that carries one is unaffected.
A run submission also accepts extra_vars, an object of variables injected into the run. Ansible
receives them the way --extra-vars supplies them, and a plugin tool reads them as its input. A
template carries its own extra_vars and a launch may merge more over them.
A template may carry steps, a pipeline graph, instead of a single tool. Such a
template is a saved workflow: every path that fires a template, a launch, a schedule, or a webhook
trigger, runs the graph as a pipeline, and the template's survey answers and extra vars reach every
step. A workflow template sets no top-level playbook, command, tool, shards, or Ansible
controls, since each step names its own; the graph is validated when the template is saved, so a
cycle or an unknown dependency is refused then rather than on every launch.
curl -X POST https://switchtender.example.com/v1/templates -H "Authorization: Bearer $SWITCHTENDER_TOKEN" -H 'Content-Type: application/json' -d '{
"name": "build and ship",
"inventory": "prod",
"steps": [
{"name": "build", "tool": "bash", "command": "make release"},
{"name": "deploy", "playbook": "deploy.yml", "depends_on": ["build"]}
]
}'A template survey field accepts bounds beyond its type, checked at launch before any answer becomes
an extra var. A field also takes an optional help string shown beneath its prompt, and a
multiline type for a block of text such as a set of variables or a note.
| Field kind | Constraints |
|---|---|
int |
min and max bound the answer, inclusive. |
text, multiline |
min_length and max_length bound the length; pattern is a regular expression the whole answer must match. |
choice |
The answer must be one of choices. |
A launch that violates a constraint is refused with the field it failed, and no run is submitted.
A template or a run submission may carry notifications, a list of targets that receive its
terminal state in addition to the server-wide channels. Each target names a kind and the field
that kind is addressed by, plus an optional on_failure that limits the target to failed runs.
| Kind | Required fields | What is sent |
|---|---|---|
webhook |
url |
The finished run as JSON, extra vars redacted. |
slack, mattermost, rocketchat |
url |
A message to the incoming webhook. |
discord, teams |
url |
A message or Adaptive Card to the webhook. |
ntfy |
url |
A notification to the topic, raised priority on failure. |
pagerduty |
key |
An incident trigger on the routing key, failed and interrupted runs only. |
grafana |
url, key |
An annotation to that instance's annotations API with that token. |
twilio |
to |
An SMS to that recipient through the server-held Twilio account. |
email |
to |
Mail to that comma-separated recipient list through the server SMTP transport. |
curl -X POST https://switchtender.example.com/v1/templates \
-H "Authorization: Bearer $SWITCHTENDER_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"name": "prod deploy",
"playbook": "plays/deploy.yml",
"notifications": [
{"kind": "slack", "url": "https://hooks.slack.com/services/T0/B1/x"},
{"kind": "pagerduty", "key": "R0UTINGKEY", "on_failure": true},
{"kind": "email", "to": "oncall@example.com, lead@example.com"}
]
}'A malformed target is refused at create or update with the field it lacks, not dropped at delivery. A Twilio or email target names only a recipient. The account credentials stay in server flags, so a template never carries them. On read, webhook URLs, PagerDuty routing keys, and Grafana tokens come back masked; an edit that echoes the mask back keeps the stored value.
A schedule reads its cron expression in the server's local time unless it carries a timezone, an
IANA name such as America/New_York or Europe/Berlin. With one set, 0 2 * * * fires at 02:00
in that zone and follows its daylight-saving shifts, so a nightly window stays put across the year.
The field is accepted on create and update and applies to the same expression the preview endpoint
renders.
curl -X POST https://switchtender.example.com/v1/schedules \
-H "Authorization: Bearer $SWITCHTENDER_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"cron":"0 2 * * *","timezone":"America/New_York","template_id":"tpl_abc123"}'/v1/fleet and /v1/tasks take a window, the number of recent runs per host or per task the
view considers, and /v1/hosts/{host}/runs takes a limit. All three default to 10. The window is
capped at 100 and the host history limit at 500; a larger value is answered with the cap, and the
response echoes the window it actually used. The caps exist because the per-host and per-task
summaries are kept when their runs are deleted, so on a long-lived fleet the tables hold a row for
every host of every run, and every row a window admits becomes an element of the answer.
The same tables are bounded by count rather than by age. --retain-history keeps the newest N
summaries for each host and each task and drops the rest, so a host's outcome history still
outlives its runs without the tables growing forever. N is never allowed below 500, the deepest
window these endpoints will answer, so trimmed history is history no request could have reached.
curl -s "https://switchtender.example.com/v1/fleet?window=30" \
-H "Authorization: Bearer $SWITCHTENDER_TOKEN"With --worker-token set, the server also serves the mesh relay under /relay: the execution
path an outbound worker started with --server uses instead of a database connection. Every call
presents the worker bearer token.
| Method | Path | What |
|---|---|---|
| GET | /relay/v1/policies |
Read the approval policies in force. |
| POST | /relay/v1/claim |
Lease the oldest pending run for the caller. |
| POST | /relay/v1/heartbeat |
Renew the lease on a run. |
| GET | /relay/v1/runs/{id} |
Fetch one run. |
| POST | /relay/v1/runs/{id}/save |
Save the run's state. |
| POST | /relay/v1/runs/{id}/log |
Append captured output. |
| POST | /relay/v1/runs/{id}/events |
Append structured events. |
| POST | /relay/v1/runs/{id}/propose-apply |
Report a plan's findings so the control node holds its apply. |
| POST | /relay/v1/runs/{id}/host-summary |
Save the run's per-host summaries. |
| POST | /relay/v1/runs/{id}/host-facts |
Save the facts the run gathered per host. |
| POST | /relay/v1/runs/{id}/task-summary |
Save the run's per-task summaries. |
Each report call is bounded twice. It presents the per-claim capability the claim response issued, so it can only write to the run this worker holds, and one call carries at most a few thousand items, so a worker cannot force an unbounded decode on the control node; the worker sends a wide run's evidence in several calls rather than losing it to that cap. Host facts are bounded further: a worker may write facts only for hosts its run has already reported results for, so nothing can be recorded about a machine no run claims to have touched.
What that does and does not give you: a worker authors its own results, so a worker you do not trust can still describe its own run untruthfully. What it cannot do is reach past that run into the recorded state of the rest of the fleet. Give a queue only to workers you would let touch the hosts that queue targets.