# API reference — one origin, one namespace

Everything lives under `https://agentsky.dev/api/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://agentsky.dev/api/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` | duplicate slug |
| 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 /api/v1/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/api/v1/whoami \
  -H "Authorization: Bearer ast_..."
```

## Universes

### GET /api/v1/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/api/v1/universes \
  -H "Authorization: Bearer ast_..."
```

### POST /api/v1/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/api/v1/universes \
  -H "Authorization: Bearer ast_..." \
  -H "Content-Type: application/json" \
  -d '{"slug":"acme","name":"Acme"}'
```

### GET /api/v1/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/api/v1/universes/my-agent \
  -H "Authorization: Bearer ast_..."
```

## Agents

### GET /api/v1/agents

List agents in the resolved universe (scope: read)

Responses:

- `200` — OK
  - `agents` (object[], required)
    - `slug` (string) — Globally unique handle
    - `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
    - `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/api/v1/agents \
  -H "Authorization: Bearer ast_..."
```

### POST /api/v1/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. One agent can run many sessions; nothing is provisioned until you start one with POST /api/v1/sessions.

Body (application/json):

- `name` (string) — 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) — 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[])
  - `name` (string, required) — pattern ^[A-Za-z0-9_-]+$
  - `url` (string, required)
  - `source` (enum<string>, required) — "registry" · "git" · "package" · "archive"
  - `version` (string, required)
  - `config` (object) — default: {}
  - `secretRefs` (string[]) — default: []
- `customInstalls` (object[])
  - `command` (string, required)
  - `description` (string)
  - `timeoutSeconds` (integer) — default: 300
  - `allowNetwork` (boolean) — default: true
- `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.
    - `slug` (string)
    - `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` (string[])
    - `metadata` (object) — Your key-merged client metadata
    - `defaultSessionId` (string | null) — The session conversation verbs target when addressed by agent handle
    - `universe` (string) — Resolved universe slug
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

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

### GET /api/v1/agents/{slug}

Agent detail (scope: read)

Path parameters:

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

Responses:

- `200` — OK
  - `agent` (object, required) — Everything in the list item, plus configuration state.
    - `slug` (string)
    - `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` (string[])
    - `metadata` (object) — Your key-merged client metadata
    - `defaultSessionId` (string | null) — The session conversation verbs target when addressed by agent handle
    - `universe` (string) — Resolved universe slug
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

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

### PATCH /api/v1/agents/{slug}

Update displayName / capabilities / metadata (scope: write)

Omitted fields preserved; capabilities replaced whole; metadata key-merged (null deletes a key); optional expectedVersion → 409 version_conflict on mismatch.

Path parameters:

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

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)
- `expectedVersion` (integer)

Responses:

- `200` — OK
  - `agent` (object, required) — Everything in the list item, plus configuration state.
    - `slug` (string)
    - `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` (string[])
    - `metadata` (object) — Your key-merged client metadata
    - `defaultSessionId` (string | null) — The session conversation verbs target when addressed by agent handle
    - `universe` (string) — Resolved universe slug
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

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

### DELETE /api/v1/agents/{slug}

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

Path parameters:

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

Responses:

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

Example request:

```bash
curl -X DELETE https://agentsky.dev/api/v1/agents/my-agent \
  -H "Authorization: Bearer ast_..."
```

### POST /api/v1/agents/{slug}/archive

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

Path parameters:

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

Responses:

- `200` — OK
  - `agent` (object, required) — Everything in the list item, plus configuration state.
    - `slug` (string)
    - `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` (string[])
    - `metadata` (object) — Your key-merged client metadata
    - `defaultSessionId` (string | null) — The session conversation verbs target when addressed by agent handle
    - `universe` (string) — Resolved universe slug
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

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

### PUT /api/v1/agents/{slug}/prompt

Save + hot-reload the user prompt layer (scope: write)

Path parameters:

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

Body (application/json):

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

Responses:

- `200` — OK
  - `version` (integer)
  - `unchanged` (boolean) — Content identical to the active version
  - `applied` (enum<string>) — Hot-reload outcome across the agent's sessions — "live" · "on-restart" · "unreachable"
  - `sessions` (object[])
    - `sessionId` (string)
    - `status` (enum<string>) — "live" · "on-restart" · "unreachable"
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

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

### GET /api/v1/agents/{slug}/prompt/versions

Prompt version history (scope: read)

Path parameters:

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

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/api/v1/agents/my-agent/prompt/versions \
  -H "Authorization: Bearer ast_..."
```

### GET /api/v1/agents/{slug}/secrets

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

Path parameters:

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

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/api/v1/agents/my-agent/secrets \
  -H "Authorization: Bearer ast_..."
```

