Smart Agent Teams

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

EnvironmentBase URLNotes
Productionhttps://app.yarlis.comFirebase Hosting serves the web app and forwards /api/** to the API on Cloud Run
Staginghttps://sat-yarlis-staging.web.appSame setup as production
Local developmenthttp://localhost:8080The 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

TopicConvention
Content typeJSON 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 updatesPATCH changes only the fields present in the body. Send null to clear an optional field, such as a task's due_date
IdsUUID strings, for example "6bdb77a8-288a-44d5-ae29-f7beda9fd580"
Task referencesTask 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
MoneyInteger cents: monthly_budget_cents: 15000 is $150.00. A budget of 0 means no limit
TimestampsISO 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)
StatusesLower-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.

EndpointDefaultMaximumPaging
GET /companies/{company_id}/tasks5001000 (limit)None
GET /companies/{company_id}/runs100500 (limit)None. Newest first; transcripts are omitted
GET /companies/{company_id}/activity50200 (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:

EndpointLimit
GET /health100 per minute
POST /api/password/forgot5 per minute
POST /api/password/reset10 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 types

A 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.

list-tasks.ts
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

On this page