# DashCaddy API Surface

> **Generated:** 2026-07-13  
> **Total routes:** 285  
> **Files scanned:** 47  
> **Source of truth:** router.* registrations in `dashcaddy-api/routes/` + root paths in `src/app.js`

## Auth & Rate Limit Model

**Auth classification:**
- `public` = in `PUBLIC_ROUTES` allowlist (`src/utilities/middleware.js:310-364`), bypasses TOTP
- `protected` = requires valid TOTP session cookie (`dashcaddy_session`) OR API key/JWT token

**Rate limits** (from `RATE_LIMITS` in `src/utilities/constants.js:69`):
- `GENERAL` = 1000 req / 15 min / IP — default for all `/api/v1/*`
- `STRICT` = 20 req / 15 min / IP — auth key endpoints (`/auth/keys`, `/auth/jwt`, `/auth/gate`, `/auth/app-token`)
- `TOTP` = 10 req / 15 min / IP — TOTP verify/setup

**CSRF:** TOTP session uses double-submit cookie pattern. State-changing requests (POST/PUT/DELETE/PATCH) require `X-CSRF-Token` header matching the `csrf_token` cookie.

---

## Summary by Area

| Area | Routes | Public | Protected |
|---|---:|---:|---:|
| App catalog | 28 | 0 | 28 |
| Tailscale | 20 | 20 | 0 |
| Backups | 19 | 0 | 19 |
| DNS | 19 | 0 | 19 |
| Monitoring | 19 | 3 | 16 |
| Updates | 16 | 6 | 10 |
| Authentication | 15 | 10 | 5 |
| Logs | 15 | 0 | 15 |
| Configuration | 13 | 9 | 4 |
| Health | 12 | 2 | 10 |
| Services | 12 | 4 | 8 |
| Containers (lifecycle) | 10 | 0 | 10 |
| Core / system | 9 | 6 | 3 |
| Dependencies | 8 | 0 | 8 |
| Notifications | 8 | 0 | 8 |
| App recipes | 8 | 0 | 8 |
| Docker resources | 7 | 0 | 7 |
| Caddy / sites | 7 | 0 | 7 |
| Updates / workflows | 6 | 0 | 6 |
| Auto-restart | 5 | 0 | 5 |
| Certificate authority | 5 | 5 | 0 |
| OpenClaw integration | 5 | 0 | 5 |
| Config drift | 4 | 0 | 4 |
| Licensing | 4 | 2 | 2 |
| File browser | 3 | 0 | 3 |
| Theming | 3 | 1 | 2 |
| Service credentials | 2 | 0 | 2 |
| Events | 2 | 0 | 2 |
| Internal helpers | 1 | 0 | 1 |
| **TOTAL** | **285** | **68** | **217** |

## App catalog

_28 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| DELETE | `/api/v1/:appId` | protected | GENERAL (1000/15m) | `routes/apps/removal.js:39` |
| GET | `/api/v1/:appId/backup-points` | protected | GENERAL (1000/15m) | `routes/apps/restore.js:131` |
| POST | `/api/v1/:appId/restore` | protected | GENERAL (1000/15m) | `routes/apps/restore.js:38` |
| POST | `/api/v1/:appId/revert/:filename` | protected | GENERAL (1000/15m) | `routes/apps/restore.js:185` |
| POST | `/api/v1/arr/auto-setup` | protected | GENERAL (1000/15m) | `routes/arr/config.js:282` |
| POST | `/api/v1/arr/configure-overseerr` | protected | GENERAL (1000/15m) | `routes/arr/config.js:27` |
| GET | `/api/v1/arr/credentials` | protected | GENERAL (1000/15m) | `routes/arr/credentials.js:109` |
| POST | `/api/v1/arr/credentials` | protected | GENERAL (1000/15m) | `routes/arr/credentials.js:21` |
| DELETE | `/api/v1/arr/credentials/:service` | protected | GENERAL (1000/15m) | `routes/arr/credentials.js:134` |
| GET | `/api/v1/arr/detect` | protected | GENERAL (1000/15m) | `routes/arr/detect.js:20` |
| GET | `/api/v1/arr/quality-profiles` | protected | GENERAL (1000/15m) | `routes/arr/config.js:497` |
| POST | `/api/v1/arr/quality-profiles` | protected | GENERAL (1000/15m) | `routes/arr/config.js:566` |
| POST | `/api/v1/arr/smart-connect` | protected | GENERAL (1000/15m) | `routes/arr/smart-connect.js:26` |
| GET | `/api/v1/arr/smart-detect` | protected | GENERAL (1000/15m) | `routes/arr/detect.js:78` |
| POST | `/api/v1/arr/test-connection` | protected | GENERAL (1000/15m) | `routes/arr/config.js:208` |
| POST | `/api/v1/check-existing` | protected | GENERAL (1000/15m) | `routes/apps/deploy.js:241` |
| DELETE | `/api/v1/compose-stack/:stackName` | protected | GENERAL (1000/15m) | `routes/apps/compose.js:308` |
| POST | `/api/v1/deploy` | protected | GENERAL (1000/15m) | `routes/apps/deploy.js:254` |
| POST | `/api/v1/deploy-compose` | protected | GENERAL (1000/15m) | `routes/apps/compose.js:170` |
| POST | `/api/v1/import-compose` | protected | GENERAL (1000/15m) | `routes/apps/compose.js:159` |
| GET | `/api/v1/plex/libraries` | protected | GENERAL (1000/15m) | `routes/arr/plex.js:26` |
| GET | `/api/v1/ports/:basePort/suggest` | protected | GENERAL (1000/15m) | `routes/apps/templates.js:77` |
| GET | `/api/v1/ports/:port/check` | protected | GENERAL (1000/15m) | `routes/apps/templates.js:65` |
| POST | `/api/v1/restore-all` | protected | GENERAL (1000/15m) | `routes/apps/restore.js:58` |
| GET | `/api/v1/restore-status` | protected | GENERAL (1000/15m) | `routes/apps/restore.js:97` |
| GET | `/api/v1/templates` | protected | GENERAL (1000/15m) | `routes/apps/templates.js:45` |
| GET | `/api/v1/templates/:appId` | protected | GENERAL (1000/15m) | `routes/apps/templates.js:54` |
| POST | `/api/v1/update-subdomain` | protected | GENERAL (1000/15m) | `routes/apps/templates.js:91` |