### PUT /api/v1/agents/{slug}/secrets/{key}

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

Path parameters:

- `slug` (string, required) — The agent slug
- `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/api/v1/agents/my-agent/secrets/NOTION_TOKEN \
  -H "Authorization: Bearer ast_..." \
  -H "Content-Type: application/json" \
  -d '{"value":"secret_…"}'
```

### DELETE /api/v1/agents/{slug}/secrets/{key}

Unset a secret (scope: write)

Path parameters:

- `slug` (string, required) — The agent slug
- `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/api/v1/agents/my-agent/secrets/NOTION_TOKEN \
  -H "Authorization: Bearer ast_..."
```

## Sessions

### GET /api/v1/sessions

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

Query parameters:

- `agent` (string) — Agent slug or id

Responses:

- `200` — OK
  - `sessions` (object[], required)
    - `id` (string) — sess-… identifier
    - `agent` (string | null) — Agent slug
    - `agentId` (string | null)
    - `title` (string | null)
    - `status` (enum<string>) — "provisioning" · "idle" · "running" · "terminated"
    - `agentType` (string)
    - `llm` (string)
    - `default` (boolean) — Is this the agent's default session
    - `metadata` (object)
    - `createdAt` (string) — ISO 8601
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

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

### POST /api/v1/sessions

Create a session — cheap and lazy; the pod materializes on the first turn (scope: write)

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)
- `eager` (boolean)
- `title` (string) — max length 120
- `metadata` (object)
- `instructions` (object[])
  - `name` (string, required) — pattern .*\.md$
  - `content` (string) — default: ""
- `initial_events` (object[])
  - `type` ("user.message", required)
  - `parts` (object[], required)
    - type: "text" (object)
      - `index` (integer, required)
      - `type` ("text", required)
      - `text` (string) — default: ""
    - type: "reasoning" (object)
      - `index` (integer, required)
      - `type` ("reasoning", required)
      - `text` (string) — default: ""
      - `redacted` (boolean) — default: false
    - type: "tool_call" (object)
      - `index` (integer, required)
      - `type` ("tool_call", required)
      - `call_id` (string, required)
      - `tool_name` (string, required)
      - `args` (object) — default: {}
      - `args_partial` (object)
        - option 1 (object)
        - option 2 (object)
    - 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` (object)
        - option 1 (object)
        - option 2 (object)
      - `data` (object)
        - option 1 (object)
        - option 2 (object)
      - `size_bytes` (object)
        - option 1 (object)
        - option 2 (object)
    - type: "image" (object)
      - `index` (integer, required)
      - `type` ("image", required)
      - `media_type` (string, required)
      - `uri` (object)
        - option 1 (object)
        - option 2 (object)
      - `data` (object)
        - option 1 (object)
        - option 2 (object)
      - `alt` (object)
        - option 1 (object)
        - option 2 (object)
      - `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` (object)
        - option 1 (object)
        - option 2 (object)
      - `data` (object)
        - option 1 (object)
        - option 2 (object)
      - `alt` (object)
        - option 1 (object)
        - option 2 (object)
      - `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` (object)
        - option 1 (object)
        - option 2 (object)
    - type: "status" (object)
      - `index` (integer, required)
      - `type` ("status", required)
      - `level` (enum<string>, required) — "thinking" · "working" · "waiting" · "idle" · "done"
      - `text` (object)
        - option 1 (object)
        - option 2 (object)
    - type: "error" (object)
      - `index` (integer, required)
      - `type` ("error", required)
      - `code` (string, required)
      - `message` (string, required)
      - `retryable` (boolean) — default: false
- `vcpus` (integer)
- `memoryMb` (integer)
- `modelBilling` (enum<string>) — "platform" · "account"

Responses:

- `201` — Created
  - `session` (object, required)
    - `id` (string) — sess-… identifier
    - `agent` (string | null) — Agent slug
    - `agentId` (string | null)
    - `title` (string | null)
    - `status` (enum<string>) — "provisioning" · "idle" · "running" · "terminated"
    - `agentType` (string)
    - `llm` (string)
    - `default` (boolean) — Is this the agent's default session
    - `metadata` (object)
    - `createdAt` (string) — ISO 8601
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/api/v1/sessions \
  -H "Authorization: Bearer ast_..." \
  -H "Content-Type: application/json" \
  -d '{"agent":"research-agent","title":"Signups digest","initial_events":[{"type":"user.message","parts":[{"type":"text","index":0,"text":"Summarize today's signups"}]}]}'
```

