API overview
Base URLs, authentication, data conventions, limits and the OpenAPI spec shared by every SAT endpoint.
The SAT REST API is what the web app, the sat CLI and runners use. Everything you can do in the product, you can do with it: manage companies, agents, tasks, goals, projects, runs, approvals and routines, read costs and the audit trail, and subscribe to live changes. This page covers the conventions every endpoint shares. The endpoint pages under Endpoints are generated from the OpenAPI spec.
Base URLs
| Environment | Base URL | Notes |
|---|---|---|
| Production | https://app.yarlis.com | Firebase Hosting serves the web app and forwards /api/** to the API on Cloud Run |
| Staging | https://sat-yarlis-staging.web.app | Same setup as production |
| Local development | http://localhost:8080 | The API started with bun run dev:api. See Run locally |
All paths start with /api, for example https://app.yarlis.com/api/companies. The health check is /health.
Live events need the API's direct origin
Firebase Hosting buffers streamed responses, so live events through app.yarlis.com arrive late or not at all. Open the event stream against the Cloud Run origin of the API instead. The CLI's prod profile already does this. Every other endpoint works through the Firebase origin.
Versioning
The API is not versioned yet. Paths carry no version, and there is no version header. The spec reports 2.1.0 as its version, which is informational. Changes are made in place, so pin to a commit of @sat/api-client if you need a stable contract, and watch the changelog.
Authentication
Send a bearer credential on every request except registration, sign-in, token refresh, password reset and /health:
GET /api/companies HTTP/1.1
Host: app.yarlis.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...The credential is either:
- an access token from
POST /api/login, valid for 30 minutes and renewed with the refresh token, or - a personal API key (
sat_live_...) for scripts, CI and runners. It does not expire unless you set an expiry.
Both authenticate as a person (a member). Company endpoints then check that this person is a member of the company. See Authentication.
Requests and responses
| Topic | Convention |
|---|---|
| Content type | JSON in and out (Content-Type: application/json). The live event stream is text/event-stream. Some deletes and an empty claim answer 204 No Content with no body |
| Partial updates | PATCH changes only the fields present in the body. Send null to clear an optional field, such as a task's due_date |
| Ids | UUID strings, for example "6bdb77a8-288a-44d5-ae29-f7beda9fd580" |
| Task references | Task endpoints take {task_ref}: the task's UUID or its key, such as NF-12. The prefix is matched case-insensitively. Other resources take UUIDs only; the CLI resolves names, the API does not |
| Money | Integer cents: monthly_budget_cents: 15000 is $150.00. A budget of 0 means no limit |
| Timestamps | ISO 8601 with a UTC offset, for example 2026-10-05T10:42:07.120000+00:00. Treat a timestamp without an offset as UTC (local SQLite databases store them that way) |
| Statuses | Lower-case strings, for example in_review or pending_approval. See Statuses |
Lists and pagination
List endpoints return a JSON array, not an envelope. There is no cursor pagination, except for the activity log.
| Endpoint | Default | Maximum | Paging |
|---|---|---|---|
GET /companies/{company_id}/tasks | 500 | 1000 (limit) | None |
GET /companies/{company_id}/runs | 100 | 500 (limit) | None. Newest first; transcripts are omitted |
GET /companies/{company_id}/activity | 50 | 200 (limit) | before: pass the created_at of the last row you have |
| Every other list (agents, goals, projects, approvals, routines, comments, members, companies) | All rows | — | None. Agents leave out terminated ones unless you pass include_terminated=true |
Errors
Errors use one envelope:
{"error": {"code": "RUN_LEASE_LOST", "message": "Run is cancelled", "details": {}}}Branch on code, not on message. Framework errors use the same envelope: request validation errors are 422 VALIDATION_ERROR, unknown paths 404 NOT_FOUND, wrong methods 405 METHOD_NOT_ALLOWED, a missing bearer credential 401 AUTH_REQUIRED, rate limiting 429 RATE_LIMITED and unexpected failures 500 INTERNAL_ERROR. Company resources you cannot see answer 404, never 403. See Errors for every code.
Rate limits
Most endpoints have no rate limit. These are limited per client IP address:
| Endpoint | Limit |
|---|---|
GET /health | 100 per minute |
POST /api/password/forgot | 5 per minute |
POST /api/password/reset | 10 per minute |
Over the limit, the API answers 429 with the error envelope, code RATE_LIMITED and a message such as Too many requests: 5 per 1 minute. Sign-in is not rate limited; instead, five wrong passwords in a row lock the account for 30 minutes. See Authentication.
CORS
Browsers can call the API from the origins listed in the API's ALLOWED_ORIGINS environment variable (comma separated). If it is unset, every origin is allowed. Production sets it to the web app's origins. Allowed methods are GET, POST, PUT, PATCH, DELETE and OPTIONS, with any request header. If you serve a web client from your own origin, or read live events from the API's direct origin, that origin must be in the list.
OpenAPI spec
The spec is checked in at libs/api-client/openapi.json. It is exported from the FastAPI app:
bun run --cwd libs/api-client generate # writes openapi.json and the TypeScript typesA running API serves the spec at /openapi.json and interactive docs at /docs only when it runs with DEBUG=true. The production API does not serve them.
The checked-in spec also contains legacy routes (/api/agents, /api/sessions, /api/users/..., /api/monitoring/..., /api/websocket/...). They are not part of the supported API, and this reference leaves them out.
Typed client
@sat/api-client (libs/api-client) is a TypeScript client generated from the spec, used by the web app and the CLI. It is a workspace package in the SAT repository, not published to npm. It wraps openapi-fetch with bearer authentication, a single shared token refresh on 401, and the error envelope as an ApiError.
import { createApiClient, type TokenStore } from '@sat/api-client';
const tokens: TokenStore = {
getAccessToken: () => process.env.SAT_API_KEY ?? null,
getRefreshToken: () => null,
setTokens: () => {},
clear: () => {},
};
const api = createApiClient({ baseUrl: 'https://app.yarlis.com', tokens });
const tasks = await api.call(() =>
api.client.GET('/api/companies/{company_id}/tasks', {
params: { path: { company_id: process.env.SAT_COMPANY_ID! }, query: { status: ['blocked'] } },
})
);
console.log(tasks.map((t) => `${t.key} ${t.title}`));api.call returns the data or throws ApiError with status, code and message. api.authedFetch is a plain fetch with the same authentication and refresh. For live events, see subscribeToCompanyEvents in Live events.
Endpoint groups
Authentication
Register, sign in, refresh, sign out and the current user.
API keys
Personal keys for runners, CI and scripts.
Companies
Companies and their members.
Agents
Hire, configure, pause, wake and terminate agents; the org chart.
Tasks
Tasks, keys like NF-12 and comment threads.
Goals
The goal tree.
Projects
Projects with repositories and budgets.
Runs
Agent executions and the runner protocol.
Approvals
Hires and budget overrides waiting for the board.
Routines
Recurring work on a cron schedule.
Insights
Dashboard, costs and the activity log.
Live events
The server-sent event stream.
Health
Liveness and readiness.