## Tailscale

_20 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| GET | `/api/v1/tailscale/acl` | public | GENERAL (1000/15m) | `routes/tailscale.js:301` |
| GET | `/api/v1/tailscale/admin/devices` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:154` |
| DELETE | `/api/v1/tailscale/admin/devices/:id` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:170` |
| GET | `/api/v1/tailscale/admin/keys` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:202` |
| POST | `/api/v1/tailscale/admin/keys` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:211` |
| DELETE | `/api/v1/tailscale/admin/keys/:id` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:237` |
| GET | `/api/v1/tailscale/admin/users` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:191` |
| GET | `/api/v1/tailscale/api-devices` | public | GENERAL (1000/15m) | `routes/tailscale.js:274` |
| GET | `/api/v1/tailscale/check-connection` | public | GENERAL (1000/15m) | `routes/tailscale.js:96` |
| POST | `/api/v1/tailscale/config` | public | GENERAL (1000/15m) | `routes/tailscale.js:80` |
| GET | `/api/v1/tailscale/devices` | public | GENERAL (1000/15m) | `routes/tailscale.js:113` |
| DELETE | `/api/v1/tailscale/oauth-config` | public | GENERAL (1000/15m) | `routes/tailscale.js:259` |
| POST | `/api/v1/tailscale/oauth-config` | public | GENERAL (1000/15m) | `routes/tailscale.js:201` |
| POST | `/api/v1/tailscale/protect-service` | public | GENERAL (1000/15m) | `routes/tailscale.js:147` |
| DELETE | `/api/v1/tailscale/settings` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:124` |
| GET | `/api/v1/tailscale/settings` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:60` |
| PUT | `/api/v1/tailscale/settings` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:76` |
| POST | `/api/v1/tailscale/settings/test` | public | GENERAL (1000/15m) | `routes/tailscale-admin.js:131` |
| GET | `/api/v1/tailscale/status` | public | GENERAL (1000/15m) | `routes/tailscale.js:36` |
| POST | `/api/v1/tailscale/sync` | public | GENERAL (1000/15m) | `routes/tailscale.js:287` |

## Backups

_19 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| POST | `/api/v1/backups/backup/:appId` | protected | GENERAL (1000/15m) | `routes/backups.js:161` |
| POST | `/api/v1/backups/compare/:filename` | protected | GENERAL (1000/15m) | `routes/backups.js:373` |
| GET | `/api/v1/backups/config` | protected | GENERAL (1000/15m) | `routes/backups.js:480` |
| POST | `/api/v1/backups/config` | protected | GENERAL (1000/15m) | `routes/backups.js:486` |
| DELETE | `/api/v1/backups/credentials/:provider` | protected | GENERAL (1000/15m) | `routes/backups.js:631` |
| GET | `/api/v1/backups/credentials/:provider` | protected | GENERAL (1000/15m) | `routes/backups.js:558` |
| POST | `/api/v1/backups/credentials/:provider` | protected | GENERAL (1000/15m) | `routes/backups.js:590` |
| POST | `/api/v1/backups/execute` | protected | GENERAL (1000/15m) | `routes/backups.js:492` |
| GET | `/api/v1/backups/files` | protected | GENERAL (1000/15m) | `routes/backups.js:118` |
| GET | `/api/v1/backups/files/:appId` | protected | GENERAL (1000/15m) | `routes/backups.js:189` |
| GET | `/api/v1/backups/history` | protected | GENERAL (1000/15m) | `routes/backups.js:498` |
| POST | `/api/v1/backups/restore-file/:filename` | protected | GENERAL (1000/15m) | `routes/backups.js:237` |
| POST | `/api/v1/backups/restore/:backupId` | protected | GENERAL (1000/15m) | `routes/backups.js:538` |
| GET | `/api/v1/backups/schedule` | protected | GENERAL (1000/15m) | `routes/backups.js:29` |
| POST | `/api/v1/backups/schedule` | protected | GENERAL (1000/15m) | `routes/backups.js:59` |
| POST | `/api/v1/backups/schedule` | protected | GENERAL (1000/15m) | `routes/backups.js:511` |
| DELETE | `/api/v1/backups/schedule/:appId` | protected | GENERAL (1000/15m) | `routes/backups.js:102` |
| GET | `/api/v1/backups/storage-info` | protected | GENERAL (1000/15m) | `routes/backups.js:505` |
| POST | `/api/v1/backups/test-destination` | protected | GENERAL (1000/15m) | `routes/backups.js:546` |

## DNS

_19 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| GET | `/api/v1/dns/check-update` | protected | GENERAL (1000/15m) | `routes/dns.js:669` |
| DELETE | `/api/v1/dns/credentials` | protected | GENERAL (1000/15m) | `routes/dns.js:597` |
| POST | `/api/v1/dns/credentials` | protected | GENERAL (1000/15m) | `routes/dns.js:490` |
| GET | `/api/v1/dns/logs` | protected | GENERAL (1000/15m) | `routes/dns.js:337` |
| GET | `/api/v1/dns/propagation` | protected | GENERAL (1000/15m) | `routes/dns.js:802` |
| GET | `/api/v1/dns/propagation/:domain` | protected | GENERAL (1000/15m) | `routes/dns.js:847` |
| POST | `/api/v1/dns/propagation/verify` | protected | GENERAL (1000/15m) | `routes/dns.js:815` |
| GET | `/api/v1/dns/provider/status` | protected | GENERAL (1000/15m) | `routes/dns.js:55` |
| GET | `/api/v1/dns/providers` | protected | GENERAL (1000/15m) | `routes/dns.js:48` |
| DELETE | `/api/v1/dns/record` | protected | GENERAL (1000/15m) | `routes/dns.js:176` |
| POST | `/api/v1/dns/record` | protected | GENERAL (1000/15m) | `routes/dns.js:225` |
| POST | `/api/v1/dns/refresh-token` | protected | GENERAL (1000/15m) | `routes/dns.js:655` |
| GET | `/api/v1/dns/resolve` | protected | GENERAL (1000/15m) | `routes/dns.js:292` |
| POST | `/api/v1/dns/restart/:dnsId` | protected | GENERAL (1000/15m) | `routes/dns.js:621` |
| GET | `/api/v1/dns/token-status` | protected | GENERAL (1000/15m) | `routes/dns.js:474` |
| DELETE | `/api/v1/dns/universal/record` | protected | GENERAL (1000/15m) | `routes/dns.js:119` |
| POST | `/api/v1/dns/universal/record` | protected | GENERAL (1000/15m) | `routes/dns.js:71` |
| GET | `/api/v1/dns/universal/resolve` | protected | GENERAL (1000/15m) | `routes/dns.js:145` |
| POST | `/api/v1/dns/update` | protected | GENERAL (1000/15m) | `routes/dns.js:732` |

## Monitoring

_19 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| GET | `/api/v1/certificates` | protected | GENERAL (1000/15m) | `routes/ssl-monitor.js:26` |
| GET | `/api/v1/certificates/:serviceId` | protected | GENERAL (1000/15m) | `routes/ssl-monitor.js:35` |
| POST | `/api/v1/check` | protected | GENERAL (1000/15m) | `routes/ssl-monitor.js:50` |
| POST | `/api/v1/check/:serviceId` | protected | GENERAL (1000/15m) | `routes/ssl-monitor.js:59` |
| GET | `/api/v1/config` | public | GENERAL (1000/15m) | `routes/ssl-monitor.js:80` |
| POST | `/api/v1/config` | public | GENERAL (1000/15m) | `routes/ssl-monitor.js:90` |
| GET | `/api/v1/monitoring/aggregated/:containerId` | protected | GENERAL (1000/15m) | `routes/monitoring.js:72` |
| GET | `/api/v1/monitoring/alerts` | protected | GENERAL (1000/15m) | `routes/monitoring.js:104` |
| DELETE | `/api/v1/monitoring/alerts/:containerId` | protected | GENERAL (1000/15m) | `routes/monitoring.js:174` |
| GET | `/api/v1/monitoring/alerts/:containerId` | protected | GENERAL (1000/15m) | `routes/monitoring.js:168` |
| POST | `/api/v1/monitoring/alerts/:containerId` | protected | GENERAL (1000/15m) | `routes/monitoring.js:162` |
| POST | `/api/v1/monitoring/alerts/:containerId/test` | protected | GENERAL (1000/15m) | `routes/monitoring.js:111` |
| GET | `/api/v1/monitoring/alerts/config` | protected | GENERAL (1000/15m) | `routes/monitoring.js:85` |
| POST | `/api/v1/monitoring/alerts/config` | protected | GENERAL (1000/15m) | `routes/monitoring.js:91` |
| GET | `/api/v1/monitoring/history/:containerId` | protected | GENERAL (1000/15m) | `routes/monitoring.js:49` |
| GET | `/api/v1/monitoring/stats` | public | GENERAL (1000/15m) | `routes/monitoring.js:20` |
| GET | `/api/v1/monitoring/stats/:containerId` | protected | GENERAL (1000/15m) | `routes/monitoring.js:38` |
| GET | `/api/v1/stats/container/:id` | protected | GENERAL (1000/15m) | `routes/monitoring.js:240` |
| GET | `/api/v1/stats/containers` | protected | GENERAL (1000/15m) | `routes/monitoring.js:182` |

## Updates

_16 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| POST | `/api/v1/system/rollback` | protected | GENERAL (1000/15m) | `routes/updates.js:164` |
| GET | `/api/v1/system/rollback-versions` | protected | GENERAL (1000/15m) | `routes/updates.js:158` |
| POST | `/api/v1/system/update-apply` | protected | GENERAL (1000/15m) | `routes/updates.js:95` |
| GET | `/api/v1/system/update-check` | public | GENERAL (1000/15m) | `routes/updates.js:89` |
| GET | `/api/v1/system/update-history` | public | GENERAL (1000/15m) | `routes/updates.js:152` |
| POST | `/api/v1/system/update-notify` | public | GENERAL (1000/15m) | `routes/updates.js:126` |
| GET | `/api/v1/system/update-status` | public | GENERAL (1000/15m) | `routes/updates.js:143` |
| GET | `/api/v1/system/version` | public | GENERAL (1000/15m) | `routes/updates.js:83` |
| GET | `/api/v1/updates/auto-update` | protected | GENERAL (1000/15m) | `routes/updates.js:65` |
| POST | `/api/v1/updates/auto-update/:containerId` | protected | GENERAL (1000/15m) | `routes/updates.js:59` |
| GET | `/api/v1/updates/available` | public | GENERAL (1000/15m) | `routes/updates.js:29` |
| POST | `/api/v1/updates/check` | protected | GENERAL (1000/15m) | `routes/updates.js:22` |
| GET | `/api/v1/updates/history` | protected | GENERAL (1000/15m) | `routes/updates.js:49` |
| POST | `/api/v1/updates/rollback/:containerId` | protected | GENERAL (1000/15m) | `routes/updates.js:43` |
| POST | `/api/v1/updates/schedule/:containerId` | protected | GENERAL (1000/15m) | `routes/updates.js:71` |
| POST | `/api/v1/updates/update/:containerId` | protected | GENERAL (1000/15m) | `routes/updates.js:37` |

## Authentication

_15 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| GET | `/api/v1/auth/app-token/:serviceId` | public | STRICT (20/15m) | `routes/auth/sso-gate.js:104` |
| GET | `/api/v1/auth/gate/:serviceId` | public | STRICT (20/15m) | `routes/auth/sso-gate.js:26` |
| POST | `/api/v1/auth/jwt` | protected | STRICT (20/15m) | `routes/auth/keys.js:103` |
| GET | `/api/v1/auth/keys` | protected | STRICT (20/15m) | `routes/auth/keys.js:36` |
| POST | `/api/v1/auth/keys` | protected | STRICT (20/15m) | `routes/auth/keys.js:47` |
| DELETE | `/api/v1/auth/keys/:keyId` | protected | STRICT (20/15m) | `routes/auth/keys.js:81` |
| GET | `/api/v1/auth/login-page` | public | GENERAL (1000/15m) | `routes/auth/sso-gate.js:206` |
| GET | `/api/v1/totp/check-session` | public | TOTP (10/15m) | `routes/auth/totp.js:228` |
| GET | `/api/v1/totp/config` | public | TOTP (10/15m) | `routes/auth/totp.js:30` |
| POST | `/api/v1/totp/config` | public | TOTP (10/15m) | `routes/auth/totp.js:286` |
| POST | `/api/v1/totp/disable` | protected | TOTP (10/15m) | `routes/auth/totp.js:253` |
| GET | `/api/v1/totp/recovery-info` | public | TOTP (10/15m) | `routes/auth/totp.js:56` |
| POST | `/api/v1/totp/setup` | public | TOTP (10/15m) | `routes/auth/totp.js:116` |
| POST | `/api/v1/totp/verify` | public | TOTP (10/15m) | `routes/auth/totp.js:194` |
| POST | `/api/v1/totp/verify-setup` | public | TOTP (10/15m) | `routes/auth/totp.js:157` |

## Logs

_15 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| DELETE | `/api/v1/audit-logs` | protected | GENERAL (1000/15m) | `routes/errorlogs.js:71` |
| GET | `/api/v1/audit-logs` | protected | GENERAL (1000/15m) | `routes/errorlogs.js:55` |
| DELETE | `/api/v1/error-logs` | protected | GENERAL (1000/15m) | `routes/errorlogs.js:47` |
| GET | `/api/v1/error-logs` | protected | GENERAL (1000/15m) | `routes/errorlogs.js:20` |
| GET | `/api/v1/logs/container/:id` | protected | GENERAL (1000/15m) | `routes/logs.js:39` |
| GET | `/api/v1/logs/containers` | protected | GENERAL (1000/15m) | `routes/logs.js:23` |
| GET | `/api/v1/logs/digest/:date` | protected | GENERAL (1000/15m) | `routes/logs.js:184` |
| POST | `/api/v1/logs/digest/generate` | protected | GENERAL (1000/15m) | `routes/logs.js:176` |
| GET | `/api/v1/logs/digest/history` | protected | GENERAL (1000/15m) | `routes/logs.js:169` |
| GET | `/api/v1/logs/digest/latest` | protected | GENERAL (1000/15m) | `routes/logs.js:152` |
| GET | `/api/v1/logs/digest/live` | protected | GENERAL (1000/15m) | `routes/logs.js:162` |
| GET | `/api/v1/logs/docker-disk` | protected | GENERAL (1000/15m) | `routes/logs.js:203` |
| POST | `/api/v1/logs/docker-maintenance` | protected | GENERAL (1000/15m) | `routes/logs.js:211` |
| GET | `/api/v1/logs/file` | protected | GENERAL (1000/15m) | `routes/logs.js:218` |
| GET | `/api/v1/logs/stream/:id` | protected | GENERAL (1000/15m) | `routes/logs.js:93` |

## Configuration

_13 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| POST | `/api/v1/assets/upload` | protected | GENERAL (1000/15m) | `routes/config/assets.js:33` |
| GET | `/api/v1/backup/export` | protected | GENERAL (1000/15m) | `routes/config/backup.js:51` |
| POST | `/api/v1/backup/preview` | protected | GENERAL (1000/15m) | `routes/config/backup.js:153` |
| POST | `/api/v1/backup/restore` | protected | GENERAL (1000/15m) | `routes/config/backup.js:218` |
| DELETE | `/api/v1/config` | public | GENERAL (1000/15m) | `routes/config/settings.js:78` |
| GET | `/api/v1/config` | public | GENERAL (1000/15m) | `routes/config/settings.js:26` |
| POST | `/api/v1/config` | public | GENERAL (1000/15m) | `routes/config/settings.js:35` |
| DELETE | `/api/v1/favicon` | public | GENERAL (1000/15m) | `routes/config/assets.js:272` |
| GET | `/api/v1/favicon` | public | GENERAL (1000/15m) | `routes/config/assets.js:203` |
| POST | `/api/v1/favicon` | public | GENERAL (1000/15m) | `routes/config/assets.js:212` |
| DELETE | `/api/v1/logo` | public | GENERAL (1000/15m) | `routes/config/assets.js:170` |
| GET | `/api/v1/logo` | public | GENERAL (1000/15m) | `routes/config/assets.js:77` |
| POST | `/api/v1/logo` | public | GENERAL (1000/15m) | `routes/config/assets.js:112` |

## Health

_12 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| DELETE | `/api/v1/health-checks/:serviceId/configure` | protected | GENERAL (1000/15m) | `routes/health.js:357` |
| POST | `/api/v1/health-checks/:serviceId/configure` | protected | GENERAL (1000/15m) | `routes/health.js:351` |
| GET | `/api/v1/health-checks/:serviceId/stats` | protected | GENERAL (1000/15m) | `routes/health.js:340` |
| GET | `/api/v1/health-checks/incidents` | protected | GENERAL (1000/15m) | `routes/health.js:363` |
| GET | `/api/v1/health-checks/incidents/history` | protected | GENERAL (1000/15m) | `routes/health.js:371` |
| GET | `/api/v1/health-checks/status` | public | GENERAL (1000/15m) | `routes/health.js:319` |
| GET | `/api/v1/health/ca` | public | GENERAL (1000/15m) | `routes/health.js:267` |
| GET | `/api/v1/health/cached` | protected | GENERAL (1000/15m) | `routes/health.js:179` |
| GET | `/api/v1/health/probe` | protected | GENERAL (1000/15m) | `routes/health.js:230` |
| GET | `/api/v1/health/pylon` | protected | GENERAL (1000/15m) | `routes/health.js:245` |
| GET | `/api/v1/health/service/:id` | protected | GENERAL (1000/15m) | `routes/health.js:188` |
| GET | `/api/v1/health/services` | protected | GENERAL (1000/15m) | `routes/health.js:109` |

## Services

_12 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| DELETE | `/api/v1/seedhost-creds` | protected | GENERAL (1000/15m) | `routes/services.js:310` |
| GET | `/api/v1/seedhost-creds` | protected | GENERAL (1000/15m) | `routes/services.js:289` |
| POST | `/api/v1/seedhost-creds` | protected | GENERAL (1000/15m) | `routes/services.js:272` |
| GET | `/api/v1/services` | public | GENERAL (1000/15m) | `routes/services.js:374` |
| POST | `/api/v1/services` | public | GENERAL (1000/15m) | `routes/services.js:389` |
| PUT | `/api/v1/services` | public | GENERAL (1000/15m) | `routes/services.js:434` |
| DELETE | `/api/v1/services/:id` | protected | GENERAL (1000/15m) | `routes/services.js:462` |
| DELETE | `/api/v1/services/:serviceId/credentials` | protected | GENERAL (1000/15m) | `routes/services.js:237` |
| GET | `/api/v1/services/:serviceId/credentials` | protected | GENERAL (1000/15m) | `routes/services.js:252` |
| POST | `/api/v1/services/:serviceId/credentials` | protected | GENERAL (1000/15m) | `routes/services.js:213` |
| GET | `/api/v1/services/status` | public | GENERAL (1000/15m) | `routes/services.js:327` |
| POST | `/api/v1/services/update` | protected | GENERAL (1000/15m) | `routes/services.js:486` |

## Containers (lifecycle)

_10 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| DELETE | `/api/v1/containers/:id` | protected | GENERAL (1000/15m) | `routes/containers.js:235` |
| GET | `/api/v1/containers/:id/check-update` | protected | GENERAL (1000/15m) | `routes/containers.js:155` |
| GET | `/api/v1/containers/:id/logs` | protected | GENERAL (1000/15m) | `routes/containers.js:193` |
| GET | `/api/v1/containers/:id/resources` | protected | GENERAL (1000/15m) | `routes/containers.js:223` |
| PUT | `/api/v1/containers/:id/resources` | protected | GENERAL (1000/15m) | `routes/containers.js:205` |
| POST | `/api/v1/containers/:id/restart` | protected | GENERAL (1000/15m) | `routes/containers.js:48` |
| POST | `/api/v1/containers/:id/start` | protected | GENERAL (1000/15m) | `routes/containers.js:34` |
| POST | `/api/v1/containers/:id/stop` | protected | GENERAL (1000/15m) | `routes/containers.js:41` |
| POST | `/api/v1/containers/:id/update` | protected | GENERAL (1000/15m) | `routes/containers.js:55` |
| GET | `/api/v1/containers/discover` | protected | GENERAL (1000/15m) | `routes/containers.js:242` |

## Core / system

_9 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| GET | `/api/v1/docs` | protected | GENERAL (1000/15m) | `src/app.js:925` |
| GET | `/api/v1/docs/spec` | protected | GENERAL (1000/15m) | `src/app.js:943` |
| GET | `/api/v1/network/ips` | protected | GENERAL (1000/15m) | `src/app.js:899` |
| GET | `/health` | public | GENERAL (1000/15m) | `src/app.js:777` |
| GET | `/health/live` | public | GENERAL (1000/15m) | `src/app.js:778` |
| GET | `/health/ready` | public | GENERAL (1000/15m) | `src/app.js:782` |
| GET | `/healthz` | public | GENERAL (1000/15m) | `src/app.js:779` |
| GET | `/probe/:id` | public | GENERAL (1000/15m) | `src/app.js:786` |
| GET | `/readyz` | public | GENERAL (1000/15m) | `src/app.js:783` |

## Dependencies

_8 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| DELETE | `/api/v1/dependencies/:serviceId` | protected | GENERAL (1000/15m) | `routes/dependencies.js:166` |
| GET | `/api/v1/dependencies/:serviceId` | protected | GENERAL (1000/15m) | `routes/dependencies.js:80` |
| POST | `/api/v1/dependencies/:serviceId` | protected | GENERAL (1000/15m) | `routes/dependencies.js:123` |
| GET | `/api/v1/dependencies/:serviceId/chain` | protected | GENERAL (1000/15m) | `routes/dependencies.js:105` |
| POST | `/api/v1/dependencies/:serviceId/restart` | protected | GENERAL (1000/15m) | `routes/dependencies.js:198` |
| GET | `/api/v1/dependencies/:serviceId/status` | protected | GENERAL (1000/15m) | `routes/dependencies.js:114` |
| GET | `/api/v1/dependencies/graph` | protected | GENERAL (1000/15m) | `routes/dependencies.js:48` |
| GET | `/api/v1/dependencies/validate` | protected | GENERAL (1000/15m) | `routes/dependencies.js:56` |

## Notifications

_8 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| GET | `/api/v1/notifications/config` | protected | GENERAL (1000/15m) | `routes/notifications.js:20` |
| POST | `/api/v1/notifications/config` | protected | GENERAL (1000/15m) | `routes/notifications.js:53` |
| POST | `/api/v1/notifications/health-check` | protected | GENERAL (1000/15m) | `routes/notifications.js:214` |
| DELETE | `/api/v1/notifications/history` | protected | GENERAL (1000/15m) | `routes/notifications.js:208` |
| GET | `/api/v1/notifications/history` | protected | GENERAL (1000/15m) | `routes/notifications.js:192` |
| POST | `/api/v1/notifications/send` | protected | GENERAL (1000/15m) | `routes/notifications.js:246` |
| GET | `/api/v1/notifications/status` | protected | GENERAL (1000/15m) | `routes/notifications.js:224` |
| POST | `/api/v1/notifications/test` | protected | GENERAL (1000/15m) | `routes/notifications.js:159` |

## App recipes

_8 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| DELETE | `/api/v1/:recipeId` | protected | GENERAL (1000/15m) | `routes/recipes/manage.js:197` |
| POST | `/api/v1/:recipeId/restart` | protected | GENERAL (1000/15m) | `routes/recipes/manage.js:171` |
| POST | `/api/v1/:recipeId/start` | protected | GENERAL (1000/15m) | `routes/recipes/manage.js:108` |
| POST | `/api/v1/:recipeId/stop` | protected | GENERAL (1000/15m) | `routes/recipes/manage.js:139` |
| POST | `/api/v1/deploy` | protected | GENERAL (1000/15m) | `routes/recipes/deploy.js:29` |
| GET | `/api/v1/deployed` | protected | GENERAL (1000/15m) | `routes/recipes/manage.js:24` |
| GET | `/api/v1/templates` | protected | GENERAL (1000/15m) | `routes/recipes/index.js:34` |
| GET | `/api/v1/templates/:recipeId` | protected | GENERAL (1000/15m) | `routes/recipes/index.js:63` |

## Docker resources

_7 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| GET | `/api/v1/docker/disk-usage` | protected | GENERAL (1000/15m) | `routes/docker-resources.js:84` |
| GET | `/api/v1/docker/networks` | protected | GENERAL (1000/15m) | `routes/docker-resources.js:50` |
| POST | `/api/v1/docker/networks` | protected | GENERAL (1000/15m) | `routes/docker-resources.js:64` |
| DELETE | `/api/v1/docker/networks/:id` | protected | GENERAL (1000/15m) | `routes/docker-resources.js:76` |
| GET | `/api/v1/docker/volumes` | protected | GENERAL (1000/15m) | `routes/docker-resources.js:17` |
| POST | `/api/v1/docker/volumes` | protected | GENERAL (1000/15m) | `routes/docker-resources.js:30` |
| DELETE | `/api/v1/docker/volumes/:name` | protected | GENERAL (1000/15m) | `routes/docker-resources.js:42` |

## Caddy / sites

_7 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| GET | `/api/v1/caddy/cas` | protected | GENERAL (1000/15m) | `routes/sites.js:57` |
| GET | `/api/v1/caddy/config` | protected | GENERAL (1000/15m) | `routes/sites.js:31` |
| POST | `/api/v1/caddy/reload` | protected | GENERAL (1000/15m) | `routes/sites.js:38` |
| GET | `/api/v1/caddyfile` | protected | GENERAL (1000/15m) | `routes/sites.js:25` |
| POST | `/api/v1/site` | protected | GENERAL (1000/15m) | `routes/sites.js:160` |
| DELETE | `/api/v1/site/:domain` | protected | GENERAL (1000/15m) | `routes/sites.js:135` |
| POST | `/api/v1/site/external` | protected | GENERAL (1000/15m) | `routes/sites.js:188` |

## Updates / workflows

_6 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| GET | `/api/v1/workflows/workflows` | protected | GENERAL (1000/15m) | `routes/workflows.js:22` |
| POST | `/api/v1/workflows/workflows/:workflowId/disable` | protected | GENERAL (1000/15m) | `routes/workflows.js:35` |
| POST | `/api/v1/workflows/workflows/:workflowId/enable` | protected | GENERAL (1000/15m) | `routes/workflows.js:28` |
| GET | `/api/v1/workflows/workflows/:workflowId/history` | protected | GENERAL (1000/15m) | `routes/workflows.js:52` |
| POST | `/api/v1/workflows/workflows/:workflowId/run` | protected | GENERAL (1000/15m) | `routes/workflows.js:42` |
| GET | `/api/v1/workflows/workflows/history` | protected | GENERAL (1000/15m) | `routes/workflows.js:60` |

## Auto-restart

_5 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| GET | `/api/v1/policies` | protected | GENERAL (1000/15m) | `routes/auto-restart.js:30` |
| DELETE | `/api/v1/policies/:serviceId` | protected | GENERAL (1000/15m) | `routes/auto-restart.js:103` |
| GET | `/api/v1/policies/:serviceId` | protected | GENERAL (1000/15m) | `routes/auto-restart.js:39` |
| POST | `/api/v1/policies/:serviceId` | protected | GENERAL (1000/15m) | `routes/auto-restart.js:60` |
| POST | `/api/v1/policies/:serviceId/test` | protected | GENERAL (1000/15m) | `routes/auto-restart.js:123` |

## Certificate authority

_5 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| GET | `/api/v1/ca/cert/:domain` | public | GENERAL (1000/15m) | `routes/ca.js:127` |
| GET | `/api/v1/ca/certs` | public | GENERAL (1000/15m) | `routes/ca.js:242` |
| GET | `/api/v1/ca/info` | public | GENERAL (1000/15m) | `routes/ca.js:15` |
| GET | `/api/v1/ca/install-script` | public | GENERAL (1000/15m) | `routes/ca.js:63` |
| GET | `/api/v1/ca/root.crt` | public | GENERAL (1000/15m) | `routes/ca.js:45` |

## OpenClaw integration

_5 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| DELETE | `/api/v1/openclaw/` | protected | GENERAL (1000/15m) | `routes/openclaw.js:244` |
| POST | `/api/v1/openclaw/deploy` | protected | GENERAL (1000/15m) | `routes/openclaw.js:150` |
| GET | `/api/v1/openclaw/proxy/*` | protected | GENERAL (1000/15m) | `routes/openclaw.js:216` |
| POST | `/api/v1/openclaw/proxy/*` | protected | GENERAL (1000/15m) | `routes/openclaw.js:230` |
| GET | `/api/v1/openclaw/status` | protected | GENERAL (1000/15m) | `routes/openclaw.js:116` |

## Config drift

_4 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| POST | `/api/v1/fix` | protected | GENERAL (1000/15m) | `routes/config-drift.js:51` |
| GET | `/api/v1/last` | protected | GENERAL (1000/15m) | `routes/config-drift.js:39` |
| POST | `/api/v1/polling` | protected | GENERAL (1000/15m) | `routes/config-drift.js:66` |
| GET | `/api/v1/report` | protected | GENERAL (1000/15m) | `routes/config-drift.js:30` |

## Licensing

_4 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| POST | `/api/v1/license/activate` | protected | GENERAL (1000/15m) | `routes/license.js:16` |
| POST | `/api/v1/license/deactivate` | protected | GENERAL (1000/15m) | `routes/license.js:41` |
| GET | `/api/v1/license/feature/:feature` | public | GENERAL (1000/15m) | `routes/license.js:52` |
| GET | `/api/v1/license/status` | public | GENERAL (1000/15m) | `routes/license.js:35` |

## File browser

_3 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| GET | `/api/v1/browse/directories` | protected | GENERAL (1000/15m) | `routes/browse.js:52` |
| GET | `/api/v1/browse/roots` | protected | GENERAL (1000/15m) | `routes/browse.js:34` |
| GET | `/api/v1/media/detected-mounts` | protected | GENERAL (1000/15m) | `routes/browse.js:137` |

## Theming

_3 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| GET | `/api/v1/themes` | public | GENERAL (1000/15m) | `routes/themes.js:40` |
| DELETE | `/api/v1/themes/:slug` | protected | GENERAL (1000/15m) | `routes/themes.js:65` |
| POST | `/api/v1/themes/:slug` | protected | GENERAL (1000/15m) | `routes/themes.js:45` |

## Service credentials

_2 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| GET | `/api/v1/credentials/list` | protected | GENERAL (1000/15m) | `routes/credentials.js:15` |
| POST | `/api/v1/credentials/rotate-key` | protected | GENERAL (1000/15m) | `routes/credentials.js:21` |

## Events

_2 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| GET | `/api/v1/events/clients` | protected | GENERAL (1000/15m) | `routes/events.js:154` |
| GET | `/api/v1/events/stream` | protected | GENERAL (1000/15m) | `routes/events.js:126` |

## Internal helpers

_1 routes_

| Method | Full path | Auth | Rate limit | Defined in |
|---|---|---|---|---|
| GET | `/api/v1/status` | protected | GENERAL (1000/15m) | `routes/context.js:8` |

## Mount Point Map

How `src/app.js` wires route files to URL prefixes (via `apiRouter.use`):

| Route file(s) | Mounted at |
|---|---|
| `routes/ca` | `/api/v1/ca` |
| `routes/containers` | `/api/v1/containers` |
| `routes/dependencies` | `/api/v1/dependencies` |
| `routes/dns` | `/api/v1/dns` |
| `routes/docker-resources` | `/api/v1/docker` |
| `routes/events` | `/api/v1/events` |
| `routes/license` | `/api/v1/license` |
| `routes/notifications` | `/api/v1/notifications` |
| `routes/openclaw` | `/api/v1/openclaw` |
| `routes/recipes/` | `/api/v1/recipes` |
| `routes/tailscale` | `/api/v1/tailscale` |
| `routes/tailscale-admin` | `/api/v1/tailscale` |
| `routes/workflows` | `/api/v1/workflows` |
| `routes/auth/` | `/api/v1 (root)` |
| `routes/config/` | `/api/v1 (root)` |
| `routes/services` | `/api/v1 (root)` |
| `routes/health` | `/api/v1 (root)` |
| `routes/monitoring` | `/api/v1 (root)` |
| `routes/updates` | `/api/v1 (root)` |
| `routes/sites` | `/api/v1 (root)` |
| `routes/credentials` | `/api/v1 (root)` |
| `routes/arr/` | `/api/v1 (root)` |
| `routes/apps/` | `/api/v1 (root)` |
| `routes/logs` | `/api/v1 (root)` |
| `routes/backups` | `/api/v1 (root)` |
| `routes/browse` | `/api/v1 (root)` |
| `routes/errorlogs` | `/api/v1 (root)` |
| `routes/themes` | `/api/v1 (root)` |
| `routes/auto-restart` | `/api/v1 (root)` |
| `routes/config-drift` | `/api/v1 (root)` |
| `routes/ssl-monitor` | `/api/v1 (root)` |

## PUBLIC_ROUTES Allowlist

Source: `src/utilities/middleware.js:310-364` (42 entries)

| Method | Path | Match |
|---|---|---|
| ANY | `/health` | exact |
| ANY | `/health/live` | exact |
| ANY | `/health/ready` | exact |
| ANY | `/healthz` | exact |
| ANY | `/readyz` | exact |
| ANY | `/probe/` | prefix |
| ANY | `/api/v1/tailscale/` | prefix |
| ANY | `/api/v1/totp/config` | exact |
| ANY | `/api/v1/totp/recovery-info` | exact |
| ANY | `/api/v1/totp/verify` | exact |
| ANY | `/api/v1/totp/setup` | exact |
| ANY | `/api/v1/totp/verify-setup` | exact |
| ANY | `/api/v1/totp/check-session` | exact |
| ANY | `/api/v1/auth/gate/` | prefix |
| ANY | `/api/v1/auth/app-token/` | prefix |
| ANY | `/api/v1/auth/login-page` | exact |
| ANY | `/api/v1/services` | exact |
| ANY | `/api/v1/ca/info` | exact |
| ANY | `/api/v1/ca/root.crt` | exact |
| ANY | `/api/v1/ca/install-script` | exact |
| ANY | `/api/v1/health/ca` | exact |
| GET | `/api/v1/ca/cert/` | prefix |
| ANY | `/api/v1/ca/certs` | exact |
| ANY | `/api/v1/csrf-token` | exact |
| ANY | `/api/v1/logo` | exact |
| ANY | `/api/v1/favicon` | exact |
| ANY | `/api/v1/themes` | exact |
| ANY | `/api/v1/license/status` | exact |
| GET | `/api/v1/license/feature/` | prefix |
| ANY | `/api/v1/config` | exact |
| ANY | `/api/v1/services/status` | exact |
| ANY | `/api/v1/health-checks/status` | exact |
| ANY | `/api/v1/monitoring/stats` | exact |
| ANY | `/api/v1/system/version` | exact |
| ANY | `/api/v1/system/update-status` | exact |
| ANY | `/api/v1/system/update-history` | exact |
| ANY | `/api/v1/system/update-check` | exact |
| ANY | `/api/v1/updates/available` | exact |
| ANY | `/api/v1/system/update-notify` | exact |
| ANY | `/api/v1/monitoring/stats` | exact |
| ANY | `/api/v1/health-checks/status` | exact |
| ANY | `/api/v1/version` | exact |

## Root-Level Endpoints (defined directly in src/app.js)

| Method | Path | Auth | Purpose |
|---|---|---|---|
| GET | `/health` | public | Liveness (alias for `/health/live`) |
| GET | `/health/live` | public | Process-only check, no I/O |
| GET | `/health/ready` | public | Checks config + services + Docker + Caddy-admin (3s timeout each) |
| GET | `/healthz` | public | k8s alias for `/health/live` |
| GET | `/readyz` | public | k8s alias for `/health/ready` |
| GET | `/probe/:id` | public | Per-service health probe, sets `X-DashCaddy-HealthCheck: 1` |
| GET | `/api/v1/network/ips` | protected | Detected network interfaces + IPs (cached) |
| GET | `/api/v1/docs` | protected | Interactive Swagger UI |
| GET | `/api/v1/docs/spec` | protected | Raw OpenAPI 3.0.3 spec |
| GET | `/api/v1/version` | public (per PUBLIC_ROUTES) | API version |

## OpenAPI Spec Cross-Check

- Routes defined in code: **236**
- Paths in `openapi.yaml`: **112**

### In code but NOT documented in OpenAPI (142)

- `/api/v1/:appId`
- `/api/v1/:appId/backup-points`
- `/api/v1/:appId/restore`
- `/api/v1/:appId/revert/:filename`
- `/api/v1/:recipeId`
- `/api/v1/:recipeId/restart`
- `/api/v1/:recipeId/start`
- `/api/v1/:recipeId/stop`
- `/api/v1/arr/quality-profiles`
- `/api/v1/audit-logs`
- `/api/v1/auth/jwt`
- `/api/v1/auth/keys`
- `/api/v1/auth/keys/:keyId`
- `/api/v1/auth/login-page`
- `/api/v1/backups/backup/:appId`
- `/api/v1/backups/compare/:filename`
- `/api/v1/backups/credentials/:provider`
- `/api/v1/backups/files`
- `/api/v1/backups/files/:appId`
- `/api/v1/backups/restore-file/:filename`
- `/api/v1/backups/schedule`
- `/api/v1/backups/schedule/:appId`
- `/api/v1/backups/storage-info`
- `/api/v1/backups/test-destination`
- `/api/v1/browse/directories`
- `/api/v1/ca/cert/:domain`
- `/api/v1/ca/certs`
- `/api/v1/ca/info`
- `/api/v1/ca/install-script`
- `/api/v1/ca/root.crt`
- ... and 112 more

### Documented but NOT in code (18)

- `/api/v1/apps/:appId`
- `/api/v1/apps/check-existing`
- `/api/v1/apps/check-port/:port`
- `/api/v1/apps/deploy`
- `/api/v1/apps/suggest-port/:basePort`
- `/api/v1/apps/templates`
- `/api/v1/apps/templates/:appId`
- `/api/v1/apps/update-subdomain`
- `/api/v1/audit-log`
- `/api/v1/browse/dir`
- `/api/v1/caddy/get-cas`
- `/api/v1/health`
- `/api/v1/health-check/configure/:serviceId`
- `/api/v1/health-check/incidents`
- `/api/v1/health-check/incidents/history`
- `/api/v1/health-check/stats/:serviceId`
- `/api/v1/health-check/status`
- `/api/v1/service-creds/:serviceId`