Example response (201 Created):

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

### GET /api/v1/sessions/{id}

Session detail — status is provisioning | idle | running | terminated (scope: read)

Path parameters:

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

Responses:

- `200` — OK
  - `session` (object, required)
    - `id` (string) — sess-… identifier
    - `agent` (string | null) — Agent slug
    - `agentId` (string | null)
    - `title` (string | null)
    - `status` (enum<string>) — "provisioning" · "idle" · "running" · "terminated"
    - `agentType` (string)
    - `llm` (string)
    - `default` (boolean) — Is this the agent's default session
    - `metadata` (object)
    - `createdAt` (string) — ISO 8601
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl https://agentsky.dev/api/v1/sessions/sess-8c41f0 \
  -H "Authorization: Bearer ast_..."
```

### PATCH /api/v1/sessions/{id}

Update title / metadata (scope: write)

Path parameters:

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

Body (application/json):

- `title` (object)
  - option 1 (object)
  - option 2 (object)
- `metadata` (object)
- `modelBilling` (enum<string>) — "platform" · "account"

Responses:

- `200` — OK
  - `session` (object, required)
    - `id` (string) — sess-… identifier
    - `agent` (string | null) — Agent slug
    - `agentId` (string | null)
    - `title` (string | null)
    - `status` (enum<string>) — "provisioning" · "idle" · "running" · "terminated"
    - `agentType` (string)
    - `llm` (string)
    - `default` (boolean) — Is this the agent's default session
    - `metadata` (object)
    - `createdAt` (string) — ISO 8601
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X PATCH https://agentsky.dev/api/v1/sessions/sess-8c41f0 \
  -H "Authorization: Bearer ast_..." \
  -H "Content-Type: application/json" \
  -d '{"title":"Signups digest","metadata":{"team":"growth"}}'
```

### DELETE /api/v1/sessions/{id}

End the session — emits session.deleted, tears down the pod (scope: admin)

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/api/v1/sessions/sess-8c41f0 \
  -H "Authorization: Bearer ast_..."
```

### GET /api/v1/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, agent.message, and turn.status_idle (turn boundaries; stop_reason.type is end_turn or interrupted). agent.reasoning, agent.tool_use, agent.tool_result, agent.status and error 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 (history only — not sent on the live stream) · agent.message · agent.reasoning · agent.tool_use · agent.tool_result · agent.status · turn.status_idle · turn.interrupted · session.deleted · error
    - `sessionId` (string, required)
    - `agent` (string | null) — Agent slug
    - `at` (string) — ISO 8601
    - `messageId` (string) — message events
    - `parts` (object[]) — message events
      - type: "text" (object)
        - `index` (integer, required)
        - `type` ("text", required)
        - `text` (string) — default: ""
      - type: "reasoning" (object)
        - `index` (integer, required)
        - `type` ("reasoning", required)
        - `text` (string) — default: ""
        - `redacted` (boolean) — default: false
      - type: "tool_call" (object)
        - `index` (integer, required)
        - `type` ("tool_call", required)
        - `call_id` (string, required)
        - `tool_name` (string, required)
        - `args` (object) — default: {}
        - `args_partial` (object)
          - option 1 (object)
          - option 2 (object)
      - 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` (object)
          - option 1 (object)
          - option 2 (object)
        - `data` (object)
          - option 1 (object)
          - option 2 (object)
        - `size_bytes` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "image" (object)
        - `index` (integer, required)
        - `type` ("image", required)
        - `media_type` (string, required)
        - `uri` (object)
          - option 1 (object)
          - option 2 (object)
        - `data` (object)
          - option 1 (object)
          - option 2 (object)
        - `alt` (object)
          - option 1 (object)
          - option 2 (object)
        - `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` (object)
          - option 1 (object)
          - option 2 (object)
        - `data` (object)
          - option 1 (object)
          - option 2 (object)
        - `alt` (object)
          - option 1 (object)
          - option 2 (object)
        - `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` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "status" (object)
        - `index` (integer, required)
        - `type` ("status", required)
        - `level` (enum<string>, required) — "thinking" · "working" · "waiting" · "idle" · "done"
        - `text` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "error" (object)
        - `index` (integer, required)
        - `type` ("error", required)
        - `code` (string, required)
        - `message` (string, required)
        - `retryable` (boolean) — default: false
    - `text` (string) — message events — parts flattened to plain text
    - `part` (object) — reasoning / tool_use / tool_result / status events
      - type: "text" (object)
        - `index` (integer, required)
        - `type` ("text", required)
        - `text` (string) — default: ""
      - type: "reasoning" (object)
        - `index` (integer, required)
        - `type` ("reasoning", required)
        - `text` (string) — default: ""
        - `redacted` (boolean) — default: false
      - type: "tool_call" (object)
        - `index` (integer, required)
        - `type` ("tool_call", required)
        - `call_id` (string, required)
        - `tool_name` (string, required)
        - `args` (object) — default: {}
        - `args_partial` (object)
          - option 1 (object)
          - option 2 (object)
      - 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` (object)
          - option 1 (object)
          - option 2 (object)
        - `data` (object)
          - option 1 (object)
          - option 2 (object)
        - `size_bytes` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "image" (object)
        - `index` (integer, required)
        - `type` ("image", required)
        - `media_type` (string, required)
        - `uri` (object)
          - option 1 (object)
          - option 2 (object)
        - `data` (object)
          - option 1 (object)
          - option 2 (object)
        - `alt` (object)
          - option 1 (object)
          - option 2 (object)
        - `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` (object)
          - option 1 (object)
          - option 2 (object)
        - `data` (object)
          - option 1 (object)
          - option 2 (object)
        - `alt` (object)
          - option 1 (object)
          - option 2 (object)
        - `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` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "status" (object)
        - `index` (integer, required)
        - `type` ("status", required)
        - `level` (enum<string>, required) — "thinking" · "working" · "waiting" · "idle" · "done"
        - `text` (object)
          - option 1 (object)
          - option 2 (object)
      - type: "error" (object)
        - `index` (integer, required)
        - `type` ("error", required)
        - `code` (string, required)
        - `message` (string, required)
        - `retryable` (boolean) — default: false
    - `stop_reason` (object) — turn.status_idle — discriminate on type; never break on bare idle
      - `type` (string) — "end_turn" | "interrupted"; more reserved
  - `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/api/v1/sessions/sess-8c41f0/events?cursor=&limit=&types[]= \
  -H "Authorization: Bearer ast_..."
