API overview
The API is a Fastify service you host yourself. There is no public endpoint — your base URL is wherever you deployed it.
https://<your-watcher-host>Authentication
Section titled “Authentication”Every route except a short public list requires a Bearer JWT.
-
Create an account (once, from the CLI, for the first admin):
Terminal window ADMIN_PASSWORD='a-long-random-password' pnpm users:create you@example.com --role admin --id local -
Exchange credentials for a token:
Terminal window TOKEN=$(curl -s https://<your-host>/auth/login \-H 'content-type: application/json' \-d '{"email":"you@example.com","password":"a-long-random-password"}' \| jq -r .token) -
Send it on every request:
Terminal window curl -s https://<your-host>/watches -H "authorization: Bearer $TOKEN"
Tokens are self-contained and last 7 days by default (AUTH_TOKEN_TTL). There is
no refresh token: when a token expires, sign in again.
Public routes
Section titled “Public routes”/health, /config/status, /auth/login, /auth/register, /auth/google,
/auth/verify-email, /auth/forgot-password, /auth/reset-password.
Everything else returns 401 without a valid token.
| Role | Can do |
|---|---|
user |
Everything scoped to their own data: watches, runs, feedback, trust rules, devices, AI assistance |
admin |
The above, plus /admin/* — key pools and model configuration |
A user calling an admin route gets 403.
Ownership scoping
Section titled “Ownership scoping”Errors
Section titled “Errors”Errors are JSON with an error message. Validation failures also include the
underlying issues.
{ "error": "Invalid watch", "issues": [ { "path": ["threshold"], "message": "..." } ] }| Status | Meaning |
|---|---|
400 |
The body or query failed validation |
401 |
Missing, malformed or expired token |
403 |
Authenticated, but the role is insufficient |
404 |
Not found, or not yours |
409 |
Conflict — a duplicate watch id, or a run-all already in flight |
422 |
A key was rejected by its live validation probe |
429 |
Rate limited |
502 |
An upstream model or search call failed |
503 |
A capability is not configured on this instance |
Rate limits
Section titled “Rate limits”In-memory fixed windows, per process:
| Scope | Limit |
|---|---|
/auth/* |
20 requests per minute per IP |
| AI assistance routes | 10 requests per minute per owner |
| Admin routes that probe a key | 10 requests per minute overall |
Exceeding one returns 429. Limits reset when the process restarts.
The API sends CORS headers only for origins listed in WEB_ORIGIN
(comma-separated). With none configured it sends no CORS headers, so only
same-origin and non-browser callers can reach it. There is no wildcard mode.
Content type
Section titled “Content type”Send content-type: application/json when there is a body, and omit it when
there is not — a bodyless POST declaring a JSON content type is rejected as an
empty body.
Checking an instance
Section titled “Checking an instance”curl -s https://<your-host>/health # {"status":"ok"}curl -s https://<your-host>/config/status # booleans onlyBoth are public. /config/status reveals which integrations are configured
(never their values), which is useful for support and worth knowing when you
decide whether to expose the API publicly.