# API reference — one origin, one namespace

Everything lives under `https://api.agentsky.dev/v1` with a Bearer token. Management is request/response; conversation is a 202 send plus one standing SSE stream. Machine-readable spec (generated from the same schemas the routes validate with): https://api.agentsky.dev/v1/openapi.json.

## Authentication

Every request sends `Authorization: Bearer ast_…`. Tokens are minted in Settings → API tokens or by `sky auth login` — never by other tokens. Scopes are ordered `read ⊂ write ⊂ admin`; a token is personal (acts as you everywhere) or scoped to one universe. Pass `X-Universe: <slug>` to act inside a non-personal universe.

## Errors

Every non-2xx body is `{ "error": { "code", "message" } }`:

| HTTP | code | When |
|------|------|------|
| 400 | `invalid_request` | malformed JSON / validation failure |
| 401 | `invalid_token` | missing, unknown, revoked, or expired token |
| 402 | `insufficient_credits` | the spend gate blocked the turn |
| 403 | `insufficient_scope` · `universe_mismatch` · `forbidden` | scope too low; X-Universe conflicts with a scoped token; agent has no API binding |
| 404 | `not_found` | unknown agent/universe (also masks unauthorized slugs) |
| 409 | `conflict` | version conflict |
| 400 | `not_downloadable` | `GET /files/{file_id}/content` on an upload — only session files download |
| 400 | `file_is_session_resource` | `DELETE /files/{file_id}` on a copy that backs a live session resource — delete the resource instead |
| 400 | `too_many_resources` | more than 500 resources or 5 repositories on one session |
| 400 | `egress_header_conflict` | two secrets, or two repository tokens, set one header on one host with different values |
| 413 | `file_too_large` | a file upload over 32 MiB |
| 422 | `invalid_spec` | valid JSON, invalid domain rules (e.g. llm not allowed for type) |
| 429 | `rate_limited` | 120 requests/min per token exceeded — honor Retry-After |

## Identity

### GET /whoami

Caller identity, resolved universe, effective scopes (scope: read)

Responses:

- `200` — OK
  - `user` (object, required)
    - `id` (string)
    - `email` (string)
    - `name` (string | null)
  - `universe` (object, required)
    - `slug` (string)
    - `name` (string)
    - `isPersonal` (boolean)
  - `scopes` (enum<string>[], required) — "read" · "write" · "admin"
  - `auth` (enum<string>, required) — "token" · "session"
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/whoami \
  -H "Authorization: Bearer ast_..."
```

## Universes

### GET /universes

List universes (scope: read)

Responses:

- `200` — OK
  - `universes` (object[], required)
    - `slug` (string)
    - `name` (string)
    - `isPersonal` (boolean)
    - `createdAt` (string) — ISO 8601
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/universes \
  -H "Authorization: Bearer ast_..."
```

### POST /universes

Create a universe (scope: write, personal tokens only)

Body (application/json):

- `slug` (string, required) — max length 40 — pattern ^[a-z0-9][a-z0-9-]*[a-z0-9]$
- `name` (string) — max length 80

Responses:

- `201` — Created
  - `universe` (object, required)
    - `slug` (string)
    - `name` (string)
    - `isPersonal` (boolean)
    - `createdAt` (string) — ISO 8601
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/universes \
  -H "Authorization: Bearer ast_..." \
  -H "Content-Type: application/json" \
  -d '{"slug":"acme","name":"Acme"}'
```

### GET /universes/{slug}

Universe detail (scope: read)

Path parameters:

- `slug` (string, required) — The universe slug

Responses:

- `200` — OK
  - `universe` (object, required)
    - `slug` (string)
    - `name` (string)
    - `isPersonal` (boolean)
    - `createdAt` (string) — ISO 8601
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/universes/slug \
  -H "Authorization: Bearer ast_..."
```

## Agents

### GET /agents

List agents in the resolved universe (scope: read)

Responses:

- `200` — OK
  - `agents` (object[], required)
    - `id` (string) — The agent id (agent_…) — the only address
    - `name` (string)
    - `displayName` (string)
    - `agentType` (string) — Engine: claude_code · codex · hermes · openclaw · pi · dsh · kimi_code · opencode
    - `llm` (string)
    - `harnessVersion` (string | null) — The engine CLI version this agent's sessions run; PATCH to move to the platform's current version
    - `reasoningEffort` (string | null) — Harness-native reasoning effort; null = the harness's own default. Applies from the next provision/restart. Allowed values depend on agentType — hermes: none/minimal/low/medium/high/xhigh/max/ultra; claude_code: low/medium/high/xhigh/max; codex: none/minimal/low/medium/high/xhigh/max; openclaw: not supported; pi: off/minimal/low/medium/high/xhigh/max; dsh: not supported; kimi_code: not supported; opencode: none/minimal/low/medium/high/xhigh/max — "none" · "off" · "minimal" · "low" · "medium" · "high" · "xhigh" · "max" · "ultra" · null
    - `capabilities` (enum<string>[]) — The built-in capabilities this agent is granted — the exact set its sessions may execute (enforced; empty = none). Ids are listed at /docs/capabilities. — "exa.search" · "exa.contents" · "tinyfish.fetch" · "tinyfish.browser" · "dataforseo.serp" · "gptimage.generate" · "rembg.remove-background" · "seedance.generate" · "mm.i2v" · "fish-audio.transcribe"
    - `version` (integer) — Bumped on every update; see expectedVersion
    - `archived` (boolean)
    - `sessionCount` (integer)
    - `createdAt` (string) — ISO 8601
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/agents \
  -H "Authorization: Bearer ast_..."
```

### POST /agents

Create an agent (scope: write)

An agent is a reusable configuration — engine, prompt, capabilities, secrets. Every field is optional: an empty body creates the default hermes agent with no built-in capabilities (capabilities are explicit opt-in; unknown ids → 400). The response's `id` (agent_…) is the agent's only address. One agent can run many sessions; creating the agent provisions nothing — the pod is provisioned by POST /v1/sessions.

Body (application/json):

- `name` (string) — Prompt/display label only — NOT the handle. The addressable handle is the auto-minted `slug` in the response — max length 60
- `description` (string) — max length 500
- `agentType` (enum<string>) — "hermes" · "claude_code" · "codex" · "openclaw" · "pi" · "dsh" · "kimi_code" · "opencode"
- `llm` (string)
- `reasoningEffort` (enum<string>) — Harness-native reasoning effort; omitted = the harness's own default. Allowed values depend on agentType — hermes: none/minimal/low/medium/high/xhigh/max/ultra; claude_code: low/medium/high/xhigh/max; codex: none/minimal/low/medium/high/xhigh/max; openclaw: not supported; pi: off/minimal/low/medium/high/xhigh/max; dsh: not supported; kimi_code: not supported; opencode: none/minimal/low/medium/high/xhigh/max — "none" · "off" · "minimal" · "low" · "medium" · "high" · "xhigh" · "max" · "ultra"
- `prompt` (string) — max length 100000
- `displayName` (string) — Seeds the minted `slug` (the unique addressable handle, returned in the response); omitted = an auto-generated asteroid name — max length 60
- `capabilities` (enum<string>[]) — "exa.search" · "exa.contents" · "tinyfish.fetch" · "tinyfish.browser" · "dataforseo.serp" · "gptimage.generate" · "rembg.remove-background" · "seedance.generate" · "mm.i2v" · "fish-audio.transcribe" — default: []
- `instructions` (object[])
  - `name` (string, required) — pattern .*\.md$
  - `content` (string) — default: ""
- `skills` (object[]) — RFC-0073 skill attachments: store references ({type:'skill', skillId, version?: 'latest'|skr_…}) and GitHub shortcuts ({type:'github', url, ref?, tokenSecretRef?}). Max 20.
  - type: "skill" (object)
    - `type` ("skill", required)
    - `skillId` (string, required)
    - `version` (string) — default: "latest"
  - type: "github" (object)
    - `type` ("github", required)
    - `url` (string, required)
    - `ref` (string) — Branch or commit SHA; default branch when omitted.
    - `tokenSecretRef` (string) — Agent secret key holding a clone token for private repos.
- `mcpServers` (object[]) — Remote streamable-HTTP MCP servers this agent connects to.
  - `type` ("url") — default: "url"
  - `name` (string, required) — max length 255
  - `url` (string, required)
  - `tools` (object)
    - `include` (string[]) — default: []
    - `exclude` (string[]) — default: []
- `customData` (object[])
  - `id` (string, required)
  - `name` (string, required) — pattern ^[a-z0-9_]+$
  - `kind` (enum<string>, required) — "spreadsheet" · "doc" · "private_api" · "other"
  - `scope` (enum<string>) — "creator" · "user" — default: "creator"
  - `description` (string)
  - `uri` (string)
  - `config` (object) — default: {}
- `metadata` (object)

Responses:

- `201` — Created
  - `agent` (object, required) — Everything in the list item, plus configuration state.
    - `id` (string) — The agent id (agent_…) — the only address
    - `name` (string)
    - `displayName` (string)
    - `agentType` (string)
    - `llm` (string)
    - `harnessVersion` (string | null)
    - `reasoningEffort` (string | null) — "none" · "off" · "minimal" · "low" · "medium" · "high" · "xhigh" · "max" · "ultra" · null
    - `version` (integer)
    - `archived` (boolean)
    - `sessionCount` (integer)
    - `createdAt` (string)
    - `description` (string | null)
    - `prompt` (string | null) — The user prompt layer
    - `promptVersion` (integer | null)
    - `capabilities` (enum<string>[]) — The built-in capabilities this agent is granted — the exact set its sessions may execute (enforced; empty = none). Ids are listed at /docs/capabilities. — "exa.search" · "exa.contents" · "tinyfish.fetch" · "tinyfish.browser" · "dataforseo.serp" · "gptimage.generate" · "rembg.remove-background" · "seedance.generate" · "mm.i2v" · "fish-audio.transcribe"
    - `mcpServers` (object[]) — Declared remote MCP server references, including tool filters; runtime authentication fields are never returned
      - `type` ("url") — default: "url"
      - `name` (string, required) — max length 255
      - `url` (string, required)
      - `tools` (object)
        - `include` (string[]) — default: []
        - `exclude` (string[]) — default: []
    - `metadata` (object) — Your key-merged client metadata
    - `defaultSessionId` (string | null) — The session conversation verbs target when addressed by agent id
    - `universe` (string) — Resolved universe slug
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/agents \
  -H "Authorization: Bearer ast_..." \
  -H "Content-Type: application/json" \
  -d '{"displayName":"Research Agent","agentType":"hermes","llm":"deepseek-v4-pro","capabilities":["exa.search"]}'
```

Example response (201 Created):

```json
{
  "agent": {
    "id": "agent_cmtb2f0a8m9k7x51q4ez6r0d",
    "name": "Research Agent",
    "displayName": "Research Agent",
    "agentType": "hermes",
    "llm": "deepseek-v4-pro",
    "version": 1,
    "capabilities": [
      "exa.search"
    ],
    "defaultSessionId": null,
    "universe": "acme"
  }
}
```

### GET /agents/{id}

Agent detail (scope: read)

Path parameters:

- `id` (string, required) — The agent id (agent_…)

Responses:

- `200` — OK
  - `agent` (object, required) — Everything in the list item, plus configuration state.
    - `id` (string) — The agent id (agent_…) — the only address
    - `name` (string)
    - `displayName` (string)
    - `agentType` (string)
    - `llm` (string)
    - `harnessVersion` (string | null)
    - `reasoningEffort` (string | null) — "none" · "off" · "minimal" · "low" · "medium" · "high" · "xhigh" · "max" · "ultra" · null
    - `version` (integer)
    - `archived` (boolean)
    - `sessionCount` (integer)
    - `createdAt` (string)
    - `description` (string | null)
    - `prompt` (string | null) — The user prompt layer
    - `promptVersion` (integer | null)
    - `capabilities` (enum<string>[]) — The built-in capabilities this agent is granted — the exact set its sessions may execute (enforced; empty = none). Ids are listed at /docs/capabilities. — "exa.search" · "exa.contents" · "tinyfish.fetch" · "tinyfish.browser" · "dataforseo.serp" · "gptimage.generate" · "rembg.remove-background" · "seedance.generate" · "mm.i2v" · "fish-audio.transcribe"
    - `mcpServers` (object[]) — Declared remote MCP server references, including tool filters; runtime authentication fields are never returned
      - `type` ("url") — default: "url"
      - `name` (string, required) — max length 255
      - `url` (string, required)
      - `tools` (object)
        - `include` (string[]) — default: []
        - `exclude` (string[]) — default: []
    - `metadata` (object) — Your key-merged client metadata
    - `defaultSessionId` (string | null) — The session conversation verbs target when addressed by agent id
    - `universe` (string) — Resolved universe slug
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/agents/agent_cmtb2f0a8m9k7x51q4ez6r0d \
  -H "Authorization: Bearer ast_..."
```

### PATCH /agents/{id}

Update displayName / capabilities / metadata (scope: write)

Omitted fields preserved; capabilities replaced whole (explicit [] revokes all; effective on the next capability call, no restart); metadata key-merged (null deletes a key); optional expectedVersion → 409 version_conflict on mismatch.

Path parameters:

- `id` (string, required) — The agent id (agent_…)

Body (application/json):

- `displayName` (string) — max length 60
- `capabilities` (enum<string>[]) — "exa.search" · "exa.contents" · "tinyfish.fetch" · "tinyfish.browser" · "dataforseo.serp" · "gptimage.generate" · "rembg.remove-background" · "seedance.generate" · "mm.i2v" · "fish-audio.transcribe"
- `metadata` (object)
- `harnessVersion` (string)
- `reasoningEffort` (object) — Harness-native reasoning effort; null clears back to the harness default. Applies from the next provision/restart. Allowed values depend on agentType — hermes: none/minimal/low/medium/high/xhigh/max/ultra; claude_code: low/medium/high/xhigh/max; codex: none/minimal/low/medium/high/xhigh/max; openclaw: not supported; pi: off/minimal/low/medium/high/xhigh/max; dsh: not supported; kimi_code: not supported; opencode: none/minimal/low/medium/high/xhigh/max
  - option 1 (object)
  - option 2 (object)
- `skills` (object[]) — Replaces the whole skills array; omitted preserves. Applies from the next provision.
  - type: "skill" (object)
    - `type` ("skill", required)
    - `skillId` (string, required)
    - `version` (string) — default: "latest"
  - type: "github" (object)
    - `type` ("github", required)
    - `url` (string, required)
    - `ref` (string) — Branch or commit SHA; default branch when omitted.
    - `tokenSecretRef` (string) — Agent secret key holding a clone token for private repos.
- `mcpServers` (object[]) — Replaces the whole MCP server list; omitted preserves. Applies from the next provision.
  - `type` ("url") — default: "url"
  - `name` (string, required) — max length 255
  - `url` (string, required)
  - `tools` (object)
    - `include` (string[]) — default: []
    - `exclude` (string[]) — default: []
- `expectedVersion` (integer)

Responses:

- `200` — OK
  - `agent` (object, required) — Everything in the list item, plus configuration state.
    - `id` (string) — The agent id (agent_…) — the only address
    - `name` (string)
    - `displayName` (string)
    - `agentType` (string)
    - `llm` (string)
    - `harnessVersion` (string | null)
    - `reasoningEffort` (string | null) — "none" · "off" · "minimal" · "low" · "medium" · "high" · "xhigh" · "max" · "ultra" · null
    - `version` (integer)
    - `archived` (boolean)
    - `sessionCount` (integer)
    - `createdAt` (string)
    - `description` (string | null)
    - `prompt` (string | null) — The user prompt layer
    - `promptVersion` (integer | null)
    - `capabilities` (enum<string>[]) — The built-in capabilities this agent is granted — the exact set its sessions may execute (enforced; empty = none). Ids are listed at /docs/capabilities. — "exa.search" · "exa.contents" · "tinyfish.fetch" · "tinyfish.browser" · "dataforseo.serp" · "gptimage.generate" · "rembg.remove-background" · "seedance.generate" · "mm.i2v" · "fish-audio.transcribe"
    - `mcpServers` (object[]) — Declared remote MCP server references, including tool filters; runtime authentication fields are never returned
      - `type` ("url") — default: "url"
      - `name` (string, required) — max length 255
      - `url` (string, required)
      - `tools` (object)
        - `include` (string[]) — default: []
        - `exclude` (string[]) — default: []
    - `metadata` (object) — Your key-merged client metadata
    - `defaultSessionId` (string | null) — The session conversation verbs target when addressed by agent id
    - `universe` (string) — Resolved universe slug
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X PATCH https://agentsky.dev/agents/agent_cmtb2f0a8m9k7x51q4ez6r0d \
  -H "Authorization: Bearer ast_..." \
  -H "Content-Type: application/json" \
  -d '{"displayName":"Research Agent","expectedVersion":1}'
```