```

Example response (200 OK):

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

## Conversation

### GET /api/v1/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)
      - `slug` (string | null)
      - `name` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

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

### PUT /api/v1/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)
      - `slug` (string | null)
      - `name` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

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

### PATCH /api/v1/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)
      - `slug` (string | null)
      - `name` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

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

### DELETE /api/v1/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)
    - `slug` (string | null)
    - `name` (string)
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

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

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

Send a turn — bare 202; output arrives on the stream (scope: write)

The conversation plane (edge-routed to the chat-proxy). Optional Idempotency-Key header dedupes retries. Turn boundaries are read from the stream's status events.

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: ""
  - type: "reasoning" (object)
    - `index` (integer, required)
    - `type` ("reasoning", required)
    - `text` (string) — default: ""
    - `redacted` (boolean) — default: false
  - type: "tool_call" (object)
    - `index` (integer, required)
    - `type` ("tool_call", required)
    - `call_id` (string, required)
    - `tool_name` (string, required)
    - `args` (object) — default: {}
    - `args_partial` (object)
      - option 1 (object)
      - option 2 (object)
  - 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` (object)
      - option 1 (object)
      - option 2 (object)
    - `data` (object)
      - option 1 (object)
      - option 2 (object)
    - `size_bytes` (object)
      - option 1 (object)
      - option 2 (object)
  - type: "image" (object)
    - `index` (integer, required)
    - `type` ("image", required)
    - `media_type` (string, required)
    - `uri` (object)
      - option 1 (object)
      - option 2 (object)
    - `data` (object)
      - option 1 (object)
      - option 2 (object)
    - `alt` (object)
      - option 1 (object)
      - option 2 (object)
    - `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` (object)
      - option 1 (object)
      - option 2 (object)
    - `data` (object)
      - option 1 (object)
      - option 2 (object)
    - `alt` (object)
      - option 1 (object)
      - option 2 (object)
    - `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` (object)
      - option 1 (object)
      - option 2 (object)
  - type: "status" (object)
    - `index` (integer, required)
    - `type` ("status", required)
    - `level` (enum<string>, required) — "thinking" · "working" · "waiting" · "idle" · "done"
    - `text` (object)
      - option 1 (object)
      - option 2 (object)
  - 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/api/v1/sessions/sess-8c41f0/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
{}
```

### GET /api/v1/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, turn.status_idle (carries stop_reason — never break on bare idle), turn.interrupted, session.deleted (terminal), error. 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/api/v1/sessions/sess-8c41f0/stream \
  -H "Authorization: Bearer ast_..."
```

### POST /api/v1/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; turn.interrupted lands on the stream otherwise — "interrupting" · "no_turn"
- `4xx` — `{ "error": { "code", "message" } }` (see the error table)

