# Workspace API

Everything in a workspace that isn't a job or a run: API keys, secrets, alerting, statistics and the audit log, plus the server's health endpoints.

## Identity and workspace

`GET /api/v1/me` returns the user and accessible workspaces. `GET /api/v1/workspaces/{ws}` returns `id`, `name`, `slug`, `plan` and `created_at`; `PATCH` accepts `name` and `slug`. The workspace must match the authenticated workspace; other IDs return `404`.

## API keys

| Endpoint | Description |
|---|---|
| `GET /api/v1/api-keys` | List keys: `id`, `name`, `prefix` (e.g. `cp_ab12cd34`), `scopes`, `created_at`, `last_used_at`, `revoked_at` |
| `POST /api/v1/api-keys` | Create: `{"name": "ci", "scopes": ["jobs:read","runs:read"]}`. Response includes the full `key` once. |
| `DELETE /api/v1/api-keys/{id}` | Revoke. Takes effect immediately. |

## Secrets

| Endpoint | Description |
|---|---|
| `GET /api/v1/secrets` | List names with `created_at`/`updated_at`. Values are never returned. |
| `PUT /api/v1/secrets/{name}` | Create or replace: `{"value": "…"}` |
| `DELETE /api/v1/secrets/{name}` | Delete |

See [Secrets](../secrets/index.md).

## Alert channels

| Endpoint | Description |
|---|---|
| `GET /api/v1/alert-channels` | List |
| `POST /api/v1/alert-channels` | Create. Email channels start unverified. Webhook channels return a `signing_secret` once. |
| `GET /api/v1/alert-channels/{id}` | Read one channel |
| `GET /api/v1/alert-channels/verify?token=…` | Verify an address using its signed email link (no login required) |
| `POST /api/v1/alert-channels/{id}/verify` | Resend confirmation links |
| `PATCH /api/v1/alert-channels/{id}` | Update |
| `DELETE /api/v1/alert-channels/{id}` | Delete (rules referencing it stop sending to it) |
| `POST /api/v1/alert-channels/{id}/test` | Send a test alert |

See [Alert channels](../alerts/channels.md).

## Alert rules

| Endpoint | Description |
|---|---|
| `GET /api/v1/alert-rules` | List |
| `POST /api/v1/alert-rules` | Create |
| `GET /api/v1/alert-rules/{id}` | Read one rule |
| `PATCH /api/v1/alert-rules/{id}` | Update |
| `DELETE /api/v1/alert-rules/{id}` | Delete |

See [Alert rules](../alerts/rules.md).

## Stats

`GET /api/v1/stats/overview?window=24h` (`1h`, `24h`, `7d`, `30d`):

```json
{
  "window": "24h",
  "jobs": {"active": 42, "paused": 3, "disabled": 1, "completed": 7},
  "runs": {"total": 18240, "succeeded": 18190, "failed": 50, "sla_breached": 4},
  "start_lag_ms": {"p50": 3, "p95": 9, "p99": 41},
  "failing_jobs": [{"id": "job_01J9…", "name": "crm-sync", "consecutive_failures": 6}]
}
```

## Audit log

`GET /api/v1/audit-log?action=job.pause&limit=50`. Each entry has `actor` (`user:…`, `key:…` or `system`), `action` (e.g. `job.create`, `job.update`, `job.pause`, `job.auto_disable`, `api_key.create`, `secret.update`), `target`, `details` and `created_at`. Entries are kept for 30 days by default.

## Health (no authentication, outside `/api/v1`)

| Endpoint | Returns |
|---|---|
| `GET /healthz` | `200` while the process is running. Use for liveness probes. |
| `GET /readyz` | `200` when both databases are writable, the queue file responds, the dispatcher is active and result writing is keeping up; `503` otherwise, with a JSON breakdown. Use for readiness probes and uptime checks. |
| `GET /version` | `{"version": "1.0.0", "commit": "…", "build_date": "…", "edition": "oss"}` |

```json
{"dispatcher": "active", "queue": true, "heartbeat": "2026-10-18T09:30:00Z", "writer_backpressure": false, "sqlite_writable": true}
```

`dispatcher` is one of `starting`, `active`, `standby` (another process holds the dispatcher lock) or `stopped`. `queue` is `false` when the queue file can't be read, `sqlite_writable` covers both databases, and `heartbeat` is the dispatcher's last tick.

## Usage and suspension

`GET /api/v1/usage` returns the workspace's UTC calendar-month run usage and effective limit:

```json
{"month":"2026-10","runs":1200,"limit":50000,"over_quota_policy":"block","reset_at":"2026-11-01T00:00:00Z"}
```

`limit` is `0` for unlimited. `over_quota_policy` is `block` or `allow`. `reset_at` is the start of the next UTC month. At a blocking cap, manual job triggers return `429` with `error.code: "limit_exceeded"` and `limit: "max_runs_per_month_per_workspace"`.

Suspended workspaces retain read access. Mutations return `403` with `error.code: "workspace_suspended"`; operators restore access with the `cronpad workspace unsuspend <slug>` command. Skipped runs carry a `skip_reason` (`quota_exceeded`, `workspace_suspended`, `previous_run_in_progress` or `misfire`) separate from delivery errors in `last_error`.