API Reference

Cross-cutting rules of the Appstrate API: context headers, versioning, rate limits, pagination, long polling, and realtime streams. Per-operation reference is generated from OpenAPI.

This page covers the conventions shared by every endpoint. Each operation's parameters, bodies, and responses are in the generated reference pages and in the live OpenAPI document, which is the source of truth.

Base URL

All endpoints are under /api on your instance:

http://localhost:3000/api
ResourcePathCredentials
OpenAPI 3.1 documentGET /api/openapi.jsonNone. Responds with an ETag; send If-None-Match to get 304
Swagger UIGET /api/docsNone
LLM-oriented indexGET /llms.txtNone

Browser clients on another origin must be listed in the instance's TRUSTED_ORIGINS. Cross-origin responses expose the headers described below (Request-Id, Appstrate-Version, RateLimit, Link, ETag, and others).

Authentication

API keys (apst_...), session cookies, and OIDC tokens are accepted. Context headers (X-Org-Id, X-Space-Id, Appstrate-User) select the organization, the space, and the end-user. All of it is in Authentication.

Versioning

The API uses date-based versions, sent in the Appstrate-Version header and echoed on every authenticated response:

Appstrate-Version: 2026-03-21
  • The version is resolved from the request header, then from the version pinned on the organization, then the current version, 2026-03-21. A header or a pin the server cannot serve is a 400 unsupported_api_version; it never silently falls back.
  • 2026-03-21 is currently the only version, and a version does not yet change any response: the header is a contract marker you can send now to pin your integration. No Deprecation or Sunset header is emitted yet.
  • The platform is still in beta (1.0.0-beta.x) and some releases change the wire contract on purpose, for example the ask_ to apst_ key format and the applications to spaces rename. Breaking changes are called out in the changelog.

Errors

Errors are RFC 9457 application/problem+json with a stable code and a request_id. Every response, success or error, carries a Request-Id header (req_...). See Errors.

Rate limiting

Rate limits are applied per route, per caller. The caller is the user, or the API key (apikey:<id>), and public routes are limited per IP. Limits are enforced with Redis when REDIS_URL is set and in memory otherwise.

Responses from a rate-limited route carry the IETF headers, on success as well as on 429:

RateLimit: limit=20, remaining=18, reset=45
RateLimit-Policy: 20;w=60

reset is the number of seconds until the window refills. A request over the limit is 429 with code: "rate_limited", a Retry-After header, and retry_after in the body.

RouteLimit
Launch a run (POST /api/agents/{scope}/{name}/run)20 per minute
Create a schedule10 per minute
Create a webhook10 per minute
Import a package10 per minute
Create an end-user60 per minute
List or read end-users, webhooks300 per minute
Run logs (GET /api/runs/{id}/logs)120 per minute
File reads (GET /api/files)120 per minute

Runs are also capped per organization across all callers and all launch paths, 200 per minute by default (PLATFORM_RUN_LIMITS), and by concurrency limits. Operators tune these in the environment variables and rate limits guides. The RateLimit-Policy header on each response is authoritative for that route.

Idempotency

A handful of POST operations accept an Idempotency-Key. Others reject it. See Idempotency.

Pagination

List endpoints return an envelope:

{
  "object": "list",
  "data": [{ "id": "…" }],
  "hasMore": true
}

Some also return total or limit. The style depends on the resource, and the operation's parameters tell you which:

StyleParametersUsed by
Offsetlimit, offset/api/runs, /api/agents/{scope}/{name}/runs, /api/schedules/{id}/runs, /api/integrations
Cursorlimit, startingAfter (and endingBefore for end-users)/api/end-users, /api/files, /api/notifications, /api/webhooks/{id}/deliveries, /api/chat/sessions
Sequencesince, limit/api/runs/{id}/logs

limit defaults to 20 on most lists (50 for an agent's runs, 100 for integrations and chat sessions, 1000 for run logs) and is capped at 100 (1000 for run logs). Out-of-range values on most lists fall back to the default instead of failing.

When another page follows, the response carries an RFC 8288 Link header with rel="next", and rel="prev", first, last where they apply. A generic client can follow next until it disappears, whatever the body shape. Cursor values are resource ids: pass the id of the last item you received.

Waiting for a run

POST /api/agents/{scope}/{name}/run answers 201 with the created run as soon as it is queued. It does not wait for the run to finish. To wait without polling, long-poll the run:

curl "http://localhost:3000/api/runs/$RUN_ID?wait=true" \
  -H "Authorization: Bearer $APPSTRATE_KEY"

wait takes a number of seconds or true, and is capped at 55 seconds, below the idle timeout of common proxies. The call returns as soon as the run reaches a terminal status (success, failed, timeout, cancelled) or when the wait elapses, in which case the status is still non-terminal: call again. Each caller can hold at most 10 waits at once. Past that, the call returns immediately.

Realtime streams

Server-Sent Events streams push run changes to a browser or a backend without polling.

EndpointStreams
GET /api/realtime/runs/{id}One run. The stream opens with a run_update snapshot of the current state
GET /api/realtime/agents/{packageId}/runsEvery run of one agent
GET /api/realtime/runsEvery run in the space

Authentication cannot use headers, because EventSource cannot send them:

  • API key: ?token=apst_.... The organization and space come from the key.
  • Session cookie: send the cookie, plus ?orgId=...&spaceId=....

On the agent stream, encode the package id as one path segment: @acme/email-daily-digest becomes %40acme%2Femail-daily-digest.

curl -N "http://localhost:3000/api/realtime/runs/$RUN_ID?token=$APPSTRATE_KEY"

Event names, as event: lines in the stream:

EventCarries
run_updateA run's status and timing changed
run_logOne log entry. By default without its data payload
run_metricRunning total of cost and token usage for a run
connection_updateOne of your own integration connections changed
chat_session_updateA change signal for one of your chat sessions, when the chat module is loaded. Needs a session
pingKeep-alive, immediately on connect and after every 30 seconds without another event

Parameters: channels=run_update,run_log limits the stream to those events (unknown names are ignored, and an empty result falls back to everything). verbose=true includes full payloads, such as log data. Leave it off for safer consumption.

Delivery rules to build around:

  • A caller receives only the runs it may read: every run in the space with runs:read-all, otherwise the runs it launched. Debug-level log entries need runs:delete.
  • There is no replay. Last-Event-ID is not honored; a reconnect resumes at the live tail, and anything missed is gone. After a reconnect, read the run with GET /api/runs/{id} and the logs with GET /api/runs/{id}/logs?since=....
  • A client that stops reading is dropped once about 2000 frames are queued for it. Reconnect and resynchronize the same way.
  • Frame ids look like <subscriber>:<n>. They are unique per connection and meant for deduplication, not for resuming.

Conditional requests

A resource that supports optimistic concurrency returns a strong ETag. Send it back in If-Match on the next write. A stale tag is 412 precondition_failed, and a missing mandatory one is 428 precondition_required. The package draft save requires it. Other resources are last write wins.

On this page