Example request:

```bash
curl -X POST https://agentsky.dev/api/v1/sessions/sess-8c41f0/interrupt \
  -H "Authorization: Bearer ast_..."
```

Example response (200 OK):

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

### GET /api/v1/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/api/v1/channels/apps \
  -H "Authorization: Bearer ast_..."
```

### POST /api/v1/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/api/v1/channels/apps \
  -H "Authorization: Bearer ast_..."
```

### GET /api/v1/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/api/v1/channels/apps/sess-8c41f0 \
  -H "Authorization: Bearer ast_..."
```

### PATCH /api/v1/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/api/v1/channels/apps/sess-8c41f0 \
  -H "Authorization: Bearer ast_..."
```

### DELETE /api/v1/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/api/v1/channels/apps/sess-8c41f0 \
  -H "Authorization: Bearer ast_..."
```

### GET /api/v1/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/api/v1/channels/connections \
  -H "Authorization: Bearer ast_..."
```

### POST /api/v1/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/api/v1/channels/connections \
  -H "Authorization: Bearer ast_..."
```

### DELETE /api/v1/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/api/v1/channels/connections/sess-8c41f0 \
  -H "Authorization: Bearer ast_..."
```

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

Re-point the surface at a different session (single active binding) (scope: write)

Path parameters:

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

Body (application/json):

- `destination` (object, required)
  - `session` (string, required)

Responses:

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

Example request:

```bash
curl -X PUT https://agentsky.dev/api/v1/channels/connections/sess-8c41f0/binding \
  -H "Authorization: Bearer ast_..."
```

### GET /api/v1/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/api/v1/channels/connections/sess-8c41f0/capabilities \
  -H "Authorization: Bearer ast_..."
```

### GET /api/v1/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/api/v1/channels/bindings?connection_id= \
  -H "Authorization: Bearer ast_..."
```

### POST /api/v1/channels/bindings

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

Body (application/json):

- `connection_id` (string, required)
- `thread_id` (string) — Narrows the scope from the connection to one thread
- `destination` (object, required) — Exactly one of webhook (endpoint id) or session (session id)
  - `webhook` (string)
  - `session` (string)
- `is_default` (boolean)
- `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/api/v1/channels/bindings \
  -H "Authorization: Bearer ast_..."
```

### DELETE /api/v1/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/api/v1/channels/bindings/sess-8c41f0 \
  -H "Authorization: Bearer ast_..."
```

### GET /api/v1/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/api/v1/channels/webhooks \
  -H "Authorization: Bearer ast_..."
```

### POST /api/v1/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/api/v1/channels/webhooks \
  -H "Authorization: Bearer ast_..."
```

### PATCH /api/v1/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/api/v1/channels/webhooks/sess-8c41f0 \
  -H "Authorization: Bearer ast_..."
```

### DELETE /api/v1/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/api/v1/channels/webhooks/sess-8c41f0 \
  -H "Authorization: Bearer ast_..."
```

### POST /api/v1/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/api/v1/channels/threads/sess-8c41f0/messages \
  -H "Authorization: Bearer ast_..."
```

### PUT /api/v1/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/api/v1/channels/threads/sess-8c41f0/messages/mid/reactions/emoji \
  -H "Authorization: Bearer ast_..."
```

### DELETE /api/v1/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/api/v1/channels/threads/sess-8c41f0/messages/mid/reactions/emoji \
  -H "Authorization: Bearer ast_..."
```

### POST /api/v1/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/api/v1/channels/deliveries \
  -H "Authorization: Bearer ast_..."
```

### GET /api/v1/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/api/v1/channels/events/stream \
  -H "Authorization: Bearer ast_..."
```

## Stream events

Send and listen are decoupled: POST `…/messages`, then read `…/stream` until `turn.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` | parts, text, channel | history only (`GET …/events`) — user messages are not sent on the live stream |
| `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) |
| `turn.status_idle` | stop_reason | turn complete (`stop_reason.type` is `end_turn` or `interrupted` — never break on bare idle); the stream stays open |
| `turn.interrupted` | by | an interrupt took effect (live only; history records it as `turn.status_idle` with `stop_reason.type: "interrupted"`) |
| `session.deleted` | — | terminal — nothing follows; close the stream |
| `error` | code, message, retryable | the turn failed (live only); the stream stays open |

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: turn.status_idle
data: {"id":"turn_9d52a1…#idle","type":"turn.status_idle","sessionId":"sess-8c41f0…","agent":"nemesis-ee87","at":"…","stop_reason":{"type":"end_turn"}}
```
