API

The HTTP API that the yard command talks to.

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/status

Errors#

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, empty
POST   /hooks/origin                    Origin events; 204, or 400/413 on a bad body
POST   /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/plain
GET    /api/v1/events                   ?app&container&source&event&lines&since&until&follow → JSON or NDJSON
POST   /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?}                  → bind
DELETE /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}
  • id is the 12-character container id.

  • answers is true, false, or null: the HTTP probe answered, was refused, or did not apply.

  • tip is the last remote tip that a check saw.

  • binds is a list of {host, dial}.

  • oom and error come 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_DEPLOY is 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.