The yard command is a thin client for this API. Anything the command does, a script can do with curl.
Auth#
Every /api/v1/ call needs Authorization: Bearer <token>. The admin token is in tokens/admin.token on the host. yard token new <name> makes another one.
The hooks /hooks/origin and /hooks/github take no token. The GitHub hook verifies X-Hub-Signature-256 and answers 401 on a bad signature. A delivery only schedules a check. Yard then reads the tip from the forge and never trusts the event body.
curl -H "Authorization: Bearer $TOKEN" https://yard.example.com/api/v1/statusErrors#
An error is one JSON document:
{"error": {"code": "not_found", "message": "...", "help": "...", "example": "...", "context": {"name": "..."}}}code and message are always present. help, example, and context are optional.
Routes#
GET /health 200, emptyPOST /hooks/origin Origin events; 204, or 400/413 on a bad bodyPOST /hooks/github GitHub events; 401 on a bad signature, 400/413 on a bad body, else 204 GET /api/v1/status ?running=1 → {host, rows}GET /api/v1/doctor → [{name, ok, detail}]GET /api/v1/logs ?container&lines&since&until&follow → text/plainGET /api/v1/events ?app&container&source&event&lines&since&until&follow → JSON or NDJSONPOST /api/v1/prune {days?: 7, dry?: false} → {images, freed, days, dry} GET /api/v1/apps → [app]POST /api/v1/apps {name, repo, ref?, trigger?} → {app}GET /api/v1/apps/{name} → {app, env_file, rows, binds, deploys, log}PATCH /api/v1/apps/{name} any of {repo, ref, trigger} → {app}DELETE /api/v1/apps/{name} → {name}POST /api/v1/apps/{name}/deploy {sha?} → {app, deploy} GET /api/v1/apps/{name}/env → {KEY: VALUE}PATCH /api/v1/apps/{name}/env {KEY: VALUE | null} → {keys}PUT /api/v1/apps/{name}/env {KEY: VALUE} → {keys} GET /api/v1/forges → [forge]PUT /api/v1/forges/origin {app_id} → {forge, app}PUT /api/v1/forges/github {app_id, private_key, webhook_secret} → {forge, app} GET /api/v1/binds → [bind]PUT /api/v1/binds/{host} {dial, try?} → bindDELETE /api/v1/binds/{host} → {host} GET /api/v1/tokens → [token]POST /api/v1/tokens {name} → {name, token}DELETE /api/v1/tokens/{name} → {name}PATCH on the env sets each key and removes each key whose value is null. PUT replaces the whole file.
Apps#
An app is {name, repo, ref, trigger, created}. repo is a full https://<host>/<owner>/<name> URL, and the host is github.com or origin.cursor.com. ref is a branch, main by default. trigger is push by default, or manual.
GET /api/v1/apps/{name} returns the full app page. app adds image and sha from the app container. The other keys are env_file, rows, binds, deploys, and log.
The env never appears in an app response. It has its own routes.
Status rows#
A status row describes one container:
{app, container, service, state, running, health, answers, restarts, exit_code, started, exited, id, oom, error, image, sha, tip, binds, trigger, cpu, mem, data, deploy}idis the 12-character container id.answersistrue,false, ornull: the HTTP probe answered, was refused, or did not apply.tipis the last remote tip that a check saw.bindsis a list of{host, dial}.oomanderrorcome from the inspect of a stopped container.
Deploys#
A deploy row is the deploy-<name> container: {app, sha, image, status, started, ended}. status is running, ok, or failed. A leftover up-<name> container from an older host reads the same way.
POST /api/v1/apps/{name}/deploy returns the outcome. Its status is one of:
ok— the deploy finished. The new image is live.reverted— the new container never got healthy. The previous image runs.pending— a Yard self-upgrade runs detached. This process does not wait for it.skipped—YARD_SKIP_DEPLOYis set. Nothing ran.
A failed deploy is not a status here: the route answers a 500 deploy_failed error whose message is the deploy log.
sha deploys one commit instead of the ref tip. A full 40-character sha deploys as it is. A prefix resolves against past deploys: one match deploys, and no match or several matches is an error.
Logs and events#
GET /api/v1/logs reads one container's journal. container is a name, a 12-character id, or a full id; anything else is a 400. follow=1 streams.
GET /api/v1/events merges Podman and Yard events into one timeline. follow=1 streams NDJSON. An event row omits empty keys:
{time, source, event, app, container, container_id, image, exit_code, oom, actor, sha, status, message, fields}fields holds the remaining Yard event fields. No field carries a secret: an env change logs key names only.
Forges and binds#
A forge row is {name, host, connected, app_id, installation_id, selection, last_delivery, last_check, public_key?, slug?, error?}. Keys and secrets never appear in a response.
A bind row is {host, dial, id, state, try}. dial is container:port. try is how long Caddy retries the upstream, 10 seconds by default.