### DELETE /agents/{id}

Delete the agent — only with zero sessions (409 agent_has_sessions) (scope: admin)

Path parameters:

- `id` (string, required) — The agent id (agent_…)

Responses:

- `200` — OK
  - `ok` (true, required)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X DELETE https://agentsky.dev/agents/agent_cmtb2f0a8m9k7x51q4ez6r0d \
  -H "Authorization: Bearer ast_..."
```

### POST /agents/{id}/archive

Archive the agent — read-only, sessions keep running, new sessions rejected (scope: admin)

Path parameters:

- `id` (string, required) — The agent id (agent_…)

Responses:

- `200` — OK
  - `agent` (object, required) — Everything in the list item, plus configuration state.
    - `id` (string) — The agent id (agent_…) — the only address
    - `name` (string)
    - `displayName` (string)
    - `agentType` (string)
    - `llm` (string)
    - `harnessVersion` (string | null)
    - `reasoningEffort` (string | null) — "none" · "off" · "minimal" · "low" · "medium" · "high" · "xhigh" · "max" · "ultra" · null
    - `version` (integer)
    - `archived` (boolean)
    - `sessionCount` (integer)
    - `createdAt` (string)
    - `description` (string | null)
    - `prompt` (string | null) — The user prompt layer
    - `promptVersion` (integer | null)
    - `capabilities` (enum<string>[]) — The built-in capabilities this agent is granted — the exact set its sessions may execute (enforced; empty = none). Ids are listed at /docs/capabilities. — "exa.search" · "exa.contents" · "tinyfish.fetch" · "tinyfish.browser" · "dataforseo.serp" · "gptimage.generate" · "rembg.remove-background" · "seedance.generate" · "mm.i2v" · "fish-audio.transcribe"
    - `mcpServers` (object[]) — Declared remote MCP server references, including tool filters; runtime authentication fields are never returned
      - `type` ("url") — default: "url"
      - `name` (string, required) — max length 255
      - `url` (string, required)
      - `tools` (object)
        - `include` (string[]) — default: []
        - `exclude` (string[]) — default: []
    - `metadata` (object) — Your key-merged client metadata
    - `defaultSessionId` (string | null) — The session conversation verbs target when addressed by agent id
    - `universe` (string) — Resolved universe slug
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/agents/agent_cmtb2f0a8m9k7x51q4ez6r0d/archive \
  -H "Authorization: Bearer ast_..."
```

### PUT /agents/{id}/prompt

Save the user prompt layer; applies to sessions started after the save (scope: write)

Path parameters:

- `id` (string, required) — The agent id (agent_…)

Body (application/json):

- `prompt` (string, required) — max length 100000

Responses:

- `200` — OK
  - `version` (integer)
  - `unchanged` (boolean) — Content identical to the active version
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X PUT https://agentsky.dev/agents/agent_cmtb2f0a8m9k7x51q4ez6r0d/prompt \
  -H "Authorization: Bearer ast_..." \
  -H "Content-Type: application/json" \
  -d '{"prompt":"You are a meticulous research agent…"}'
```

### GET /agents/{id}/prompt/versions

Prompt version history (scope: read)

Path parameters:

- `id` (string, required) — The agent id (agent_…)

Responses:

- `200` — OK
  - `versions` (object[], required)
    - `version` (integer)
    - `note` (string | null)
    - `createdAt` (string)
    - `active` (boolean)
    - `content` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/agents/agent_cmtb2f0a8m9k7x51q4ez6r0d/prompt/versions \
  -H "Authorization: Bearer ast_..."
```

### GET /agents/{id}/secrets

Declared secret keys + set/unset — never values (scope: read)

Path parameters:

- `id` (string, required) — The agent id (agent_…)

Responses:

- `200` — OK
  - `secrets` (object[], required)
    - `key` (string)
    - `description` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/agents/agent_cmtb2f0a8m9k7x51q4ez6r0d/secrets \
  -H "Authorization: Bearer ast_..."
```

### PUT /agents/{id}/secrets/{key}

Set a declared secret's value (write-only) (scope: write)

Path parameters:

- `id` (string, required) — The agent id (agent_…)
- `key` (string, required)

Body (application/json):

- `value` (string, required) — max length 10000
- `description` (string) — max length 500

Responses:

- `200` — OK
  - `ok` (true, required)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X PUT https://agentsky.dev/agents/agent_cmtb2f0a8m9k7x51q4ez6r0d/secrets/NOTION_TOKEN \
  -H "Authorization: Bearer ast_..." \
  -H "Content-Type: application/json" \
  -d '{"value":"secret_…"}'
```

### DELETE /agents/{id}/secrets/{key}

Unset a secret (scope: write)

Path parameters:

- `id` (string, required) — The agent id (agent_…)
- `key` (string, required)

Responses:

- `200` — OK
  - `ok` (true, required)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X DELETE https://agentsky.dev/agents/agent_cmtb2f0a8m9k7x51q4ez6r0d/secrets/NOTION_TOKEN \
  -H "Authorization: Bearer ast_..."
```

## Skills

### GET /skills

List skills in the resolved universe (scope: read)

Query parameters:

- `cursor` (string)
- `limit` (integer)

Responses:

- `200` — OK
  - `skills` (object[], required)
    - `id` (string, required)
    - `displayName` (string, required)
    - `latestRevisionId` (string | null, required) — What `latest` references resolve to.
    - `createdAt` (string, required)
    - `updatedAt` (string, required)
  - `nextCursor` (string | null)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/skills?cursor=&limit= \
  -H "Authorization: Bearer ast_..."
```

### POST /skills

Create a skill (uploads revision 1) (scope: write)

A skill is a universe-scoped, immutably-revisioned file bundle agents load on demand (RFC-0073). Attach it to agents via the `skills` array on create/patch.

Responses:

- `201` — Created
  - `skill` (object, required)
    - `id` (string, required)
    - `displayName` (string, required)
    - `latestRevisionId` (string | null, required) — What `latest` references resolve to.
    - `createdAt` (string, required)
    - `updatedAt` (string, required)
  - `revision` (object, required)
    - `id` (string, required)
    - `skillId` (string, required)
    - `name` (string, required) — Frontmatter name — the skill's identity name.
    - `description` (string, required)
    - `files` (object[], required)
      - `path` (string, required)
      - `size` (integer, required)
      - `sha256` (string, required)
      - `executable` (boolean, required)
    - `createdAt` (string, required)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/skills \
  -H "Authorization: Bearer ast_..."
```

### GET /skills/{skillId}

Skill detail (scope: read)

Path parameters:

- `skillId` (string, required) — The skill id (skill_…)

Responses:

- `200` — OK
  - `skill` (object, required)
    - `id` (string, required)
    - `displayName` (string, required)
    - `latestRevisionId` (string | null, required) — What `latest` references resolve to.
    - `createdAt` (string, required)
    - `updatedAt` (string, required)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/skills/skillId \
  -H "Authorization: Bearer ast_..."
```

### DELETE /skills/{skillId}

Delete a skill and all its revisions (scope: write)

409 skill_in_use while any agent references the skill.

Path parameters:

- `skillId` (string, required) — The skill id (skill_…)

Responses:

- `200` — OK
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X DELETE https://agentsky.dev/skills/skillId \
  -H "Authorization: Bearer ast_..."
```

### GET /skills/{skillId}/revisions

List a skill's revisions, newest first (scope: read)

Path parameters:

- `skillId` (string, required) — The skill id (skill_…)

Responses:

- `200` — OK
  - `revisions` (object[], required)
    - `id` (string, required)
    - `skillId` (string, required)
    - `name` (string, required) — Frontmatter name — the skill's identity name.
    - `description` (string, required)
    - `files` (object[], required)
      - `path` (string, required)
      - `size` (integer, required)
      - `sha256` (string, required)
      - `executable` (boolean, required)
    - `createdAt` (string, required)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/skills/skillId/revisions \
  -H "Authorization: Bearer ast_..."
```

### POST /skills/{skillId}/revisions

Create a revision — a complete snapshot (scope: write)

Omitted files are not carried over. The new SKILL.md name must match the skill's name (400 name_mismatch).

Responses:

- `201` — Created
  - `revision` (object, required)
    - `id` (string, required)
    - `skillId` (string, required)
    - `name` (string, required) — Frontmatter name — the skill's identity name.
    - `description` (string, required)
    - `files` (object[], required)
      - `path` (string, required)
      - `size` (integer, required)
      - `sha256` (string, required)
      - `executable` (boolean, required)
    - `createdAt` (string, required)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/skills/skillId/revisions \
  -H "Authorization: Bearer ast_..."
```

### GET /skills/{skillId}/revisions/{revisionId}

Skill revision detail (metadata + file manifest) (scope: read)

Path parameters:

- `skillId` (string, required) — The skill id (skill_…)
- `revisionId` (string, required) — The skill revision id (skr_…)

Responses:

- `200` — OK
  - `revision` (object, required)
    - `id` (string, required)
    - `skillId` (string, required)
    - `name` (string, required) — Frontmatter name — the skill's identity name.
    - `description` (string, required)
    - `files` (object[], required)
      - `path` (string, required)
      - `size` (integer, required)
      - `sha256` (string, required)
      - `executable` (boolean, required)
    - `createdAt` (string, required)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/skills/skillId/revisions/revisionId \
  -H "Authorization: Bearer ast_..."
```

### DELETE /skills/{skillId}/revisions/{revisionId}

Delete one revision (scope: write)

400 last_revision for a skill's only revision; 409 revision_in_use while pinned by an agent; deleting the latest repoints latestRevisionId.

Path parameters:

- `skillId` (string, required) — The skill id (skill_…)
- `revisionId` (string, required) — The skill revision id (skr_…)

Responses:

- `200` — OK
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X DELETE https://agentsky.dev/skills/skillId/revisions/revisionId \
  -H "Authorization: Bearer ast_..."
```

## Files

### POST /files

Upload a file — universe-scoped, write-once (scope: write)

multipart/form-data with a `file` part and an optional `expires_in_seconds` (3600–7776000). 32 MiB per file (413 file_too_large). Reference the returned id from a message as `{"type":"image","source":{"type":"file","file_id":"file_…"}}` or a `document` block.

Responses:

- `200` — OK
  - `type` (enum<string>, required) — "file"
  - `id` (string, required) — file_…
  - `created_at` (string, required)
  - `filename` (string, required)
  - `mime_type` (string, required)
  - `size_bytes` (integer, required)
  - `downloadable` (boolean, required) — false for uploads (write-once); true for the per-session copies minted when a file rides a message
  - `expires_at` (string | null, required)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/files \
  -H "Authorization: Bearer ast_..."
```

Example response (200 OK):

```json
{
  "type": "file",
  "id": "file_cmtb9x4p",
  "created_at": "2026-09-10T18:00:00.000Z",
  "filename": "design.pdf",
  "mime_type": "application/pdf",
  "size_bytes": 102400,
  "downloadable": false,
  "expires_at": null
}
```

### GET /files

List files — your universe's uploads, or one session's files (scope: read)

Without `scope_id`, the universe's uploads. `scope_id=sess_…` lists that session's files: the copies minted from message content, channel attachments, and agent-stored files. `ids[]` (≤100) returns exactly those files in one page and cannot be combined with page/limit.

Query parameters:

- `limit` (integer)
- `page` (string) — A next_page cursor
- `ids[]` (array)
- `scope_id` (string) — sess_…

Responses:

- `200` — OK
  - `data` (object[], required) — Newest first
    - `type` (enum<string>, required) — "file"
    - `id` (string, required) — file_…
    - `created_at` (string, required)
    - `filename` (string, required)
    - `mime_type` (string, required)
    - `size_bytes` (integer, required)
    - `downloadable` (boolean, required) — false for uploads (write-once); true for the per-session copies minted when a file rides a message
    - `expires_at` (string | null, required)
  - `next_page` (string | null, required) — Pass back as ?page= for the next page
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/files?limit=&page=&ids[]=&scope_id= \
  -H "Authorization: Bearer ast_..."
```

Example response (200 OK):

```json
{
  "data": [
    {
      "type": "file",
      "id": "file_cmtb9x4p",
      "created_at": "2026-09-10T18:00:00.000Z",
      "filename": "design.pdf",
      "mime_type": "application/pdf",
      "size_bytes": 102400,
      "downloadable": false,
      "expires_at": null
    }
  ],
  "next_page": null
}
```

### GET /files/{file_id}

File metadata (scope: read)

Path parameters:

- `file_id` (string, required) — The file id (file_…)

Responses:

- `200` — OK
  - `type` (enum<string>, required) — "file"
  - `id` (string, required) — file_…
  - `created_at` (string, required)
  - `filename` (string, required)
  - `mime_type` (string, required)
  - `size_bytes` (integer, required)
  - `downloadable` (boolean, required) — false for uploads (write-once); true for the per-session copies minted when a file rides a message
  - `expires_at` (string | null, required)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/files/file_cmtb9x4p \
  -H "Authorization: Bearer ast_..."
```

### DELETE /files/{file_id}

Delete a file (scope: write)

Soft delete: the file stops resolving everywhere; sessions that already received a copy keep it.

Path parameters:

- `file_id` (string, required) — The file id (file_…)

