# HTTP API

Base URL: the address in `listen`, e.g. `http://127.0.0.1:8080`. Requests and responses are JSON; timestamps are Unix seconds; run IDs are 32 hex characters.

## Authentication

Send the API token as a bearer token:

```sh
-H "Authorization: Bearer $SHIPYARD_API_TOKEN"
```

Queuing, cancelling and retrying always need it. When `runtime.api_token_env` is set, reads need it too. `/healthz` and `/readyz` are always open.

## Queue a run

```sh
curl -X POST http://127.0.0.1:8080/api/v1/runs \
  -H "Authorization: Bearer $SHIPYARD_API_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: nightly-check-2026-10-03' \
  -d '{"project_id":"my-app","kind":"pipeline","task":"project-check","input":""}'
```

| Field | Meaning |
|---|---|
| `project_id` | An enabled project. |
| `kind` | `diagnostic`, `pipeline` or `ai_text`. |
| `task` | Pipeline name for `pipeline`; AI role for `ai_text`. |
| `input` | Pipeline input or the prompt. Required for `ai_text`. |

`Idempotency-Key` (8–128 characters) is required. Sending the same key and body again returns the run already queued instead of a duplicate; the same key with a different body is `409`. Answer: `202` with `{"data": run}`.

## Runs

| Request | Returns |
|---|---|
| `GET /api/v1/runs?limit=50` | Newest runs first, up to 100 per page, and `next_cursor`. |
| `GET /api/v1/runs?before=<cursor>` | The next page; `next_cursor` is `null` on the last one. |
| `GET /api/v1/runs/{id}` | One run. |
| `POST /api/v1/runs/{id}/cancel` | Cancels a queued run, asks a running one to stop. |
| `POST /api/v1/runs/{id}/retry` | Queues a failed or cancelled run again as a new run. Needs a new `Idempotency-Key`. |

A run:

```json
{
  "id": "4bb3a90cb8f0a3bbc59125a2d3a992f8",
  "project_id": "my-app",
  "kind": "pipeline",
  "task": "project-check",
  "state": "succeeded",
  "attempt": 1,
  "created_at": 1791057353,
  "updated_at": 1791057353,
  "finished_at": 1791057353,
  "logs_expired": false
}
```

`state` is one of `queued`, `running`, `cancelling`, `succeeded`, `failed`, `cancelled`. A failed run carries `error`.

## Logs

```sh
curl -H "Authorization: Bearer $SHIPYARD_API_TOKEN" \
  "http://127.0.0.1:8080/api/v1/runs/$ID/logs?offset=0"
```

Returns up to 100 events and `next_offset`; pass it back as `offset` to read on. `attempt=N` reads an earlier attempt. Expired logs answer `410`.

```json
{"data": [{"time": "2026-10-03T18:55:53Z", "level": "info", "run_id": "4bb3…", "attempt": 1, "message": "run started"}], "next_offset": 112}
```

## Projects and pipelines

`GET /api/v1/projects` and `GET /api/v1/pipelines` return the configured projects and pipelines; prompts are never returned. `read_only` in the projects answer is `true` when the configuration came from a URL.

`POST /api/v1/projects` adds a project and writes it to the configuration file:

```sh
curl -X POST http://127.0.0.1:8080/api/v1/projects \
  -H "Authorization: Bearer $SHIPYARD_API_TOKEN" \
  -d '{"id":"firmware","repository":"you/firmware","enabled":true,"issue_label":"agent-ready",
       "release_interval":"48h","runs_on":["arch=arm64"],
       "content":{"enabled":false,"interval":"168h","min_score":70}}'
```

`409 project_exists` for a duplicate ID, `409 config_read_only` for a URL-loaded configuration, `400 invalid_project` with a `message` naming the problem.

## Ships

| Request | Returns |
|---|---|
| `GET /api/v1/ships` | Ships with labels, platform, `online` and `revoked`; `enabled` tells whether the fleet endpoint is on. |
| `POST /api/v1/ships/tokens` | A one-time join token and the `ship join` command. Body: `{"name": "garage-pi", "labels": ["arch=arm64"]}`. |
| `POST /api/v1/ships/{id}/revoke` | Revokes a ship. |

Ships themselves talk to Shipyard over gRPC with mutual TLS — see [Ships and docks](/ships/).

## Errors

Errors have a stable code:

```json
{"error": {"code": "run_not_found"}}
```

| Status | Codes |
|---|---|
| 400 | `invalid_input`, `invalid_project`, `invalid_name`, `invalid_label`, `invalid_cursor`, `invalid_attempt`, `invalid_offset`, `project_unavailable`, `pipeline_unavailable`, `model_or_input_unavailable`, `unsupported_kind`, `idempotency_key_required` |
| 401 | `unauthenticated` |
| 404 | `run_not_found`, `ship_not_found` |
| 409 | `idempotency_conflict`, `run_state_conflict`, `project_exists`, `config_read_only`, `fleet_disabled` |
| 410 | `logs_expired` |
| 500 | `storage_failed`, `enqueue_failed`, `logs_unavailable` |
| 503 | `storage_unavailable` |
| 507 | `log_budget_exceeded` |

Metrics and the Alertmanager webhook are described in [Monitoring](/monitoring/).
