Base URL:
https://agentsky.dev
The authoritative, machine-readable contract for everything below is the OpenAPI document at:
https://agentsky.dev/api/v1/openapi.json
If a field or endpoint is not described here, the OpenAPI document is the source of truth — not this prose.
Core model
AgentSky has a small, fixed object model:
| Object | What it is | Lifecycle |
|---|---|---|
| Universe | An isolated tenancy: its own agents, sessions, secrets, and model subscriptions. | Created once; everything else lives inside one. |
| Agent | A reusable configuration: engine (agentType), model (llm), prompt, capabilities, secrets, install steps. | Created, patched, archived, or deleted. Runs zero or more sessions. |
| Session | One running conversation with a working directory. The pod materializes lazily on the first turn. | provisioning → idle / running → terminated. |
| Turn | One user message plus the agent's response. | Started by POST /sessions/{id}/messages; boundaries read from the stream. |
Nothing is provisioned when you create an agent — the pod and its working directory exist only after you start a session and send a turn.
Authentication
Every request is authorized with a bearer token that carries an ast_ prefix:
Authorization: Bearer ast_...
Tokens are scoped to a single universe and carry one or more scopes.
Each endpoint in the OpenAPI document declares the scope it requires:
read for reads, write for writes, admin for destructive or
universe-level actions (for example deleting an agent requires admin).
Scopes are hierarchical — read ⊂ write ⊂ admin — so a token that can
admin also implies write and read. You mint a token in the AgentSky
console (Settings → API tokens) or from the sky CLI with
sky auth login.
Send GET /api/v1/whoami first — it returns the resolved identity, universe,
and effective scopes for the token you hold, and is the cheapest way to
confirm a credential before building on it:
curl -sS https://agentsky.dev/api/v1/whoami \
-H "Authorization: Bearer $AGENTSKY_TOKEN"
{
"user": { "id": "u_...", "email": "you@example.com", "name": "You" },
"universe": { "slug": "acme", "name": "Acme", "isPersonal": false },
"scopes": ["read", "write", "admin"],
"auth": "token"
}
Notes:
universe.isPersonalistruefor a personal token. Only personal tokens can create universes (POST /api/v1/universes).- Secret values and subscription credentials are write-only: the API accepts them but never returns them.
- Rate limit: 120 requests per minute per token; exceed it and you get
HTTP
429with aRetry-Afterheader. - The human-readable documentation lives at
https://agentsky.dev/docs.
Agents
Create an agent
POST /api/v1/agents — every field is optional; an empty body {} creates a
default hermes agent. agentType selects the engine:
hermes (default) · claude_code · codex · openclaw · pi · dsh ·
kimi_code · opencode
curl -sS https://agentsky.dev/api/v1/agents \
-H "Authorization: Bearer $AGENTSKY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "support-bot",
"displayName": "Support Bot",
"agentType": "hermes",
"llm": "claude-fable-5",
"prompt": "You are a concise support assistant.",
"capabilities": ["exa.search", "gptimage.generate"]
}'
The response returns the full agent object including its generated slug
(a globally unique handle) and version (bumped on every update). You address
the agent by slug from here on.
Full CreateAgent fields:
| Field | Meaning |
|---|---|
name / displayName | Internal name (≤60 chars) and human-facing label (≤60 chars). |
description | ≤500 chars. |
agentType | Engine, one of the eight values above. |
llm | Model id, passed through to the engine; engine↔model mismatches are rejected rather than silently replaced. |
prompt | The user prompt layer (≤100000 chars). |
capabilities | Built-in tool grants — see the table below. |
instructions | Array of {name: "*.md", content} markdown files injected at startup. |
skills | Array of {name, url, source, version, config?, secretRefs?} packages loaded into the agent. |
customInstalls | Array of {command, description?, timeoutSeconds? (≤1800), allowNetwork?} shell steps run before the engine starts. |
customData | Array of {id, name, kind, scope?, description?, uri?, config?} data attachments. |
metadata | Your key-merged client metadata, echoed back on reads. |
Capabilities are not LLM tools — they are platform grants surfaced through
the in-pod actl CLI. The full enum:
exa.search · exa.contents · tinyfish.fetch · tinyfish.browser ·
dataforseo.serp · gptimage.generate · rembg.remove-background ·
seedance.generate · mm.i2v · fish-audio.transcribe
Read, update, archive, delete
GET /api/v1/agents # list agents in the resolved universe
GET /api/v1/agents/{slug} # agent detail (includes prompt, capabilities, metadata)
PATCH /api/v1/agents/{slug} # update displayName / capabilities / metadata / harnessVersion
POST /api/v1/agents/{slug}/archive # archive: read-only, sessions keep running, new sessions rejected
DELETE /api/v1/agents/{slug} # delete — only with zero sessions (409 agent_has_sessions otherwise)
PATCH supports displayName, capabilities, metadata, harnessVersion,
and expectedVersion (an optimistic-concurrency guard: pass the version you
last read, and the write fails with version_conflict if the agent has moved
on).
Prompt
PUT /api/v1/agents/{slug}/prompt # save + hot-reload the user prompt layer
GET /api/v1/agents/{slug}/prompt/versions # version history
PUT returns {version, unchanged, applied, sessions[]}. applied is
live, on-restart, or unreachable — it tells you, per session, whether the
new prompt hot-reloaded or will apply on the next restart.
Secrets
GET /api/v1/agents/{slug}/secrets # declared secret keys + descriptions — never values
PUT /api/v1/agents/{slug}/secrets/{key} # set a declared secret's value (write-only)
DELETE /api/v1/agents/{slug}/secrets/{key} # unset a secret
Secrets are injected into the agent process environment. Values are write-only: you can set and unset them, but the API never echoes them back.
Model subscriptions
A connected consumer AI subscription (Claude Pro/Max or a ChatGPT plan) can power eligible agents at $0 model usage.
GET /api/v1/model-subscriptions # list — metadata only, never credentials
PUT /api/v1/model-subscriptions/{provider} # connect/replace a credential (provider: anthropic | openai)
PATCH /api/v1/model-subscriptions/{provider} # update useForNewAgents / label
DELETE /api/v1/model-subscriptions/{provider} # disconnect — affected agents fail until reconnected or switched
A session opts in to account billing by setting modelBilling: "account"
(see Sessions below); the platform meters the turn otherwise.
Sessions
Create a session
POST /api/v1/sessions — cheap and lazy. Only agent is required; the pod
materializes on the first turn, so creation is effectively free.
curl -sS https://agentsky.dev/api/v1/sessions \
-H "Authorization: Bearer $AGENTSKY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"agent": "support-bot", "title": "First session"}'
A non-empty initial_events list (user.message only, all-or-nothing, ≤50)
starts the first turn in the same call:
curl -sS https://agentsky.dev/api/v1/sessions \
-H "Authorization: Bearer $AGENTSKY_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"agent": "support-bot",
"initial_events": [
{ "type": "user.message", "parts": [ { "index": 0, "type": "text", "text": "Hello" } ] }
]
}'
CreateSession fields: agent (required), eager, title (≤120 chars),
metadata, instructions (≤20 markdown files), initial_events, vcpus,
memoryMb, and modelBilling (platform | account). Machine shape
(vcpus / memoryMb) is per-session.
Read, update, delete
GET /api/v1/sessions # list sessions; ?agent= filters to one spec
GET /api/v1/sessions/{id} # detail — status is provisioning | idle | running | terminated
PATCH /api/v1/sessions/{id} # update title / metadata / modelBilling
DELETE /api/v1/sessions/{id} # end the session — tears down the pod, emits session.deleted
The session has a persistent working directory: files you write in one turn survive to the next. That directory is destroyed when the session is deleted.
Sending a turn
POST /api/v1/sessions/{id}/messages accepts a message and returns 202 with
an empty body — the result never comes back in this response. Output arrives
on the event stream (next section).
curl -sS -X POST https://agentsky.dev/api/v1/sessions/$SESSION_ID/messages \
-H "Authorization: Bearer $AGENTSKY_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: turn-7f3a" \
-d '{
"parts": [ { "index": 0, "type": "text", "text": "Summarize this quarter." } ]
}'
The request body requires parts (≥1). The optional Idempotency-Key header
dedupes retries — reuse the same key and a repeated send does not create a
second turn.
A turn is bounded by two turn.status_idle events on the stream: one at the
start (or from history) and one carrying the final stop_reason. The
stop_reason.type is end_turn when the turn finished normally, or
interrupted when it was aborted.
Interrupt
POST /api/v1/sessions/{id}/interrupt aborts the in-flight turn:
{ "status": "interrupting" }
status is interrupting when a turn is aborted, or no_turn when nothing
was in flight. The stream then emits turn.interrupted.
Logs
GET /api/v1/sessions/{id}/logs returns pod logs ({lines: [{at, severity, line}]}).
Pass follow=true to stream them as SSE.
Events
AgentSky has no HTTP webhooks. The event model is an SSE stream plus a pollable event history — the same event objects appear in both, so you can implement either a long-lived streaming client or a polling client on one contract.
The stream
GET /api/v1/sessions/{id}/stream is text/event-stream, live-only, and stays
open across turns. Event types:
| Event | Meaning |
|---|---|
user.message | A user message. History only — not sent on the live stream. |
agent.message | A completed agent message (messageId, parts, text). |
agent.reasoning | A reasoning delta (part). |
agent.tool_use | A tool invocation (part with call_id, tool_name, args). |
agent.tool_result | A tool result (part with call_id, tool_name, status, result). |
agent.status | A status update (part with level: thinking/working/waiting/idle/done). |
turn.status_idle | A turn boundary. stop_reason.type is end_turn or interrupted. Never break on a bare idle — read the stop_reason. |
turn.interrupted | The in-flight turn was aborted. |
session.deleted | Terminal — the session is gone. Close your client. |
error | An error (code, message, retryable). |
The history
GET /api/v1/sessions/{id}/events returns the session history, oldest-first,
with an opaque numeric cursor. Only user.message, agent.message, and
turn.status_idle are persisted; the live-only types (agent.reasoning,
agent.tool_use, agent.tool_result, agent.status, error) appear on the
stream but not in history.
curl -sS "https://agentsky.dev/api/v1/sessions/$SESSION_ID/events?limit=100" \
-H "Authorization: Bearer $AGENTSKY_TOKEN"
{
"events": [ { "id": "evt_...", "type": "agent.message", "sessionId": "sess_...", "parts": [...] } ],
"cursor": "evt_...",
"hasMore": false
}
Query parameters: cursor (resume), limit (≤500), and types[] filters —
types[]=user.message&types[]=agent.message is the transcript view.
Reconnect and dedupe
Every event carries a stable id. The reconnect contract is:
- Reopen the stream.
- List events (
GET .../events) from where you left off. - Dedupe by
id— an event you already processed is safe to ignore.
The same id is the dedupe key across the stream and the history, so a
streaming client and a polling fallback can share the same state.
Message parts
A message is a sequence of typed parts. Every part has an integer index
and a type:
type | Required extra fields |
|---|---|
text | text |
reasoning | text, redacted |
tool_call | call_id, tool_name, args, args_partial |
tool_result | call_id, tool_name, status (ok/error), result |
file | name, media_type, uri/data, size_bytes |
image | media_type, uri/data, alt, width, height |
video | media_type, uri/data, alt, width, height, duration_ms, size_bytes, thumbnail_uri |
status | level (thinking/working/waiting/idle/done), text |
error | code, message, retryable |
Media parts carry either a uri (a signed URL) or inline data, plus
metadata. Indexing is zero-based and defines display order.
Errors
Every error, on every endpoint, has one shape:
{ "error": { "code": "...", "message": "..." } }
The HTTP status distinguishes the class; code is the stable, machine-readable
value. Common ones:
| Status | code | Meaning |
|---|---|---|
| 401 | invalid_token | Missing, malformed, or unknown token. |
| 402 | insufficient_credits | The universe wallet cannot pay for the turn. |
| 409 | agent_has_sessions | Deleting an agent that still has sessions. |
| 409 | version_conflict | An optimistic expectedVersion no longer matches the agent. |
| 422 | invalid_spec | The payload is well-formed but invalid (for example an engine↔model mismatch). |
The OpenAPI document names agent_has_sessions and version_conflict
explicitly; the remaining codes above are returned at runtime under the same
{ error: { code, message } } shape. Treat retryable: true on an error
event as safe to retry; a bare error without that flag should be surfaced
to a human.
End-to-end example
A complete integration in five calls:
BASE=https://agentsky.dev
AUTH="Authorization: Bearer $AGENTSKY_TOKEN"
# 1. Confirm identity and scopes.
curl -sS $BASE/api/v1/whoami -H "$AUTH"
# 2. Create an agent.
curl -sS $BASE/api/v1/agents -H "$AUTH" -H "Content-Type: application/json" \
-d '{"name":"demo","agentType":"hermes","prompt":"Be concise."}'
# 3. Create a session and start the first turn in one call.
curl -sS $BASE/api/v1/sessions -H "$AUTH" -H "Content-Type: application/json" \
-d '{"agent":"demo","initial_events":[{"type":"user.message","parts":[{"index":0,"type":"text","text":"Say hello."}]}]}'
# 4. Open the event stream and read until turn.status_idle with stop_reason.type = "end_turn".
curl -sS -N $BASE/api/v1/sessions/$SESSION_ID/stream -H "$AUTH"
# 5. Tear down.
curl -sS -X DELETE $BASE/api/v1/sessions/$SESSION_ID -H "$AUTH"
Verification checklist
To confirm your integration before shipping, walk these in order and check the result of each:
GET /whoamireturns your universe and scopes without error.POST /agentsreturns a201with anagent.slugandversion.GET /agents/{slug}returns the configuration you created.POST /sessionsreturns a201with asession.idandstatus.POST /sessions/{id}/messagesreturns202with an empty body.GET /sessions/{id}/streamdeliversagent.messageand a terminalturn.status_idlewhosestop_reason.typeisend_turn.GET /sessions/{id}/eventsreturns the same events, dedupable byid.POST /sessions/{id}/interruptreturnsinterruptingduring a turn.DELETE /sessions/{id}returns success and the stream emitssession.deleted.
Endpoints at a glance
GET /api/v1/whoami
GET /api/v1/universes
POST /api/v1/universes
GET /api/v1/universes/{slug}
GET /api/v1/agents
POST /api/v1/agents
GET /api/v1/agents/{slug}
PATCH /api/v1/agents/{slug}
DELETE /api/v1/agents/{slug}
POST /api/v1/agents/{slug}/archive
PUT /api/v1/agents/{slug}/prompt
GET /api/v1/agents/{slug}/prompt/versions
GET /api/v1/agents/{slug}/secrets
PUT /api/v1/agents/{slug}/secrets/{key}
DELETE /api/v1/agents/{slug}/secrets/{key}
GET /api/v1/model-subscriptions
PUT /api/v1/model-subscriptions/{provider}
PATCH /api/v1/model-subscriptions/{provider}
DELETE /api/v1/model-subscriptions/{provider}
GET /api/v1/sessions
POST /api/v1/sessions
GET /api/v1/sessions/{id}
PATCH /api/v1/sessions/{id}
DELETE /api/v1/sessions/{id}
POST /api/v1/sessions/{id}/messages
GET /api/v1/sessions/{id}/events
GET /api/v1/sessions/{id}/stream
POST /api/v1/sessions/{id}/interrupt
GET /api/v1/sessions/{id}/logs