Responses:

- `200` — OK
  - `type` (enum<string>, required) — "file_deleted"
  - `id` (string, required)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X DELETE https://agentsky.dev/files/file_cmtb9x4p \
  -H "Authorization: Bearer ast_..."
```

Example response (200 OK):

```json
{
  "type": "file_deleted",
  "id": "file_cmtb9x4p"
}
```

### GET /files/{file_id}/content

Download a file's bytes (scope: read)

Only `downloadable` files (every session-scoped file: message copies, channel attachments, agent outputs); an upload answers 400 not_downloadable. 404 once the file has expired.

Path parameters:

- `file_id` (string, required) — The file id (file_…)

Responses:

- `200` — The bytes, with content-type and content-disposition
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/files/file_cmtb9x4p/content \
  -H "Authorization: Bearer ast_..."
```

## Sessions

### GET /sessions

List sessions; ?agent= filters to one spec (scope: read)

Query parameters:

- `agent` (string) — The agent id (agent_…)

Responses:

- `200` — OK
  - `sessions` (object[], required)
    - `id` (string) — sess_… identifier
    - `agentId` (string | null) — The agent id (agent_…)
    - `title` (string | null)
    - `status` (enum<string>) — provisioning = the sandbox is booting or installing the environment's packages (a turn sent meanwhile waits for it); running = a turn is in flight; terminated = archived — "provisioning" · "idle" · "running" · "terminated"
    - `agentType` (string)
    - `llm` (string) — The model this session runs on (PATCH to change; the agent's model is unchanged)
    - `reasoningEffort` (string | null) — The effective reasoning effort: this session's own setting, else the agent's, else null (the harness default)
    - `modelBilling` (enum<string>) — Who pays for model calls: platform wallet or the user's connected subscription — "platform" · "account"
    - `default` (boolean) — Is this the agent's default session
    - `metadata` (object)
    - `budget` (object | null) — Hard spend cap, or null when the session has none. A reached budget pauses the session (402 budget_reached on new work) until the cap is raised above consumed_cost or removed — removal is one-way.
    - `resources` (object[]) — The files and GitHub repositories mounted into the working directory, oldest first
      - option 1 (object)
        - `type` (enum<string>, required) — "file"
        - `id` (string, required) — sesrsc_…
        - `file_id` (string, required) — The per-session copy (file_…), read-only at mount_path
        - `mount_path` (string, required) — Absolute, under /workspace
        - `created_at` (string, required)
        - `updated_at` (string, required)
      - option 2 (object)
        - `type` (enum<string>, required) — "github_repository"
        - `id` (string, required) — sesrsc_…
        - `url` (string, required) — https://github.com/{owner}/{repo}
        - `mount_path` (string, required) — Absolute, under /workspace; the clone's root
        - `checkout` (object) — {type:"branch", name} or {type:"commit", sha}; absent = the default branch
          - `type` (enum<string>) — "branch" · "commit"
          - `name` (string)
          - `sha` (string)
        - `created_at` (string, required)
        - `updated_at` (string, required)
    - `createdAt` (string) — ISO 8601
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/sessions?agent= \
  -H "Authorization: Bearer ast_..."
```

### POST /sessions

Create a session — provisions the pod up front so the first turn isn't a cold boot (scope: write)

Optional llm and reasoningEffort override the agent's defaults for this session before billing selection or provisioning. A model must be current for the agent's harness; omitted or null reasoningEffort inherits the agent's setting. A non-empty initial_events list (user.message only, all-or-nothing) starts the first turn in the same call. Machine shape (vcpus/memoryMb) is per-session.

Body (application/json):

- `agent` (string, required)
- `llm` (string) — Model for this session (a current model for the agent's harness). Omitted = the agent's model
- `reasoningEffort` (object) — Reasoning effort for this session; omitted or null inherits the agent's setting. Allowed values depend on agentType - hermes: none/minimal/low/medium/high/xhigh/max/ultra; claude_code: low/medium/high/xhigh/max; codex: none/minimal/low/medium/high/xhigh/max; openclaw: not supported; pi: off/minimal/low/medium/high/xhigh/max; dsh: not supported; kimi_code: not supported; opencode: none/minimal/low/medium/high/xhigh/max
  - option 1 (object)
  - option 2 (object)
- `title` (string) — max length 120
- `metadata` (object)
- `instructions` (object[])
  - `name` (string, required) — pattern .*\.md$
  - `content` (string) — default: ""
- `initial_events` (object[])
  - `type` ("user.message", required)
  - `content` (object[], required)
    - type: "text" (object)
      - `type` ("text", required)
      - `text` (string, required)
    - type: "image" (object)
      - `type` ("image", required)
      - `source` (object, required)
        - type: "base64" (object)
          - `type` ("base64", required)
          - `media_type` (string, required)
          - `data` (string, required)
        - type: "url" (object)
          - `type` ("url", required)
          - `url` (string, required)
        - type: "file" (object)
          - `type` ("file", required)
          - `file_id` (string, required)
    - type: "document" (object)
      - `type` ("document", required)
      - `source` (object, required)
        - type: "base64" (object)
          - `type` ("base64", required)
          - `media_type` (string, required)
          - `data` (string, required)
        - type: "text" (object)
          - `type` ("text", required)
          - `media_type` ("text/plain") — default: "text/plain"
          - `data` (string, required)
        - type: "url" (object)
          - `type` ("url", required)
          - `url` (string, required)
        - type: "file" (object)
          - `type` ("file", required)
          - `file_id` (string, required)
      - `title` (string | null)
      - `context` (string | null)
- `environment_id` (string)
- `vault_ids` (string[])
- `vcpus` (integer)
- `memoryMb` (integer)
- `modelBilling` (enum<string>) — Who pays for model calls: platform wallet or the user's connected subscription. Omitted = auto-apply (a connected subscription with "use for new agents" powers eligible compositions) — "platform" · "account"
- `budget` (object) — Hard spend cap for this session. Attachable only at create; changeable or removable later via PATCH (removal is one-way)
  - `type` ("limit", required)
  - `max_list_cost` (object, required)
    - `amount` (string, required) — Whole US cents as an integer decimal string, e.g. "125" for $1.25 — max length 15
    - `currency` ("USD", required)
- `resources` (object[]) — Files and GitHub repositories mounted into the agent's working directory before its first turn (max 100). `{type:"file", file_id, mount_path?}` copies an upload read-only to `/workspace<mount_path>` (default `/workspace/<file_id>`); `{type:"github_repository", url, authorization_token?, checkout?: {type:"branch", name} | {type:"commit", sha}, mount_path?}` clones `https://github.com/{owner}/{repo}` to `/workspace<mount_path>` (default `/workspace/<repo>`); the token never enters the sandbox
  - type: "file" (object)
    - `type` ("file", required)
    - `file_id` (string, required) — max length 128
    - `mount_path` (object)
      - option 1 (object)
      - option 2 (object)
  - type: "github_repository" (object)
    - `type` ("github_repository", required)
    - `url` (string, required) — max length 2048
    - `authorization_token` (string) — max length 1024
    - `checkout` (object)
      - option 1 (object)
        - type: "branch" (object)
          - `type` ("branch", required)
          - `name` (string, required) — max length 255
        - type: "commit" (object)
          - `type` ("commit", required)
          - `sha` (string, required) — max length 64
      - option 2 (object)
    - `mount_path` (object)
      - option 1 (object)
      - option 2 (object)

Responses:

- `201` — Created
  - `session` (object, required)
    - `id` (string) — sess_… identifier
    - `agentId` (string | null) — The agent id (agent_…)
    - `title` (string | null)
    - `status` (enum<string>) — provisioning = the sandbox is booting or installing the environment's packages (a turn sent meanwhile waits for it); running = a turn is in flight; terminated = archived — "provisioning" · "idle" · "running" · "terminated"
    - `agentType` (string)
    - `llm` (string) — The model this session runs on (PATCH to change; the agent's model is unchanged)
    - `reasoningEffort` (string | null) — The effective reasoning effort: this session's own setting, else the agent's, else null (the harness default)
    - `modelBilling` (enum<string>) — Who pays for model calls: platform wallet or the user's connected subscription — "platform" · "account"
    - `default` (boolean) — Is this the agent's default session
    - `metadata` (object)
    - `budget` (object | null) — Hard spend cap, or null when the session has none. A reached budget pauses the session (402 budget_reached on new work) until the cap is raised above consumed_cost or removed — removal is one-way.
    - `resources` (object[]) — The files and GitHub repositories mounted into the working directory, oldest first
      - option 1 (object)
        - `type` (enum<string>, required) — "file"
        - `id` (string, required) — sesrsc_…
        - `file_id` (string, required) — The per-session copy (file_…), read-only at mount_path
        - `mount_path` (string, required) — Absolute, under /workspace
        - `created_at` (string, required)
        - `updated_at` (string, required)
      - option 2 (object)
        - `type` (enum<string>, required) — "github_repository"
        - `id` (string, required) — sesrsc_…
        - `url` (string, required) — https://github.com/{owner}/{repo}
        - `mount_path` (string, required) — Absolute, under /workspace; the clone's root
        - `checkout` (object) — {type:"branch", name} or {type:"commit", sha}; absent = the default branch
          - `type` (enum<string>) — "branch" · "commit"
          - `name` (string)
          - `sha` (string)
        - `created_at` (string, required)
        - `updated_at` (string, required)
    - `createdAt` (string) — ISO 8601
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/sessions \
  -H "Authorization: Bearer ast_..." \
  -H "Content-Type: application/json" \
  -d '{"agent":"agent_cmtb2f0a8m9k7x51q4ez6r0d","llm":"deepseek-flash","reasoningEffort":"high","title":"Signups digest","resources":[{"type":"file","file_id":"file_cmtb9x4p","mount_path":"/data/signups.csv"},{"type":"github_repository","url":"https://github.com/acme/growth","checkout":{"type":"branch","name":"main"}}],"initial_events":[{"type":"user.message","content":[{"type":"text","text":"Summarize today's signups"}]}]}'
```

Example response (201 Created):

```json
{
  "session": {
    "id": "sess-8c41f0…",
    "agent": "research-agent",
    "status": "idle",
    "default": true
  }
}
```

### GET /sessions/{id}

Session detail — status is provisioning (sandbox booting or installing packages) | idle | running | terminated (scope: read)

Path parameters:

- `id` (string, required) — The session id

Responses:

- `200` — OK
  - `session` (object, required)
    - `id` (string) — sess_… identifier
    - `agentId` (string | null) — The agent id (agent_…)
    - `title` (string | null)
    - `status` (enum<string>) — provisioning = the sandbox is booting or installing the environment's packages (a turn sent meanwhile waits for it); running = a turn is in flight; terminated = archived — "provisioning" · "idle" · "running" · "terminated"
    - `agentType` (string)
    - `llm` (string) — The model this session runs on (PATCH to change; the agent's model is unchanged)
    - `reasoningEffort` (string | null) — The effective reasoning effort: this session's own setting, else the agent's, else null (the harness default)
    - `modelBilling` (enum<string>) — Who pays for model calls: platform wallet or the user's connected subscription — "platform" · "account"
    - `default` (boolean) — Is this the agent's default session
    - `metadata` (object)
    - `budget` (object | null) — Hard spend cap, or null when the session has none. A reached budget pauses the session (402 budget_reached on new work) until the cap is raised above consumed_cost or removed — removal is one-way.
    - `resources` (object[]) — The files and GitHub repositories mounted into the working directory, oldest first
      - option 1 (object)
        - `type` (enum<string>, required) — "file"
        - `id` (string, required) — sesrsc_…
        - `file_id` (string, required) — The per-session copy (file_…), read-only at mount_path
        - `mount_path` (string, required) — Absolute, under /workspace
        - `created_at` (string, required)
        - `updated_at` (string, required)
      - option 2 (object)
        - `type` (enum<string>, required) — "github_repository"
        - `id` (string, required) — sesrsc_…
        - `url` (string, required) — https://github.com/{owner}/{repo}
        - `mount_path` (string, required) — Absolute, under /workspace; the clone's root
        - `checkout` (object) — {type:"branch", name} or {type:"commit", sha}; absent = the default branch
          - `type` (enum<string>) — "branch" · "commit"
          - `name` (string)
          - `sha` (string)
        - `created_at` (string, required)
        - `updated_at` (string, required)
    - `createdAt` (string) — ISO 8601
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/sessions/sess_cmtb7q2j \
  -H "Authorization: Bearer ast_..."
```

### PATCH /sessions/{id}

Update title / metadata / model / reasoning effort / billing / budget (scope: write)

llm and reasoningEffort are session settings: they change this session only, never the agent or its other sessions, and must fit the session's harness. An idle session restarts its engine in place; a running session finishes its turn and applies the change at the next turn (the response is 200 either way). A parked session applies it at its next wake.

Path parameters:

- `id` (string, required) — The session id

Body (application/json):

- `title` (object)
  - option 1 (object)
  - option 2 (object)
- `metadata` (object)
- `llm` (string) — Switch this session's model (a current model for its harness; the agent's model is unchanged). Applies from the next turn
- `reasoningEffort` (object) — Session-level reasoning effort; null clears back to the agent's setting. Applies from the next turn. Allowed values depend on agentType — hermes: none/minimal/low/medium/high/xhigh/max/ultra; claude_code: low/medium/high/xhigh/max; codex: none/minimal/low/medium/high/xhigh/max; openclaw: not supported; pi: off/minimal/low/medium/high/xhigh/max; dsh: not supported; kimi_code: not supported; opencode: none/minimal/low/medium/high/xhigh/max
  - option 1 (object)
  - option 2 (object)
- `modelBilling` (enum<string>) — Who pays for model calls; switching restarts the agent's engine in place — "platform" · "account"
- `budget` (object) — Change the spend cap (must be strictly above consumed cost) or null to remove it — removal is one-way
  - type: "limit" (object)
    - `type` ("limit", required)
    - `max_list_cost` (object, required)
      - `amount` (string, required) — Whole US cents as an integer decimal string, e.g. "125" for $1.25 — max length 15
      - `currency` ("USD", required)
  - option 2 (object)

Responses:

- `200` — OK
  - `session` (object, required)
    - `id` (string) — sess_… identifier
    - `agentId` (string | null) — The agent id (agent_…)
    - `title` (string | null)
    - `status` (enum<string>) — provisioning = the sandbox is booting or installing the environment's packages (a turn sent meanwhile waits for it); running = a turn is in flight; terminated = archived — "provisioning" · "idle" · "running" · "terminated"
    - `agentType` (string)
    - `llm` (string) — The model this session runs on (PATCH to change; the agent's model is unchanged)
    - `reasoningEffort` (string | null) — The effective reasoning effort: this session's own setting, else the agent's, else null (the harness default)
    - `modelBilling` (enum<string>) — Who pays for model calls: platform wallet or the user's connected subscription — "platform" · "account"
    - `default` (boolean) — Is this the agent's default session
    - `metadata` (object)
    - `budget` (object | null) — Hard spend cap, or null when the session has none. A reached budget pauses the session (402 budget_reached on new work) until the cap is raised above consumed_cost or removed — removal is one-way.
    - `resources` (object[]) — The files and GitHub repositories mounted into the working directory, oldest first
      - option 1 (object)
        - `type` (enum<string>, required) — "file"
        - `id` (string, required) — sesrsc_…
        - `file_id` (string, required) — The per-session copy (file_…), read-only at mount_path
        - `mount_path` (string, required) — Absolute, under /workspace
        - `created_at` (string, required)
        - `updated_at` (string, required)
      - option 2 (object)
        - `type` (enum<string>, required) — "github_repository"
        - `id` (string, required) — sesrsc_…
        - `url` (string, required) — https://github.com/{owner}/{repo}
        - `mount_path` (string, required) — Absolute, under /workspace; the clone's root
        - `checkout` (object) — {type:"branch", name} or {type:"commit", sha}; absent = the default branch
          - `type` (enum<string>) — "branch" · "commit"
          - `name` (string)
          - `sha` (string)
        - `created_at` (string, required)
        - `updated_at` (string, required)
    - `createdAt` (string) — ISO 8601
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X PATCH https://agentsky.dev/sessions/sess_cmtb7q2j \
  -H "Authorization: Bearer ast_..." \
  -H "Content-Type: application/json" \
  -d '{"title":"Signups digest","metadata":{"team":"growth"},"llm":"claude-sonnet-5","reasoningEffort":"high"}'
```

### DELETE /sessions/{id}

Permanently delete the session, its events, and its pod (scope: write)

Hard delete — the record, event history, pod, and stored context snapshots are permanently removed; GET answers 404 after. A running session is refused with 400 session_running: interrupt and wait for idle first. Works on idle and archived/terminated sessions.

Path parameters:

- `id` (string, required) — The session id

Responses:

- `200` — OK
  - `ok` (true, required)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X DELETE https://agentsky.dev/sessions/sess_cmtb7q2j \
  -H "Authorization: Bearer ast_..."
```

### POST /sessions/{id}/archive

Archive the session — irreversible; blocks new events, history stays readable (scope: write)

Sets status to terminated, tears down the pod and stored context, and refuses further messages (400 session_archived); the record and its events remain readable. A running session is refused with 400 session_running: interrupt and wait for idle first. Idempotent.

Path parameters:

- `id` (string, required) — The session id

Responses:

- `200` — OK
  - `session` (object, required)
    - `id` (string) — sess_… identifier
    - `agentId` (string | null) — The agent id (agent_…)
    - `title` (string | null)
    - `status` (enum<string>) — provisioning = the sandbox is booting or installing the environment's packages (a turn sent meanwhile waits for it); running = a turn is in flight; terminated = archived — "provisioning" · "idle" · "running" · "terminated"
    - `agentType` (string)
    - `llm` (string) — The model this session runs on (PATCH to change; the agent's model is unchanged)
    - `reasoningEffort` (string | null) — The effective reasoning effort: this session's own setting, else the agent's, else null (the harness default)
    - `modelBilling` (enum<string>) — Who pays for model calls: platform wallet or the user's connected subscription — "platform" · "account"
    - `default` (boolean) — Is this the agent's default session
    - `metadata` (object)
    - `budget` (object | null) — Hard spend cap, or null when the session has none. A reached budget pauses the session (402 budget_reached on new work) until the cap is raised above consumed_cost or removed — removal is one-way.
    - `resources` (object[]) — The files and GitHub repositories mounted into the working directory, oldest first
      - option 1 (object)
        - `type` (enum<string>, required) — "file"
        - `id` (string, required) — sesrsc_…
        - `file_id` (string, required) — The per-session copy (file_…), read-only at mount_path
        - `mount_path` (string, required) — Absolute, under /workspace
        - `created_at` (string, required)
        - `updated_at` (string, required)
      - option 2 (object)
        - `type` (enum<string>, required) — "github_repository"
        - `id` (string, required) — sesrsc_…
        - `url` (string, required) — https://github.com/{owner}/{repo}
        - `mount_path` (string, required) — Absolute, under /workspace; the clone's root
        - `checkout` (object) — {type:"branch", name} or {type:"commit", sha}; absent = the default branch
          - `type` (enum<string>) — "branch" · "commit"
          - `name` (string)
          - `sha` (string)
        - `created_at` (string, required)
        - `updated_at` (string, required)
    - `createdAt` (string) — ISO 8601
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/sessions/sess_cmtb7q2j/archive \
  -H "Authorization: Bearer ast_..."
```

### GET /sessions/{id}/resources

List the session's resources, oldest first (scope: read)

`limit` (1–1000) pages the list with an opaque `page` cursor; omitted lists every resource in one answer.

Path parameters:

- `id` (string, required) — The session id (sess_…)

Query parameters:

- `limit` (integer)
- `page` (string) — A next_page cursor

Responses:

- `200` — OK
  - `data` (object[], required) — Oldest first
    - option 1 (object)
      - `type` (enum<string>, required) — "file"
      - `id` (string, required) — sesrsc_…
      - `file_id` (string, required) — The per-session copy (file_…), read-only at mount_path
      - `mount_path` (string, required) — Absolute, under /workspace
      - `created_at` (string, required)
      - `updated_at` (string, required)
    - option 2 (object)
      - `type` (enum<string>, required) — "github_repository"
      - `id` (string, required) — sesrsc_…
      - `url` (string, required) — https://github.com/{owner}/{repo}
      - `mount_path` (string, required) — Absolute, under /workspace; the clone's root
      - `checkout` (object) — {type:"branch", name} or {type:"commit", sha}; absent = the default branch
        - `type` (enum<string>) — "branch" · "commit"
        - `name` (string)
        - `sha` (string)
      - `created_at` (string, required)
      - `updated_at` (string, required)
  - `next_page` (string | null, required) — Pass back as ?page= for the next page; null when `limit` was omitted or the list ended
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/sessions/sess_cmtb7q2j/resources?limit=&page= \
  -H "Authorization: Bearer ast_..."
```

Example response (200 OK):

```json
{
  "data": [
    {
      "type": "file",
      "id": "sesrsc_cmtc1a2b",
      "file_id": "file_cmtc1a2c",
      "mount_path": "/workspace/data/signups.csv",
      "created_at": "2026-09-11T18:00:00.000Z",
      "updated_at": "2026-09-11T18:00:00.000Z"
    },
    {
      "type": "github_repository",
      "id": "sesrsc_cmtc1a2d",
      "url": "https://github.com/acme/growth",
      "mount_path": "/workspace/growth",
      "checkout": {
        "type": "branch",
        "name": "main"
      },
      "created_at": "2026-09-11T18:00:00.000Z",
      "updated_at": "2026-09-11T18:00:00.000Z"
    }
  ],
  "next_page": null
}
```

### POST /sessions/{id}/resources

Add a file to a running session (scope: write)

Files only — a `github_repository` body answers 400 (repositories are attached at session create and stay for the session's lifetime). The file is copied read-only to `/workspace<mount_path>` before the session's next turn; `mount_path` follows the create rules (rooted under /workspace, no escape, no overlap with another resource). 400 too_many_resources past 500 per session; 400 session_archived on an archived session.

Path parameters:

- `id` (string, required) — The session id (sess_…)

Body (application/json):

- type: "file" (object)
  - `type` ("file", required)
  - `file_id` (string, required) — max length 128
  - `mount_path` (object)
    - option 1 (object)
    - option 2 (object)
- type: "github_repository" (object)
  - `type` ("github_repository", required)
  - `url` (string, required) — max length 2048
  - `authorization_token` (string) — max length 1024
  - `checkout` (object)
    - option 1 (object)
      - type: "branch" (object)
        - `type` ("branch", required)
        - `name` (string, required) — max length 255
      - type: "commit" (object)
        - `type` ("commit", required)
        - `sha` (string, required) — max length 64
    - option 2 (object)
  - `mount_path` (object)
    - option 1 (object)
    - option 2 (object)

Responses:

- `200` — OK
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/sessions/sess_cmtb7q2j/resources \
  -H "Authorization: Bearer ast_..." \
  -H "Content-Type: application/json" \
  -d '{"type":"file","file_id":"file_cmtb9x4p","mount_path":"/data/signups.csv"}'
```

Example response (200 OK):

```json
{
  "type": "file",
  "id": "sesrsc_cmtc1a2b",
  "file_id": "file_cmtc1a2c",
  "mount_path": "/workspace/data/signups.csv",
  "created_at": "2026-09-11T18:00:00.000Z",
  "updated_at": "2026-09-11T18:00:00.000Z"
}
```

### GET /sessions/{id}/resources/{rid}

One resource (scope: read)

Path parameters:

- `id` (string, required) — The session id (sess_…)
- `rid` (string, required) — The resource id (sesrsc_…)

Responses:

- `200` — OK
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/sessions/sess_cmtb7q2j/resources/sesrsc_cmtc1a2d \
  -H "Authorization: Bearer ast_..."
```

### POST /sessions/{id}/resources/{rid}

Rotate a repository's token (scope: write)

Only `github_repository` resources accept an update, and the token is the only field: it is re-encrypted and the session's egress policy is recompiled and pushed to its live pod — nothing changes inside the sandbox. A file answers 400. A token that conflicts with another secret's Authorization header on github.com answers 400 egress_header_conflict.

Path parameters:

- `id` (string, required) — The session id (sess_…)
- `rid` (string, required) — The resource id (sesrsc_…)

Body (application/json):

- `authorization_token` (string, required) — max length 1024

Responses:

- `200` — OK
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/sessions/sess_cmtb7q2j/resources/sesrsc_cmtc1a2d \
  -H "Authorization: Bearer ast_..." \
  -H "Content-Type: application/json" \
  -d '{"authorization_token":"github_pat_…"}'
```

### DELETE /sessions/{id}/resources/{rid}

Remove a file from the session (scope: write)

Files only — the resource and its per-session copy are deleted and the pod unlinks the file before the session's next turn. A `github_repository` answers 400: repositories cannot be removed mid-session.

Path parameters:

- `id` (string, required) — The session id (sess_…)
- `rid` (string, required) — The resource id (sesrsc_…)

Responses:

- `200` — OK
  - `type` (enum<string>, required) — "session_resource_deleted"
  - `id` (string, required)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X DELETE https://agentsky.dev/sessions/sess_cmtb7q2j/resources/sesrsc_cmtc1a2d \
  -H "Authorization: Bearer ast_..."
```

Example response (200 OK):

```json
{
  "type": "session_resource_deleted",
  "id": "sesrsc_cmtc1a2b"
}
```

## Conversation

### POST /sessions/{id}/messages

Deprecated — use POST /sessions/{id}/events (scope: write)

The legacy input shape ({parts}); accepted for one release and translated to a single user.message, then 410. Optional Idempotency-Key header dedupes retries.

Path parameters:

- `id` (string, required) — The session id

Body (application/json):

- `parts` (object[], required)
  - type: "text" (object)
    - `index` (integer, required)
    - `type` ("text", required)
    - `text` (string) — default: ""
    - `stream_id` (object)
      - option 1 (object)
      - option 2 (object)
    - `stream_index` (object)
      - option 1 (object)
      - option 2 (object)
  - type: "reasoning" (object)
    - `index` (integer, required)
    - `type` ("reasoning", required)
    - `text` (string) — default: ""
    - `redacted` (boolean) — default: false
    - `stream_id` (object)
      - option 1 (object)
      - option 2 (object)
    - `stream_index` (object)
      - option 1 (object)
      - option 2 (object)
  - type: "tool_call" (object)
    - `index` (integer, required)
    - `type` ("tool_call", required)
    - `call_id` (string, required)
    - `tool_name` (string, required)
    - `args` (object) — default: {}
    - `args_partial` (string | null)
  - type: "tool_result" (object)
    - `index` (integer, required)
    - `type` ("tool_result", required)
    - `call_id` (string, required)
    - `tool_name` (string, required)
    - `status` (enum<string>) — "ok" · "error" — default: "ok"
    - `result` (object) — default: {}
  - type: "file" (object)
    - `index` (integer, required)
    - `type` ("file", required)
    - `name` (string, required)
    - `media_type` (string, required)
    - `uri` (string | null)
    - `data` (string | null)
    - `size_bytes` (object)
      - option 1 (object)
      - option 2 (object)
  - type: "image" (object)
    - `index` (integer, required)
    - `type` ("image", required)
    - `media_type` (string, required)
    - `uri` (string | null)
    - `data` (string | null)
    - `alt` (string | null)
    - `width` (object)
      - option 1 (object)
      - option 2 (object)
    - `height` (object)
      - option 1 (object)
      - option 2 (object)
  - type: "video" (object)
    - `index` (integer, required)
    - `type` ("video", required)
    - `media_type` (string, required)
    - `uri` (string | null)
    - `data` (string | null)
    - `alt` (string | null)
    - `width` (object)
      - option 1 (object)
      - option 2 (object)
    - `height` (object)
      - option 1 (object)
      - option 2 (object)
    - `duration_ms` (object)
      - option 1 (object)
      - option 2 (object)
    - `size_bytes` (object)
      - option 1 (object)
      - option 2 (object)
    - `thumbnail_uri` (string | null)
  - type: "status" (object)
    - `index` (integer, required)
    - `type` ("status", required)
    - `level` (enum<string>, required) — "thinking" · "working" · "waiting" · "idle" · "done"
    - `text` (string | null)
  - type: "error" (object)
    - `index` (integer, required)
    - `type` ("error", required)
    - `code` (string, required)
    - `message` (string, required)
    - `retryable` (boolean) — default: false

Responses:

- `202` — Accepted — body is {}; output arrives on the stream
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/sessions/sess_cmtb7q2j/messages \
  -H "Authorization: Bearer ast_..." \
  -H "Content-Type: application/json" \
  -d '{"parts":[{"type":"text","index":0,"text":"Summarize today's signups"}]}'
```

Example response (202 Accepted):

```json
{}
```

### POST /sessions/{id}/events

Send events — the one input: user.message, user.interrupt, system.message (scope: write)

The conversation plane (edge-routed to the chat-proxy). Body {events: [...]} (1–50), validated all-or-nothing. A user.message carries content blocks: text, image and document with base64 / url / file sources (inline base64 ≤ 20 MiB per request; larger content goes through POST /files). A system.message (text only) must be last and follow the user.message it accompanies: privileged context for that turn and, through the agent's transcript, later turns. user.interrupt stops the in-flight turn. Every event echoes back in the history shape with a stable id; sources are echoed as file sources naming per-session copies (never base64). Optional Idempotency-Key header: a replay returns the original echo. 402 insufficient_credits / budget_reached, 503 egress_policy_failed, 400 session_archived refuse the whole request before any row is written.

Path parameters:

- `id` (string, required) — The session id

Body (application/json):

- `events` (object[], required)
  - type: "user.message" (object)
    - `type` ("user.message", required)
    - `content` (object[], required)
      - type: "text" (object)
        - `type` ("text", required)
        - `text` (string, required)
      - type: "image" (object)
        - `type` ("image", required)
        - `source` (object, required)
          - type: "base64" (object)
            - `type` ("base64", required)
            - `media_type` (string, required)
            - `data` (string, required)
          - type: "url" (object)
            - `type` ("url", required)
            - `url` (string, required)
          - type: "file" (object)
            - `type` ("file", required)
            - `file_id` (string, required)
      - type: "document" (object)
        - `type` ("document", required)
        - `source` (object, required)
          - type: "base64" (object)
            - `type` ("base64", required)
            - `media_type` (string, required)
            - `data` (string, required)
          - type: "text" (object)
            - `type` ("text", required)
            - `media_type` ("text/plain") — default: "text/plain"
            - `data` (string, required)
          - type: "url" (object)
            - `type` ("url", required)
            - `url` (string, required)
          - type: "file" (object)
            - `type` ("file", required)
            - `file_id` (string, required)
        - `title` (string | null)
        - `context` (string | null)
  - type: "system.message" (object)
    - `type` ("system.message", required)
    - `content` (object[], required)
      - `type` ("text", required)
      - `text` (string, required)
  - type: "user.interrupt" (object)
    - `type` ("user.interrupt", required)

Responses:

- `200` — OK — one echo per event, in order
  - `data` (object[], required) — One echo per event, in order, in the events-history shape
    - `id` (string, required) — Stable event id — the dedupe key across stream ∪ events
    - `type` (string, required) — user.message · system.message (both history only — never on the live stream; they carry `content`) · agent.message · agent.reasoning · agent.tool_use · agent.tool_result · agent.status · user.interrupt · session.status_running · session.status_idle · session.error · session.status_terminated · session.deleted
    - `sessionId` (string, required)
    - `agentId` (string | null) — The agent id (agent_…)
    - `at` (string) — ISO 8601
    - `messageId` (string) — agent.message
    - `parts` (object[]) — agent.message
      - type: "text" (object)
        - `index` (integer, required)
        - `type` ("text", required)
        - `text` (string) — default: ""
        - `stream_id` (object)
          - option 1 (object)
          - option 2 (object)
        - `stream_index` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "reasoning" (object)
        - `index` (integer, required)
        - `type` ("reasoning", required)
        - `text` (string) — default: ""
        - `redacted` (boolean) — default: false
        - `stream_id` (object)
          - option 1 (object)
          - option 2 (object)
        - `stream_index` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "tool_call" (object)
        - `index` (integer, required)
        - `type` ("tool_call", required)
        - `call_id` (string, required)
        - `tool_name` (string, required)
        - `args` (object) — default: {}
        - `args_partial` (string | null)
      - type: "tool_result" (object)
        - `index` (integer, required)
        - `type` ("tool_result", required)
        - `call_id` (string, required)
        - `tool_name` (string, required)
        - `status` (enum<string>) — "ok" · "error" — default: "ok"
        - `result` (object) — default: {}
      - type: "file" (object)
        - `index` (integer, required)
        - `type` ("file", required)
        - `name` (string, required)
        - `media_type` (string, required)
        - `uri` (string | null)
        - `data` (string | null)
        - `size_bytes` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "image" (object)
        - `index` (integer, required)
        - `type` ("image", required)
        - `media_type` (string, required)
        - `uri` (string | null)
        - `data` (string | null)
        - `alt` (string | null)
        - `width` (object)
          - option 1 (object)
          - option 2 (object)
        - `height` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "video" (object)
        - `index` (integer, required)
        - `type` ("video", required)
        - `media_type` (string, required)
        - `uri` (string | null)
        - `data` (string | null)
        - `alt` (string | null)
        - `width` (object)
          - option 1 (object)
          - option 2 (object)
        - `height` (object)
          - option 1 (object)
          - option 2 (object)
        - `duration_ms` (object)
          - option 1 (object)
          - option 2 (object)
        - `size_bytes` (object)
          - option 1 (object)
          - option 2 (object)
        - `thumbnail_uri` (string | null)
      - type: "status" (object)
        - `index` (integer, required)
        - `type` ("status", required)
        - `level` (enum<string>, required) — "thinking" · "working" · "waiting" · "idle" · "done"
        - `text` (string | null)
      - type: "error" (object)
        - `index` (integer, required)
        - `type` ("error", required)
        - `code` (string, required)
        - `message` (string, required)
        - `retryable` (boolean) — default: false
    - `text` (string) — agent.message — parts flattened to plain text
    - `content` (object[]) — user.message / system.message — the CMA content blocks as sent (text · image · document); every image/document source is a `file` source naming a per-session copy readable through GET /files/{file_id}/content
    - `part` (object) — reasoning / tool_use / tool_result / status events
      - type: "text" (object)
        - `index` (integer, required)
        - `type` ("text", required)
        - `text` (string) — default: ""
        - `stream_id` (object)
          - option 1 (object)
          - option 2 (object)
        - `stream_index` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "reasoning" (object)
        - `index` (integer, required)
        - `type` ("reasoning", required)
        - `text` (string) — default: ""
        - `redacted` (boolean) — default: false
        - `stream_id` (object)
          - option 1 (object)
          - option 2 (object)
        - `stream_index` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "tool_call" (object)
        - `index` (integer, required)
        - `type` ("tool_call", required)
        - `call_id` (string, required)
        - `tool_name` (string, required)
        - `args` (object) — default: {}
        - `args_partial` (string | null)
      - type: "tool_result" (object)
        - `index` (integer, required)
        - `type` ("tool_result", required)
        - `call_id` (string, required)
        - `tool_name` (string, required)
        - `status` (enum<string>) — "ok" · "error" — default: "ok"
        - `result` (object) — default: {}
      - type: "file" (object)
        - `index` (integer, required)
        - `type` ("file", required)
        - `name` (string, required)
        - `media_type` (string, required)
        - `uri` (string | null)
        - `data` (string | null)
        - `size_bytes` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "image" (object)
        - `index` (integer, required)
        - `type` ("image", required)
        - `media_type` (string, required)
        - `uri` (string | null)
        - `data` (string | null)
        - `alt` (string | null)
        - `width` (object)
          - option 1 (object)
          - option 2 (object)
        - `height` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "video" (object)
        - `index` (integer, required)
        - `type` ("video", required)
        - `media_type` (string, required)
        - `uri` (string | null)
        - `data` (string | null)
        - `alt` (string | null)
        - `width` (object)
          - option 1 (object)
          - option 2 (object)
        - `height` (object)
          - option 1 (object)
          - option 2 (object)
        - `duration_ms` (object)
          - option 1 (object)
          - option 2 (object)
        - `size_bytes` (object)
          - option 1 (object)
          - option 2 (object)
        - `thumbnail_uri` (string | null)
      - type: "status" (object)
        - `index` (integer, required)
        - `type` ("status", required)
        - `level` (enum<string>, required) — "thinking" · "working" · "waiting" · "idle" · "done"
        - `text` (string | null)
      - type: "error" (object)
        - `index` (integer, required)
        - `type` ("error", required)
        - `code` (string, required)
        - `message` (string, required)
        - `retryable` (boolean) — default: false
    - `stop_reason` (object) — session.status_idle — discriminate on type; never break on bare idle
      - `type` (string) — "end_turn" (also after an interrupt) | "retries_exhausted" (preceded by an exhausted session.error) | "budget_reached"; more reserved
    - `error` (object) — session.error — the stream stays open; a session.status_idle follows once the turn is over
      - `type` (string) — e.g. "pod_unavailable"
      - `message` (string)
      - `retry_status` (string) — "retrying" (the turn continues) | "exhausted" (the turn is over)
    - `by` (string) — user.interrupt — who requested the interrupt
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/sessions/sess_cmtb7q2j/events \
  -H "Authorization: Bearer ast_..." \
  -H "Content-Type: application/json" \
  -d '{"events":[{"type":"user.message","content":[{"type":"text","text":"What is in this screenshot?"},{"type":"image","source":{"type":"file","file_id":"file_cmtb9x4p"}}]},{"type":"system.message","content":[{"type":"text","text":"Answer in one paragraph."}]}]}'
```

Example response (200 OK):

```json
{
  "data": [
    {
      "id": "api-user-3f2c…",
      "type": "user.message",
      "sessionId": "sess_cmtb7q2j",
      "agentId": "agent_cmtb2f0a8m9k7x51q4ez6r0d",
      "at": "2026-09-10T18:00:00.000Z",
      "content": [
        {
          "type": "text",
          "text": "What is in this screenshot?"
        },
        {
          "type": "image",
          "source": {
            "type": "file",
            "file_id": "file_cmtb9y1q"
          }
        }
      ]
    },
    {
      "id": "api-user-3f2c…#system",
      "type": "system.message",
      "sessionId": "sess_cmtb7q2j",
      "agentId": "agent_cmtb2f0a8m9k7x51q4ez6r0d",
      "at": "2026-09-10T18:00:00.000Z",
      "content": [
        {
          "type": "text",
          "text": "Answer in one paragraph."
        }
      ]
    }
  ]
}
```

### GET /sessions/{id}/events

The event history — the same event objects the stream delivers (scope: read)

The session's history, one event per message: user.message and system.message (with content), agent.message (with parts), user.interrupt, and the turn marks session.status_running, session.status_idle (stop_reason.type is end_turn, retries_exhausted or budget_reached) and session.error. agent.reasoning, agent.tool_use, agent.tool_result and agent.status appear on the live stream only, not in history. Oldest-first with an opaque numeric cursor. Reconnect = open the stream, list events, dedupe by event id. types[] filters (e.g. types[]=user.message&types[]=agent.message is the transcript view).

Path parameters:

- `id` (string, required) — The session id

Query parameters:

- `cursor` (string)
- `limit` (integer)
- `types[]` (array)

Responses:

- `200` — OK
  - `events` (object[], required) — Oldest-first
    - `id` (string, required) — Stable event id — the dedupe key across stream ∪ events
    - `type` (string, required) — user.message · system.message (both history only — never on the live stream; they carry `content`) · agent.message · agent.reasoning · agent.tool_use · agent.tool_result · agent.status · user.interrupt · session.status_running · session.status_idle · session.error · session.status_terminated · session.deleted
    - `sessionId` (string, required)
    - `agentId` (string | null) — The agent id (agent_…)
    - `at` (string) — ISO 8601
    - `messageId` (string) — agent.message
    - `parts` (object[]) — agent.message
      - type: "text" (object)
        - `index` (integer, required)
        - `type` ("text", required)
        - `text` (string) — default: ""
        - `stream_id` (object)
          - option 1 (object)
          - option 2 (object)
        - `stream_index` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "reasoning" (object)
        - `index` (integer, required)
        - `type` ("reasoning", required)
        - `text` (string) — default: ""
        - `redacted` (boolean) — default: false
        - `stream_id` (object)
          - option 1 (object)
          - option 2 (object)
        - `stream_index` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "tool_call" (object)
        - `index` (integer, required)
        - `type` ("tool_call", required)
        - `call_id` (string, required)
        - `tool_name` (string, required)
        - `args` (object) — default: {}
        - `args_partial` (string | null)
      - type: "tool_result" (object)
        - `index` (integer, required)
        - `type` ("tool_result", required)
        - `call_id` (string, required)
        - `tool_name` (string, required)
        - `status` (enum<string>) — "ok" · "error" — default: "ok"
        - `result` (object) — default: {}
      - type: "file" (object)
        - `index` (integer, required)
        - `type` ("file", required)
        - `name` (string, required)
        - `media_type` (string, required)
        - `uri` (string | null)
        - `data` (string | null)
        - `size_bytes` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "image" (object)
        - `index` (integer, required)
        - `type` ("image", required)
        - `media_type` (string, required)
        - `uri` (string | null)
        - `data` (string | null)
        - `alt` (string | null)
        - `width` (object)
          - option 1 (object)
          - option 2 (object)
        - `height` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "video" (object)
        - `index` (integer, required)
        - `type` ("video", required)
        - `media_type` (string, required)
        - `uri` (string | null)
        - `data` (string | null)
        - `alt` (string | null)
        - `width` (object)
          - option 1 (object)
          - option 2 (object)
        - `height` (object)
          - option 1 (object)
          - option 2 (object)
        - `duration_ms` (object)
          - option 1 (object)
          - option 2 (object)
        - `size_bytes` (object)
          - option 1 (object)
          - option 2 (object)
        - `thumbnail_uri` (string | null)
      - type: "status" (object)
        - `index` (integer, required)
        - `type` ("status", required)
        - `level` (enum<string>, required) — "thinking" · "working" · "waiting" · "idle" · "done"
        - `text` (string | null)
      - type: "error" (object)
        - `index` (integer, required)
        - `type` ("error", required)
        - `code` (string, required)
        - `message` (string, required)
        - `retryable` (boolean) — default: false
    - `text` (string) — agent.message — parts flattened to plain text
    - `content` (object[]) — user.message / system.message — the CMA content blocks as sent (text · image · document); every image/document source is a `file` source naming a per-session copy readable through GET /files/{file_id}/content
    - `part` (object) — reasoning / tool_use / tool_result / status events
      - type: "text" (object)
        - `index` (integer, required)
        - `type` ("text", required)
        - `text` (string) — default: ""
        - `stream_id` (object)
          - option 1 (object)
          - option 2 (object)
        - `stream_index` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "reasoning" (object)
        - `index` (integer, required)
        - `type` ("reasoning", required)
        - `text` (string) — default: ""
        - `redacted` (boolean) — default: false
        - `stream_id` (object)
          - option 1 (object)
          - option 2 (object)
        - `stream_index` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "tool_call" (object)
        - `index` (integer, required)
        - `type` ("tool_call", required)
        - `call_id` (string, required)
        - `tool_name` (string, required)
        - `args` (object) — default: {}
        - `args_partial` (string | null)
      - type: "tool_result" (object)
        - `index` (integer, required)
        - `type` ("tool_result", required)
        - `call_id` (string, required)
        - `tool_name` (string, required)
        - `status` (enum<string>) — "ok" · "error" — default: "ok"
        - `result` (object) — default: {}
      - type: "file" (object)
        - `index` (integer, required)
        - `type` ("file", required)
        - `name` (string, required)
        - `media_type` (string, required)
        - `uri` (string | null)
        - `data` (string | null)
        - `size_bytes` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "image" (object)
        - `index` (integer, required)
        - `type` ("image", required)
        - `media_type` (string, required)
        - `uri` (string | null)
        - `data` (string | null)
        - `alt` (string | null)
        - `width` (object)
          - option 1 (object)
          - option 2 (object)
        - `height` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "video" (object)
        - `index` (integer, required)
        - `type` ("video", required)
        - `media_type` (string, required)
        - `uri` (string | null)
        - `data` (string | null)
        - `alt` (string | null)
        - `width` (object)
          - option 1 (object)
          - option 2 (object)
        - `height` (object)
          - option 1 (object)
          - option 2 (object)
        - `duration_ms` (object)
          - option 1 (object)
          - option 2 (object)
        - `size_bytes` (object)
          - option 1 (object)
          - option 2 (object)
        - `thumbnail_uri` (string | null)
      - type: "status" (object)
        - `index` (integer, required)
        - `type` ("status", required)
        - `level` (enum<string>, required) — "thinking" · "working" · "waiting" · "idle" · "done"
        - `text` (string | null)
      - type: "error" (object)
        - `index` (integer, required)
        - `type` ("error", required)
        - `code` (string, required)
        - `message` (string, required)
        - `retryable` (boolean) — default: false
    - `stop_reason` (object) — session.status_idle — discriminate on type; never break on bare idle
      - `type` (string) — "end_turn" (also after an interrupt) | "retries_exhausted" (preceded by an exhausted session.error) | "budget_reached"; more reserved
    - `error` (object) — session.error — the stream stays open; a session.status_idle follows once the turn is over
      - `type` (string) — e.g. "pod_unavailable"
      - `message` (string)
      - `retry_status` (string) — "retrying" (the turn continues) | "exhausted" (the turn is over)
    - `by` (string) — user.interrupt — who requested the interrupt
  - `cursor` (string | null, required) — Pass back as ?cursor= for the next page; null when the session has no events
  - `hasMore` (boolean, required)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/sessions/sess_cmtb7q2j/events?cursor=&limit=&types[]= \
  -H "Authorization: Bearer ast_..."
```

Example response (200 OK):

```json
{
  "events": [
    {
      "id": "turn_8c41f0…#idle",
      "type": "session.status_idle",
      "sessionId": "sess-8c41f0…",
      "agent": "agent_cmtb2f0a8m9k7x51q4ez6r0d",
      "stop_reason": {
        "type": "end_turn"
      }
    }
  ],
  "cursor": "42",
  "hasMore": false
}
```

### GET /sessions/{id}/stream

The standing event stream (SSE) (scope: read)

text/event-stream, live-only, stays open across turns. Events: agent.message, agent.reasoning, agent.tool_use, agent.tool_result, agent.status, session.status_running, session.status_idle (carries stop_reason — never break on bare idle), session.error (the stream stays open; an idle follows), user.interrupt, session.status_terminated (terminal), session.deleted (terminal). User messages are not sent on the live stream — they appear in the event history only. Reconnect = reopen + list events + dedupe by event id.

Path parameters:

- `id` (string, required) — The session id

Responses:

- `200` — SSE stream
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -N https://agentsky.dev/sessions/sess_cmtb7q2j/stream \
  -H "Authorization: Bearer ast_..."
```

### POST /sessions/{id}/interrupt

Abort the in-flight turn (scope: write)

Path parameters:

- `id` (string, required) — The session id

Responses:

- `200` — OK
  - `status` (enum<string>, required) — no_turn = nothing in flight; otherwise user.interrupt echoes on the stream and the pod's session.status_idle follows — "interrupting" · "no_turn"
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/sessions/sess_cmtb7q2j/interrupt \
  -H "Authorization: Bearer ast_..."
```

Example response (200 OK):

```json
{
  "status": "interrupting"
}
```

### POST /channels/threads/{id}/messages

Post into a thread — markdown renders natively per platform (scope: write)

Path parameters:

- `id` (string, required) — The thread id (from message.received or channel events)

Body (application/json):

- `parts` (object[], required)
  - type: "text" (object)
    - `type` ("text", required)
    - `text` (string, required)
  - type: "markdown" (object)
    - `type` ("markdown", required)
    - `text` (string, required)
  - type: "raw" (object)
    - `type` ("raw", required)
    - `platform` (string, required)
    - `payload` (string, required) — Platform-native payload, JSON-encoded
- `display_name` (string) — max length 80
- `done` (boolean) — Clears the working marker (✅) for the replied-to message

Responses:

- `201` — Created
  - `message_id` (string)
  - `thread_id` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/channels/threads/sess_cmtb7q2j/messages \
  -H "Authorization: Bearer ast_..."
```

### GET /channels/events/stream

SSE mirror of your webhook events — for local development (scope: read)

Responses:

- `200` — SSE stream
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -N https://agentsky.dev/channels/events/stream \
  -H "Authorization: Bearer ast_..."
```

## Routines

### GET /routines

List routines (scope: read)

Query parameters:

- `cursor` (string)
- `limit` (integer) — 1–500, default 50

Responses:

- `200` — OK
  - `items` (object[], required)
    - `id` (string) — rtn_… identifier
    - `name` (string)
    - `description` (string | null)
    - `metadata` (object)
    - `target` (object) — {type:"new_session", agent, environment_id?, vault_ids?} — a fresh session per fire (CMA parity) · {type:"session", session_id} — a turn into the existing session every fire (busy target skips with session_busy_error)
    - `initial_events` (object[])
    - `schedule` (object | null) — 5-field POSIX cron + IANA timezone, wall-clock matching, minute granularity; null = manual-run only
    - `budget` (object | null) — Per-run cap (RFC-0080 shape): copied onto each new session (mode new_session) or granted as a per-fire delta allowance consumed+budget (mode session). Clearable.
    - `status` (enum<string>) — "active" · "paused"
    - `paused_reason` (object | null) — {type:"manual"} | {type:"error", error:{type}} — non-null exactly when paused
    - `archived_at` (string | null)
    - `created_at` (string)
    - `updated_at` (string)
  - `cursor` (string | null)
  - `hasMore` (boolean)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/routines?cursor=&limit= \
  -H "Authorization: Bearer ast_..."
```

### POST /routines

Create a routine — scheduled agent execution (scope: write)

initial_events (1–50 user.message events) are delivered on every fire. target picks the mode: new_session creates a fresh session per run; session sends the turn into an existing session (busy target → failed run session_busy_error). schedule is a 5-field POSIX cron with an IANA timezone; omit it for a manual-run-only routine. budget bounds each run separately.

Body (application/json):

- `name` (string, required) — max length 256
- `description` (string) — max length 2048
- `metadata` (object)
- `target` (object, required)
  - type: "new_session" (object)
    - `type` ("new_session", required)
    - `agent` (string, required)
    - `environment_id` (string)
    - `vault_ids` (string[])
  - type: "session" (object)
    - `type` ("session", required)
    - `session_id` (string, required)
- `initial_events` (object[], required)
  - `type` ("user.message", required)
  - `content` (object[], required)
    - type: "text" (object)
      - `type` ("text", required)
      - `text` (string, required)
    - type: "image" (object)
      - `type` ("image", required)
      - `source` (object, required)
        - type: "base64" (object)
          - `type` ("base64", required)
          - `media_type` (string, required)
          - `data` (string, required)
        - type: "url" (object)
          - `type` ("url", required)
          - `url` (string, required)
        - type: "file" (object)
          - `type` ("file", required)
          - `file_id` (string, required)
    - type: "document" (object)
      - `type` ("document", required)
      - `source` (object, required)
        - type: "base64" (object)
          - `type` ("base64", required)
          - `media_type` (string, required)
          - `data` (string, required)
        - type: "text" (object)
          - `type` ("text", required)
          - `media_type` ("text/plain") — default: "text/plain"
          - `data` (string, required)
        - type: "url" (object)
          - `type` ("url", required)
          - `url` (string, required)
        - type: "file" (object)
          - `type` ("file", required)
          - `file_id` (string, required)
      - `title` (string | null)
      - `context` (string | null)
- `schedule` (object)
  - type: "cron" (object)
    - `type` ("cron", required)
    - `expression` (string, required) — max length 256
    - `timezone` (string, required)
  - option 2 (object)
- `budget` (object)
  - type: "limit" (object)
    - `type` ("limit", required)
    - `max_list_cost` (object, required)
      - `amount` (string, required) — Whole US cents as an integer decimal string, e.g. "125" for $1.25 — max length 15
      - `currency` ("USD", required)
  - option 2 (object)

Responses:

- `201` — Created
  - `routine` (object, required) — Scheduled agent execution: initial_events + a target (new_session per fire, or an existing session) bound to a cron schedule. Fires record routine runs.
    - `id` (string) — rtn_… identifier
    - `name` (string)
    - `description` (string | null)
    - `metadata` (object)
    - `target` (object) — {type:"new_session", agent, environment_id?, vault_ids?} — a fresh session per fire (CMA parity) · {type:"session", session_id} — a turn into the existing session every fire (busy target skips with session_busy_error)
    - `initial_events` (object[])
    - `schedule` (object | null) — 5-field POSIX cron + IANA timezone, wall-clock matching, minute granularity; null = manual-run only
    - `budget` (object | null) — Per-run cap (RFC-0080 shape): copied onto each new session (mode new_session) or granted as a per-fire delta allowance consumed+budget (mode session). Clearable.
    - `status` (enum<string>) — "active" · "paused"
    - `paused_reason` (object | null) — {type:"manual"} | {type:"error", error:{type}} — non-null exactly when paused
    - `archived_at` (string | null)
    - `created_at` (string)
    - `updated_at` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/routines \
  -H "Authorization: Bearer ast_..." \
  -H "Content-Type: application/json" \
  -d '{"name":"Weekly compliance scan","target":{"type":"new_session","agent":"agent_cmtb2f0a8m9k7x51q4ez6r0d"},"initial_events":[{"type":"user.message","content":[{"type":"text","text":"Run the weekly compliance scan."}]}],"schedule":{"type":"cron","expression":"0 20 * * 5","timezone":"America/New_York"}}'
```

### GET /routines/{id}

Routine detail (scope: read)

Path parameters:

- `id` (string, required) — The routine id (rtn_…)

Responses:

- `200` — OK
  - `routine` (object, required) — Scheduled agent execution: initial_events + a target (new_session per fire, or an existing session) bound to a cron schedule. Fires record routine runs.
    - `id` (string) — rtn_… identifier
    - `name` (string)
    - `description` (string | null)
    - `metadata` (object)
    - `target` (object) — {type:"new_session", agent, environment_id?, vault_ids?} — a fresh session per fire (CMA parity) · {type:"session", session_id} — a turn into the existing session every fire (busy target skips with session_busy_error)
    - `initial_events` (object[])
    - `schedule` (object | null) — 5-field POSIX cron + IANA timezone, wall-clock matching, minute granularity; null = manual-run only
    - `budget` (object | null) — Per-run cap (RFC-0080 shape): copied onto each new session (mode new_session) or granted as a per-fire delta allowance consumed+budget (mode session). Clearable.
    - `status` (enum<string>) — "active" · "paused"
    - `paused_reason` (object | null) — {type:"manual"} | {type:"error", error:{type}} — non-null exactly when paused
    - `archived_at` (string | null)
    - `created_at` (string)
    - `updated_at` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/routines/sess_cmtb7q2j \
  -H "Authorization: Bearer ast_..."
```

### PATCH /routines/{id}

Update a routine — omit preserves; null clears schedule/budget (scope: write)

initial_events is a full replacement; metadata is key-patched (null deletes a key); name and target cannot be cleared. Archived routines are immutable (409).

Path parameters:

- `id` (string, required) — The routine id (rtn_…)

Body (application/json):

- `name` (string) — max length 256
- `description` (object)
  - option 1 (object)
  - option 2 (object)
- `metadata` (object)
- `target` (object)
  - type: "new_session" (object)
    - `type` ("new_session", required)
    - `agent` (string, required)
    - `environment_id` (string)
    - `vault_ids` (string[])
  - type: "session" (object)
    - `type` ("session", required)
    - `session_id` (string, required)
- `initial_events` (object[])
  - `type` ("user.message", required)
  - `content` (object[], required)
    - type: "text" (object)
      - `type` ("text", required)
      - `text` (string, required)
    - type: "image" (object)
      - `type` ("image", required)
      - `source` (object, required)
        - type: "base64" (object)
          - `type` ("base64", required)
          - `media_type` (string, required)
          - `data` (string, required)
        - type: "url" (object)
          - `type` ("url", required)
          - `url` (string, required)
        - type: "file" (object)
          - `type` ("file", required)
          - `file_id` (string, required)
    - type: "document" (object)
      - `type` ("document", required)
      - `source` (object, required)
        - type: "base64" (object)
          - `type` ("base64", required)
          - `media_type` (string, required)
          - `data` (string, required)
        - type: "text" (object)
          - `type` ("text", required)
          - `media_type` ("text/plain") — default: "text/plain"
          - `data` (string, required)
        - type: "url" (object)
          - `type` ("url", required)
          - `url` (string, required)
        - type: "file" (object)
          - `type` ("file", required)
          - `file_id` (string, required)
      - `title` (string | null)
      - `context` (string | null)
- `schedule` (object)
  - type: "cron" (object)
    - `type` ("cron", required)
    - `expression` (string, required) — max length 256
    - `timezone` (string, required)
  - option 2 (object)
- `budget` (object)
  - type: "limit" (object)
    - `type` ("limit", required)
    - `max_list_cost` (object, required)
      - `amount` (string, required) — Whole US cents as an integer decimal string, e.g. "125" for $1.25 — max length 15
      - `currency` ("USD", required)
  - option 2 (object)

Responses:

- `200` — OK
  - `routine` (object, required) — Scheduled agent execution: initial_events + a target (new_session per fire, or an existing session) bound to a cron schedule. Fires record routine runs.
    - `id` (string) — rtn_… identifier
    - `name` (string)
    - `description` (string | null)
    - `metadata` (object)
    - `target` (object) — {type:"new_session", agent, environment_id?, vault_ids?} — a fresh session per fire (CMA parity) · {type:"session", session_id} — a turn into the existing session every fire (busy target skips with session_busy_error)
    - `initial_events` (object[])
    - `schedule` (object | null) — 5-field POSIX cron + IANA timezone, wall-clock matching, minute granularity; null = manual-run only
    - `budget` (object | null) — Per-run cap (RFC-0080 shape): copied onto each new session (mode new_session) or granted as a per-fire delta allowance consumed+budget (mode session). Clearable.
    - `status` (enum<string>) — "active" · "paused"
    - `paused_reason` (object | null) — {type:"manual"} | {type:"error", error:{type}} — non-null exactly when paused
    - `archived_at` (string | null)
    - `created_at` (string)
    - `updated_at` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X PATCH https://agentsky.dev/routines/sess_cmtb7q2j \
  -H "Authorization: Bearer ast_..." \
  -H "Content-Type: application/json" \
  -d '{"schedule":{"type":"cron","expression":"0 9 * * 1-5","timezone":"UTC"}}'
```

### POST /routines/{id}/pause

Pause — suppress scheduled fires; manual runs still work (scope: write)

Path parameters:

- `id` (string, required) — The routine id (rtn_…)

Responses:

- `200` — OK
  - `routine` (object, required) — Scheduled agent execution: initial_events + a target (new_session per fire, or an existing session) bound to a cron schedule. Fires record routine runs.
    - `id` (string) — rtn_… identifier
    - `name` (string)
    - `description` (string | null)
    - `metadata` (object)
    - `target` (object) — {type:"new_session", agent, environment_id?, vault_ids?} — a fresh session per fire (CMA parity) · {type:"session", session_id} — a turn into the existing session every fire (busy target skips with session_busy_error)
    - `initial_events` (object[])
    - `schedule` (object | null) — 5-field POSIX cron + IANA timezone, wall-clock matching, minute granularity; null = manual-run only
    - `budget` (object | null) — Per-run cap (RFC-0080 shape): copied onto each new session (mode new_session) or granted as a per-fire delta allowance consumed+budget (mode session). Clearable.
    - `status` (enum<string>) — "active" · "paused"
    - `paused_reason` (object | null) — {type:"manual"} | {type:"error", error:{type}} — non-null exactly when paused
    - `archived_at` (string | null)
    - `created_at` (string)
    - `updated_at` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/routines/sess_cmtb7q2j/pause \
  -H "Authorization: Bearer ast_..."
```

### POST /routines/{id}/unpause

Unpause — resumes from the NEXT occurrence; missed fires are never backfilled (scope: write)

Path parameters:

- `id` (string, required) — The routine id (rtn_…)

Responses:

- `200` — OK
  - `routine` (object, required) — Scheduled agent execution: initial_events + a target (new_session per fire, or an existing session) bound to a cron schedule. Fires record routine runs.
    - `id` (string) — rtn_… identifier
    - `name` (string)
    - `description` (string | null)
    - `metadata` (object)
    - `target` (object) — {type:"new_session", agent, environment_id?, vault_ids?} — a fresh session per fire (CMA parity) · {type:"session", session_id} — a turn into the existing session every fire (busy target skips with session_busy_error)
    - `initial_events` (object[])
    - `schedule` (object | null) — 5-field POSIX cron + IANA timezone, wall-clock matching, minute granularity; null = manual-run only
    - `budget` (object | null) — Per-run cap (RFC-0080 shape): copied onto each new session (mode new_session) or granted as a per-fire delta allowance consumed+budget (mode session). Clearable.
    - `status` (enum<string>) — "active" · "paused"
    - `paused_reason` (object | null) — {type:"manual"} | {type:"error", error:{type}} — non-null exactly when paused
    - `archived_at` (string | null)
    - `created_at` (string)
    - `updated_at` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/routines/sess_cmtb7q2j/unpause \
  -H "Authorization: Bearer ast_..."
```

### POST /routines/{id}/archive

Archive — terminal; the schedule stops and the routine becomes immutable (scope: write)

Path parameters:

- `id` (string, required) — The routine id (rtn_…)

Responses:

- `200` — OK
  - `routine` (object, required) — Scheduled agent execution: initial_events + a target (new_session per fire, or an existing session) bound to a cron schedule. Fires record routine runs.
    - `id` (string) — rtn_… identifier
    - `name` (string)
    - `description` (string | null)
    - `metadata` (object)
    - `target` (object) — {type:"new_session", agent, environment_id?, vault_ids?} — a fresh session per fire (CMA parity) · {type:"session", session_id} — a turn into the existing session every fire (busy target skips with session_busy_error)
    - `initial_events` (object[])
    - `schedule` (object | null) — 5-field POSIX cron + IANA timezone, wall-clock matching, minute granularity; null = manual-run only
    - `budget` (object | null) — Per-run cap (RFC-0080 shape): copied onto each new session (mode new_session) or granted as a per-fire delta allowance consumed+budget (mode session). Clearable.
    - `status` (enum<string>) — "active" · "paused"
    - `paused_reason` (object | null) — {type:"manual"} | {type:"error", error:{type}} — non-null exactly when paused
    - `archived_at` (string | null)
    - `created_at` (string)
    - `updated_at` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/routines/sess_cmtb7q2j/archive \
  -H "Authorization: Bearer ast_..."
```

### POST /routines/{id}/run

Manual run — fire now, outside the schedule; works while paused (scope: write)

Records trigger_context {type:"manual"}; manual runs emit no routine_run webhooks.

Path parameters:

- `id` (string, required) — The routine id (rtn_…)

Responses:

- `201` — Created
  - `run` (object, required) — One fire attempt. Exactly one of session_id / error is non-null.
    - `id` (string) — rrun_… identifier
    - `routine_id` (string)
    - `trigger_context` (object) — {type:"schedule", scheduled_at} | {type:"manual"}
    - `session_id` (string | null)
    - `error` (object | null) — {type, message}. Types: agent_archived_error · agent_not_found_error · session_archived_error · session_not_found_error · session_busy_error · environment_archived_error · environment_not_found_error · vault_not_found_error · insufficient_credits_error · rate_limited_error · run_rejected_error · unknown_error. Unrecoverable types auto-pause the routine; transient ones (credits, rate limit, busy) leave it active for the next occurrence.
    - `agent` (object) — Resolved agent reference at fire time
    - `created_at` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/routines/sess_cmtb7q2j/run \
  -H "Authorization: Bearer ast_..."
```

### GET /routine-runs

List routine runs — every fire attempt (scope: read)

Query parameters:

- `routine` (string) — Filter to one routine (rtn_…)
- `has_error` (boolean) — true = failed runs, false = runs with a session
- `trigger_type` (string)
- `created_at[gte]` (string) — Also gt / lt / lte
- `cursor` (string)
- `limit` (integer)

Responses:

- `200` — OK
  - `items` (object[], required)
    - `id` (string) — rrun_… identifier
    - `routine_id` (string)
    - `trigger_context` (object) — {type:"schedule", scheduled_at} | {type:"manual"}
    - `session_id` (string | null)
    - `error` (object | null) — {type, message}. Types: agent_archived_error · agent_not_found_error · session_archived_error · session_not_found_error · session_busy_error · environment_archived_error · environment_not_found_error · vault_not_found_error · insufficient_credits_error · rate_limited_error · run_rejected_error · unknown_error. Unrecoverable types auto-pause the routine; transient ones (credits, rate limit, busy) leave it active for the next occurrence.
    - `agent` (object) — Resolved agent reference at fire time
    - `created_at` (string)
  - `cursor` (string | null)
  - `hasMore` (boolean)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/routine-runs?routine=&has_error=&trigger_type=&created_at[gte]=&cursor=&limit= \
  -H "Authorization: Bearer ast_..."
```

### GET /routine-runs/{id}

Routine run detail (scope: read)

Path parameters:

- `id` (string, required) — The run id (rrun_…)

Responses:

- `200` — OK
  - `run` (object, required) — One fire attempt. Exactly one of session_id / error is non-null.
    - `id` (string) — rrun_… identifier
    - `routine_id` (string)
    - `trigger_context` (object) — {type:"schedule", scheduled_at} | {type:"manual"}
    - `session_id` (string | null)
    - `error` (object | null) — {type, message}. Types: agent_archived_error · agent_not_found_error · session_archived_error · session_not_found_error · session_busy_error · environment_archived_error · environment_not_found_error · vault_not_found_error · insufficient_credits_error · rate_limited_error · run_rejected_error · unknown_error. Unrecoverable types auto-pause the routine; transient ones (credits, rate limit, busy) leave it active for the next occurrence.
    - `agent` (object) — Resolved agent reference at fire time
    - `created_at` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/routine-runs/sess_cmtb7q2j \
  -H "Authorization: Bearer ast_..."
```

## Webhooks

### GET /model-subscriptions

List connected model subscriptions — metadata only, never credentials (scope: read)

Responses:

- `200` — OK
  - `modelSubscriptions` (object[], required)
    - `provider` (enum<string>) — "anthropic" · "openai"
    - `label` (string)
    - `status` (enum<string>) — "connected" · "needs_reconnect"
    - `expiresAt` (string | null)
    - `lastUsedAt` (string | null)
    - `useForNewAgents` (boolean)
    - `createdAt` (string)
    - `agents` (object[]) — Agents in this universe running on the subscription
      - `id` (string)
      - `agentId` (string | null)
      - `name` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/model-subscriptions \
  -H "Authorization: Bearer ast_..."
```

### PUT /model-subscriptions/{provider}

Connect or replace a subscription credential (write-only) (scope: write)

anthropic: the sk-ant-oat… token from `claude setup-token`. openai: the contents of ~/.codex/auth.json from `codex login`. Eligible agents can then run at $0 model usage; with useForNewAgents (default on), new eligible agents use it automatically.

Path parameters:

- `provider` (string, required) — anthropic = Claude subscription · openai = ChatGPT plan

Body (application/json):

- `credential` (string, required) — max length 100000
- `label` (string) — max length 60

Responses:

- `200` — OK
  - `modelSubscription` (object, required) — A connected consumer AI subscription (Claude Pro/Max or ChatGPT plan) powering eligible agents at $0 model usage. The credential itself is write-only.
    - `provider` (enum<string>) — "anthropic" · "openai"
    - `label` (string)
    - `status` (enum<string>) — "connected" · "needs_reconnect"
    - `expiresAt` (string | null)
    - `lastUsedAt` (string | null)
    - `useForNewAgents` (boolean)
    - `createdAt` (string)
    - `agents` (object[]) — Agents in this universe running on the subscription
      - `id` (string)
      - `agentId` (string | null)
      - `name` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X PUT https://agentsky.dev/model-subscriptions/provider \
  -H "Authorization: Bearer ast_..."
```

### PATCH /model-subscriptions/{provider}

Update useForNewAgents / label (scope: write)

Path parameters:

- `provider` (string, required) — anthropic = Claude subscription · openai = ChatGPT plan

Body (application/json):

- `useForNewAgents` (boolean)
- `label` (string) — max length 60

Responses:

- `200` — OK
  - `modelSubscription` (object, required) — A connected consumer AI subscription (Claude Pro/Max or ChatGPT plan) powering eligible agents at $0 model usage. The credential itself is write-only.
    - `provider` (enum<string>) — "anthropic" · "openai"
    - `label` (string)
    - `status` (enum<string>) — "connected" · "needs_reconnect"
    - `expiresAt` (string | null)
    - `lastUsedAt` (string | null)
    - `useForNewAgents` (boolean)
    - `createdAt` (string)
    - `agents` (object[]) — Agents in this universe running on the subscription
      - `id` (string)
      - `agentId` (string | null)
      - `name` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X PATCH https://agentsky.dev/model-subscriptions/provider \
  -H "Authorization: Bearer ast_..."
```

### DELETE /model-subscriptions/{provider}

Disconnect — affected agents keep their pointer and fail until reconnected or switched (scope: write)

Path parameters:

- `provider` (string, required) — anthropic = Claude subscription · openai = ChatGPT plan

Responses:

- `200` — OK
  - `affectedAgents` (object[])
    - `id` (string)
    - `agentId` (string | null)
    - `name` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X DELETE https://agentsky.dev/model-subscriptions/provider \
  -H "Authorization: Bearer ast_..."
```

### POST /sessions/{id}/share

Turn sharing on and get the share link — idempotent; {"rotate": true} replaces it (scope: write)

The returned URL opens the session's guest page for anyone holding it, with no account; turns sent through it bill the session's owner. Rotating kills the old URL immediately.

Path parameters:

- `id` (string, required) — The session id

Body (application/json):

- `rotate` (boolean) — default: false

Responses:

- `200` — OK
  - `shareUrl` (string, required)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/sessions/sess_cmtb7q2j/share \
  -H "Authorization: Bearer ast_..."
```

### DELETE /sessions/{id}/share

Turn sharing off — the link stops working immediately (scope: write)

Path parameters:

- `id` (string, required) — The session id

Responses:

- `200` — OK
  - `ok` (true, required)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X DELETE https://agentsky.dev/sessions/sess_cmtb7q2j/share \
  -H "Authorization: Bearer ast_..."
```

### GET /webhooks

List webhook endpoints (scope: read)

Responses:

- `200` — OK
  - `webhooks` (object[], required)
    - `id` (string) — whep_… identifier
    - `url` (string)
    - `events` (string[])
    - `status` (enum<string>) — "active" · "disabled"
    - `failure_count` (integer)
    - `created_at` (string)
    - `secret` (string) — whsec_… — present ONLY in the create response
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/webhooks \
  -H "Authorization: Bearer ast_..."
```

### POST /webhooks

Register a webhook endpoint — the whsec_ secret appears only in this response (scope: write)

Body (application/json):

- `url` (string, required) — max length 2048
- `events` (string[], required)

Responses:

- `201` — Created
  - `webhook` (object, required) — Outbound webhook endpoint. Deliveries carry x-asteroids-event/-delivery/-timestamp/-signature (v1=hmac-sha256(secret, "<ts>.<body>")), retry 3× (0s/5s/30s), and sustained failure disables (PATCH status:active re-enables). Payloads are thin {id, type, created_at, data:{type, id}} — fetch the resource, dedupe on the event id.
    - `id` (string) — whep_… identifier
    - `url` (string)
    - `events` (string[])
    - `status` (enum<string>) — "active" · "disabled"
    - `failure_count` (integer)
    - `created_at` (string)
    - `secret` (string) — whsec_… — present ONLY in the create response
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/webhooks \
  -H "Authorization: Bearer ast_..." \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/agentsky","events":["routine_run.failed","routine.paused"]}'
```

### GET /webhooks/{id}

Webhook endpoint detail (scope: read)

Path parameters:

- `id` (string, required) — The webhook endpoint id (whep_…)

Responses:

- `200` — OK
  - `webhook` (object, required) — Outbound webhook endpoint. Deliveries carry x-asteroids-event/-delivery/-timestamp/-signature (v1=hmac-sha256(secret, "<ts>.<body>")), retry 3× (0s/5s/30s), and sustained failure disables (PATCH status:active re-enables). Payloads are thin {id, type, created_at, data:{type, id}} — fetch the resource, dedupe on the event id.
    - `id` (string) — whep_… identifier
    - `url` (string)
    - `events` (string[])
    - `status` (enum<string>) — "active" · "disabled"
    - `failure_count` (integer)
    - `created_at` (string)
    - `secret` (string) — whsec_… — present ONLY in the create response
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/webhooks/sess_cmtb7q2j \
  -H "Authorization: Bearer ast_..."
```

### PATCH /webhooks/{id}

Update url/events, or status:active to re-enable a disabled endpoint (scope: write)

Path parameters:

- `id` (string, required) — The webhook endpoint id (whep_…)

Body (application/json):

- `url` (string) — max length 2048
- `events` (string[])
- `status` (enum<string>) — "active" · "disabled"

Responses:

- `200` — OK
  - `webhook` (object, required) — Outbound webhook endpoint. Deliveries carry x-asteroids-event/-delivery/-timestamp/-signature (v1=hmac-sha256(secret, "<ts>.<body>")), retry 3× (0s/5s/30s), and sustained failure disables (PATCH status:active re-enables). Payloads are thin {id, type, created_at, data:{type, id}} — fetch the resource, dedupe on the event id.
    - `id` (string) — whep_… identifier
    - `url` (string)
    - `events` (string[])
    - `status` (enum<string>) — "active" · "disabled"
    - `failure_count` (integer)
    - `created_at` (string)
    - `secret` (string) — whsec_… — present ONLY in the create response
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X PATCH https://agentsky.dev/webhooks/sess_cmtb7q2j \
  -H "Authorization: Bearer ast_..."
```

### DELETE /webhooks/{id}

Delete a webhook endpoint (scope: write)

Path parameters:

- `id` (string, required) — The webhook endpoint id (whep_…)

Responses:

- `200` — OK
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X DELETE https://agentsky.dev/webhooks/sess_cmtb7q2j \
  -H "Authorization: Bearer ast_..."
```

### GET /channels/apps

List BYO channel apps — credentials masked (scope: read)

Responses:

- `200` — OK
  - `apps` (object[], required)
    - `id` (string)
    - `platform` (enum<string>) — "telegram" · "slack" · "discord" · "whatsapp" · "imessage"
    - `label` (string)
    - `status` (string)
    - `credential_keys` (string[]) — Key names only — values are write-only
    - `created_at` (string)
    - `setup` (object) — Steps the platform cannot automate (per-app webhook_url / verify_token / events_url / invite_url)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/channels/apps \
  -H "Authorization: Bearer ast_..."
```

### POST /channels/apps

Register your own bot/number as a channel app (scope: write)

Credentials are proven against the platform where an API exists, stored encrypted, and write-only from then on. The response's setup object carries the steps the platform cannot automate (webhook URLs to paste, invite links).

Body (application/json):

- `platform` (enum<string>, required) — "telegram" · "slack" · "discord" · "whatsapp" · "imessage"
- `label` (string, required) — max length 120
- `credentials` (object, required) — Write-only. Required keys per platform — telegram: bot_token · discord: bot_token · slack: bot_token, signing_secret · whatsapp: access_token, phone_number_id, app_secret · imessage (coming soon — answers 501 coming_soon today): api_key, phone_number, webhook_secret

Responses:

- `201` — Created
  - `id` (string)
  - `platform` (enum<string>) — "telegram" · "slack" · "discord" · "whatsapp" · "imessage"
  - `label` (string)
  - `status` (string)
  - `credential_keys` (string[]) — Key names only — values are write-only
  - `created_at` (string)
  - `setup` (object) — Steps the platform cannot automate (per-app webhook_url / verify_token / events_url / invite_url)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/channels/apps \
  -H "Authorization: Bearer ast_..."
```

### GET /channels/apps/{id}

Channel app detail — credentials masked (scope: read)

Path parameters:

- `id` (string, required) — The channel app id

Responses:

- `200` — OK
  - `id` (string)
  - `platform` (enum<string>) — "telegram" · "slack" · "discord" · "whatsapp" · "imessage"
  - `label` (string)
  - `status` (string)
  - `credential_keys` (string[]) — Key names only — values are write-only
  - `created_at` (string)
  - `setup` (object) — Steps the platform cannot automate (per-app webhook_url / verify_token / events_url / invite_url)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/channels/apps/sess_cmtb7q2j \
  -H "Authorization: Bearer ast_..."
```

### PATCH /channels/apps/{id}

Rename or rotate credentials (rotation re-runs registration) (scope: write)

Path parameters:

- `id` (string, required) — The channel app id

Body (application/json):

- `label` (string) — max length 120
- `credentials` (object) — Full replacement bag; rotation re-runs the platform registration

Responses:

- `200` — OK
  - `id` (string)
  - `platform` (enum<string>) — "telegram" · "slack" · "discord" · "whatsapp" · "imessage"
  - `label` (string)
  - `status` (string)
  - `credential_keys` (string[]) — Key names only — values are write-only
  - `created_at` (string)
  - `setup` (object) — Steps the platform cannot automate (per-app webhook_url / verify_token / events_url / invite_url)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X PATCH https://agentsky.dev/channels/apps/sess_cmtb7q2j \
  -H "Authorization: Bearer ast_..."
```

### DELETE /channels/apps/{id}

Delete the channel app (scope: write)

Path parameters:

- `id` (string, required) — The channel app id

Responses:

- `204` — Deleted
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X DELETE https://agentsky.dev/channels/apps/sess_cmtb7q2j \
  -H "Authorization: Bearer ast_..."
```

### GET /channels/connections

List connections with their bound session (scope: read)

Responses:

- `200` — OK
  - `connections` (object[], required)
    - `id` (string)
    - `platform` (string)
    - `channel_id` (string)
    - `label` (string)
    - `status` (enum<string>) — "PENDING" · "CONNECTED" · "DISCONNECTED"
    - `bound_session` (object)
      - `id` (string)
      - `label` (string)
    - `created_at` (string)
    - `connect` (object) — PENDING only: the end-user ceremony (url, and for code flows phone + code)
      - `url` (string)
      - `phone` (string)
      - `code` (string)
      - `expires_at` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/channels/connections \
  -H "Authorization: Bearer ast_..."
```

### POST /channels/connections

Connect a surface — link-flow platforms return a pending connect ceremony (scope: write)

telegram/imessage/whatsapp mint a connect link (deep link + code) the end user claims in-platform; slack/discord create the channel synchronously under the app identity. Pass app for BYO; callback_url (link flows) redirects the hosted connect page to you after the claim.

Body (application/json):

- `platform` (enum<string>, required) — "telegram" · "slack" · "discord" · "whatsapp" · "imessage" · "loopback"
- `label` (string) — max length 120
- `metadata` (object) — Echoed on channel.connected and the callback redirect
- `destination` (object) — Pre-bind: the claimed surface talks to this session
  - `session` (string)
- `app` (string) — BYO ChannelApp id — the connect ceremony runs on your bot/number
- `invite_user` (string) — Slack only: user id to invite into the created channel
- `callback_url` (string) — Link-flow platforms only: the hosted connect page redirects here after the claim with connection_id, status, and metadata query params

Responses:

- `201` — Created
  - `id` (string)
  - `platform` (string)
  - `channel_id` (string)
  - `label` (string)
  - `status` (enum<string>) — "PENDING" · "CONNECTED" · "DISCONNECTED"
  - `bound_session` (object)
    - `id` (string)
    - `label` (string)
  - `created_at` (string)
  - `connect` (object) — PENDING only: the end-user ceremony (url, and for code flows phone + code)
    - `url` (string)
    - `phone` (string)
    - `code` (string)
    - `expires_at` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/channels/connections \
  -H "Authorization: Bearer ast_..."
```

### DELETE /channels/connections/{id}

Disconnect the surface (scope: write)

Path parameters:

- `id` (string, required) — The connection id

Responses:

- `204` — Disconnected
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X DELETE https://agentsky.dev/channels/connections/sess_cmtb7q2j \
  -H "Authorization: Bearer ast_..."
```

### PUT /channels/connections/{id}/binding

Set where the surface routes — a session or your webhook endpoint (single active binding) (scope: write)

Path parameters:

- `id` (string, required) — The connection id

Body (application/json):

- `destination` (object, required) — Exactly one of webhook (endpoint id) or session (session id)
  - `webhook` (string)
  - `session` (string)

Responses:

- `200` — OK
  - `connection_id` (string)
  - `binding_id` (string)
  - `session` (string)
  - `webhook` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X PUT https://agentsky.dev/channels/connections/sess_cmtb7q2j/binding \
  -H "Authorization: Bearer ast_..."
```

### GET /channels/connections/{id}/capabilities

Declared platform capabilities — branch on these instead of guessing (scope: read)

Path parameters:

- `id` (string, required) — The connection id

Responses:

- `200` — OK
  - `connection_id` (string)
  - `platform` (string)
  - `capabilities` (object) — threads · reactions · markers · proactive · markdown · modals · ephemeral · streaming
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/channels/connections/sess_cmtb7q2j/capabilities \
  -H "Authorization: Bearer ast_..."
```

### GET /channels/bindings

List bindings, optionally by connection (scope: read)

Query parameters:

- `connection_id` (string)

Responses:

- `200` — OK
  - `bindings` (object[], required)
    - `id` (string)
    - `connection_id` (string)
    - `thread_id` (string)
    - `destination` (object) — Exactly one of webhook (endpoint id) or session (session id)
      - `webhook` (string)
      - `session` (string)
    - `is_default` (boolean)
    - `status` (string)
    - `route_key` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/channels/bindings?connection_id= \
  -H "Authorization: Bearer ast_..."
```

### POST /channels/bindings

Route one thread of a connection to a session or your webhook (scope: write)

Body (application/json):

- `connection_id` (string, required)
- `thread_id` (string, required) — The one thread this rule routes. Connection-level routing is a single slot — set it via PUT /connections/{id}/binding
- `destination` (object, required) — Exactly one of webhook (endpoint id) or session (session id)
  - `webhook` (string)
  - `session` (string)
- `route_key` (string) — max length 60

Responses:

- `201` — Created
  - `id` (string)
  - `connection_id` (string)
  - `thread_id` (string)
  - `destination` (object) — Exactly one of webhook (endpoint id) or session (session id)
    - `webhook` (string)
    - `session` (string)
  - `is_default` (boolean)
  - `status` (string)
  - `route_key` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/channels/bindings \
  -H "Authorization: Bearer ast_..."
```

### DELETE /channels/bindings/{id}

Delete the binding (scope: write)

Path parameters:

- `id` (string, required) — The binding id

Responses:

- `204` — Deleted
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X DELETE https://agentsky.dev/channels/bindings/sess_cmtb7q2j \
  -H "Authorization: Bearer ast_..."
```

### GET /channels/webhooks

List webhook endpoints (scope: read)

Responses:

- `200` — OK
  - `webhooks` (object[], required)
    - `id` (string)
    - `url` (string)
    - `events` (string[])
    - `status` (string)
    - `secret` (string) — whsec_ HMAC secret — returned on create only
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/channels/webhooks \
  -H "Authorization: Bearer ast_..."
```

### POST /channels/webhooks

Create a webhook endpoint — HMAC-signed deliveries, 0/5/30s retries (scope: write)

Deliveries carry X-Asteroids-{Event,Delivery,Timestamp,Signature}; ~20 consecutive failures disable the endpoint. Consumers must be idempotent by delivery id.

Body (application/json):

- `url` (string, required) — https (http allowed for localhost only)
- `events` (string[]) — Default ["message.received"]

Responses:

- `201` — Created
  - `id` (string)
  - `url` (string)
  - `events` (string[])
  - `status` (string)
  - `secret` (string) — whsec_ HMAC secret — returned on create only
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/channels/webhooks \
  -H "Authorization: Bearer ast_..."
```

### PATCH /channels/webhooks/{id}

Re-enable a disabled endpoint (resets failures, keeps the secret) and/or re-pick events (scope: write)

Path parameters:

- `id` (string, required) — The webhook endpoint id

Body (application/json):

- `status` (enum<string>) — "ACTIVE"
- `events` (string[])

Responses:

- `200` — OK
  - `id` (string)
  - `url` (string)
  - `events` (string[])
  - `status` (string)
  - `secret` (string) — whsec_ HMAC secret — returned on create only
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X PATCH https://agentsky.dev/channels/webhooks/sess_cmtb7q2j \
  -H "Authorization: Bearer ast_..."
```

### DELETE /channels/webhooks/{id}

Delete the webhook endpoint (scope: write)

Path parameters:

- `id` (string, required) — The webhook endpoint id

Responses:

- `204` — Deleted
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X DELETE https://agentsky.dev/channels/webhooks/sess_cmtb7q2j \
  -H "Authorization: Bearer ast_..."
```

### PUT /channels/threads/{id}/messages/{mid}/reactions/{emoji}

Add a reaction (platform-neutral emoji names) (scope: write)

Path parameters:

- `id` (string, required) — The thread id
- `mid` (string, required)
- `emoji` (string, required)

Responses:

- `204` — Reacted
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X PUT https://agentsky.dev/channels/threads/sess_cmtb7q2j/messages/mid/reactions/emoji \
  -H "Authorization: Bearer ast_..."
```

### DELETE /channels/threads/{id}/messages/{mid}/reactions/{emoji}

Remove a reaction (scope: write)

Path parameters:

- `id` (string, required) — The thread id
- `mid` (string, required)
- `emoji` (string, required)

Responses:

- `204` — Removed
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X DELETE https://agentsky.dev/channels/threads/sess_cmtb7q2j/messages/mid/reactions/emoji \
  -H "Authorization: Bearer ast_..."
```

### POST /channels/deliveries

Proactive fan-out: post to every thread bound to a destination (scope: write)

Body (application/json):

- `destination` (object, required) — Exactly one of webhook (endpoint id) or session (session id)
  - `webhook` (string)
  - `session` (string)
- `parts` (object[], required)
  - type: "text" (object)
    - `type` ("text", required)
    - `text` (string, required)
  - type: "markdown" (object)
    - `type` ("markdown", required)
    - `text` (string, required)
  - type: "raw" (object)
    - `type` ("raw", required)
    - `platform` (string, required)
    - `payload` (string, required) — Platform-native payload, JSON-encoded
- `display_name` (string) — max length 80

Responses:

- `200` — OK — per-thread results
  - `results` (object[])
    - `thread_id` (string)
    - `status` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/channels/deliveries \
  -H "Authorization: Bearer ast_..."
```

## Stream events

Send and listen are decoupled: POST `…/events`, then read `…/stream` until `session.status_idle` — there is no non-streaming reply mode. Frames are `id:` + `event:` + one `data:` JSON object `{ id, type, sessionId, agent, at, …payload }` (the `id` is the dedupe key against `GET …/events`):

| event | payload | meaning |
|-------|---------|---------|
| `user.message` | content | history only (`GET …/events`) and the POST echo — user messages are not sent on the live stream; `content` is the CMA block list you sent, every image/document source rewritten to a `file` source |
| `system.message` | content | history only and the POST echo — the per-turn system text sent alongside a user.message |
| `user.interrupt` | by | an interrupt was accepted — echoed on the live stream and kept in history; the pod's `session.status_idle` follows |
| `agent.message` | parts, text | a user-facing agent post; a turn may carry several |
| `agent.reasoning` | part | raw engine event: thinking (live only) |
| `agent.tool_use` | part | raw engine event: a tool invocation (live only) |
| `agent.tool_result` | part | raw engine event: the tool's outcome (live only) |
| `agent.status` | part | raw engine event: working / waiting / … (live only) |
| `session.status_running` | — | a turn started |
| `session.status_idle` | stop_reason | the turn is over (`stop_reason.type` is `end_turn` — also after an interrupt — `budget_reached`, or `retries_exhausted` after an exhausted `session.error`); never break on bare idle; the stream stays open |
| `session.error` | error | a failure inside the turn (`error.type`, `error.message`, `error.retry_status` `retrying` or `exhausted`); the stream stays open and a `session.status_idle` follows |
| `session.status_terminated` | — | the session was archived; terminal — nothing follows; close the stream |
| `session.deleted` | — | terminal — nothing follows; close the stream |

One turn on the standing stream:

```
id: msg-77e0c4…
event: agent.message
data: {"id":"msg-77e0c4…","type":"agent.message","sessionId":"sess-8c41f0…","agent":"nemesis-ee87","at":"…","text":"Signups today: 42, up 12%.","parts":[…]}

id: turn_9d52a1…#idle
event: session.status_idle
data: {"id":"turn_9d52a1…#idle","type":"session.status_idle","sessionId":"sess-8c41f0…","agent":"nemesis-ee87","at":"…","stop_reason":{"type":"end_turn"}}
```
