---
source: https://threa.io/developers/reference
notes: Markdown mirror generated from the page above. YOUR_WORKSPACE_ID is the ws_… id in the app URL after /w/; YOUR_API_KEY is a key from Settings > API keys.
---

# API reference.

Every endpoint, generated from the [OpenAPI spec](https://threa.io/openapi.json) we ship. Set your workspace ID and key in the Credentials panel (top right) and the read-only examples run against your own workspace as you read.

The base is `https://app.threa.io`, under the stable `/api/v1` prefix and scoped to a workspace: `/api/v1/workspaces/{workspaceId}/…`. The `v1` segment does not move as the API evolves; versions are dated and selected with the `Threa-Version` header, described in [Versioning](https://threa.io/developers/versioning.md). Every request authenticates with `Authorization: Bearer <key>`. Error shapes, paging, idempotency, and rate limits live in [Operations](https://threa.io/developers/operations.md).

Version

[2026-07-24 latest](https://threa.io/developers/reference.md) [2026-07-22](https://threa.io/developers/reference/2026-07-22) [2026-07-12](https://threa.io/developers/reference/2026-07-12)

## Identity

Confirm who a key belongs to and list the bots you own.

### GET /api/v1/workspaces/{workspaceId}/me

**Get the authenticated principal**

Returns a discriminated union describing the authenticated principal: the API-key owner (\`kind: "user"\`) or the bot whose key is in use (\`kind: "bot"\`). Also reports the key's API version pin, the version this request resolved to, and the supported versions. Used by clients (e.g. the OpenClaw channel plugin) to verify their key and discover their identity after pairing.

Required scope: none — any valid key

#### Response 200

- `data` (object, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource

*GET /me:*

```bash
curl "https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/me" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

*Response 200 · example:*

```json
{
  "data": {
    "kind": "user",
    "workspaceId": "ws_01jd2q4z8kxw9v7r3m5t8n",
    "userId": "usr_01jd2q4z8kxw9v7r3m5t8n",
    "apiVersion": {
      "pinned": "…",
      "resolved": "…",
      "current": "…",
      "supported": [
        "…"
      ]
    }
  }
}
```

### GET /api/v1/workspaces/{workspaceId}/me/bots

**List my personal bots**

For user-scoped keys: lists the authenticated user's personal bots, optionally filtered by trait. Used by the frontend to enumerate quick-switcher commands. Bot-scoped keys receive 403.

Required scope: none — any valid key

#### Query parameters

- `traits` (string) — one of: mentionable, active-scratchpad

#### Response 200

- `data` (object[], required)
  - `id` (string, required)
  - `workspaceId` (string, required)
  - `traits` (string[], required)
  - `slug` (string | null, required)
  - `name` (string, required)
  - `description` (string | null, required)
  - `avatarEmoji` (string | null, required)
  - `avatarUrl` (string | null, required)
  - `archivedAt` (string | null, required)
  - `createdAt` (string, required) — date-time
  - `updatedAt` (string, required) — date-time
  - `type` (string, required)
  - `ownerUserId` (string, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource

*GET /me/bots:*

```bash
curl "https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/me/bots" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

*Response 200 · example:*

```json
{
  "data": [
    {
      "id": "usr_01jd2q4z8kxw9v7r3m5t8n",
      "workspaceId": "ws_01jd2q4z8kxw9v7r3m5t8n",
      "traits": [
        "mentionable"
      ],
      "slug": "api-v3",
      "name": "Maya Reyes",
      "description": "Jordan asked to pause the v3 auth boundary until the rotation flow is reviewed.",
      "avatarEmoji": "…",
      "avatarUrl": "https://files.threa.io/attachments/rotation-flow.png",
      "archivedAt": "2026-03-12T11:42:00.000Z",
      "createdAt": "2026-03-12T11:42:00.000Z",
      "updatedAt": "2026-03-12T11:42:00.000Z",
      "type": "…",
      "ownerUserId": "usr_01jd2q4z8kxw9v7r3m5t8n"
    }
  ]
}
```

## Streams

List and inspect streams (channels, scratchpads, threads)

Recipes [Notify a stream from CI →](https://threa.io/developers/recipes.md#ci-notify)

### GET /api/v1/workspaces/{workspaceId}/streams

**List streams**

List streams accessible to this API key, with optional type and text filters.

Required scope: `streams:read`

#### Query parameters

- `type` (any, required)
- `query` (string)
- `after` (string)
- `limit` (integer) — 1–200 · default 50
- `includeArchived` (string) — one of: true, false

#### Response 200

- `data` (object[], required)
  - `id` (string, required)
  - `type` (string, required) — one of: scratchpad, channel, dm, thread, system
  - `displayName` (string, required)
  - `slug` (string)
  - `description` (string)
  - `visibility` (string, required)
  - `memoryMode` (string, required) — one of: auto, off. GAM memory automation gate: 'auto' extracts memos, 'off' disables it
  - `parentStreamId` (string)
  - `rootStreamId` (string)
  - `anchorId` (string) — Canonical id of the timeline item a thread anchors on. The prefix is the kind: 'msg_…' for a message, 'event_…' for a card. Present on threads only.
  - `createdAt` (string, required) — date-time
  - `archivedAt` (string) — date-time
- `hasMore` (boolean, required)
- `cursor` (string | null, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource

*GET /streams:*

```bash
curl "https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/streams?type=channel&limit=20" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

*Response 200 · example:*

```json
{
  "data": [
    {
      "id": "stream_01jd2q4z8kxw9v7r3m5t8n",
      "type": "scratchpad",
      "displayName": "api-v3",
      "slug": "api-v3",
      "description": "Jordan asked to pause the v3 auth boundary until the rotation flow is reviewed.",
      "visibility": "open",
      "memoryMode": "auto",
      "parentStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
      "rootStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
      "anchorId": "anchor_01jd2q4z8kxw9v7r3m5t8n",
      "createdAt": "2026-03-12T11:42:00.000Z",
      "archivedAt": "2026-03-12T11:42:00.000Z"
    }
  ],
  "hasMore": false,
  "cursor": null
}
```

### GET /api/v1/workspaces/{workspaceId}/streams/{streamId}

**Get a stream**

Required scope: `streams:read`

#### Path parameters

- `streamId` (string, required) — Stream ID (prefixed ULID)

#### Response 200

- `data` (object, required)
  - `id` (string, required)
  - `type` (string, required) — one of: scratchpad, channel, dm, thread, system
  - `displayName` (string, required)
  - `slug` (string)
  - `description` (string)
  - `visibility` (string, required)
  - `memoryMode` (string, required) — one of: auto, off. GAM memory automation gate: 'auto' extracts memos, 'off' disables it
  - `parentStreamId` (string)
  - `rootStreamId` (string)
  - `anchorId` (string) — Canonical id of the timeline item a thread anchors on. The prefix is the kind: 'msg_…' for a message, 'event_…' for a card. Present on threads only.
  - `createdAt` (string, required) — date-time
  - `archivedAt` (string) — date-time

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*GET /streams/{streamId}:*

```bash
curl "https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/streams/STREAM_ID" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

*Response 200 · example:*

```json
{
  "data": {
    "id": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "type": "scratchpad",
    "displayName": "api-v3",
    "slug": "api-v3",
    "description": "Jordan asked to pause the v3 auth boundary until the rotation flow is reviewed.",
    "visibility": "open",
    "memoryMode": "auto",
    "parentStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "rootStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "anchorId": "anchor_01jd2q4z8kxw9v7r3m5t8n",
    "createdAt": "2026-03-12T11:42:00.000Z",
    "archivedAt": "2026-03-12T11:42:00.000Z"
  }
}
```

### PATCH /api/v1/workspaces/{workspaceId}/streams/{streamId}

**Set a stream's description**

Set the stream's description from markdown (parsed to rich text, same as message content). Send an empty string to clear it. User-scoped keys attribute the change to the key owner; workspace-scoped keys to the bot.

Required scope: `streams:write`

#### Path parameters

- `streamId` (string, required) — Stream ID (prefixed ULID)

#### Request body

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

#### Response 200

- `data` (object, required)
  - `id` (string, required)
  - `type` (string, required) — one of: scratchpad, channel, dm, thread, system
  - `displayName` (string, required)
  - `slug` (string)
  - `description` (string)
  - `visibility` (string, required)
  - `memoryMode` (string, required) — one of: auto, off. GAM memory automation gate: 'auto' extracts memos, 'off' disables it
  - `parentStreamId` (string)
  - `rootStreamId` (string)
  - `anchorId` (string) — Canonical id of the timeline item a thread anchors on. The prefix is the kind: 'msg_…' for a message, 'event_…' for a card. Present on threads only.
  - `createdAt` (string, required) — date-time
  - `archivedAt` (string) — date-time

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*PATCH /streams/{streamId}:*

```bash
curl -X PATCH https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/streams/STREAM_ID \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "description": "string"
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "id": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "type": "scratchpad",
    "displayName": "api-v3",
    "slug": "api-v3",
    "description": "Jordan asked to pause the v3 auth boundary until the rotation flow is reviewed.",
    "visibility": "open",
    "memoryMode": "auto",
    "parentStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "rootStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "anchorId": "anchor_01jd2q4z8kxw9v7r3m5t8n",
    "createdAt": "2026-03-12T11:42:00.000Z",
    "archivedAt": "2026-03-12T11:42:00.000Z"
  }
}
```

### GET /api/v1/workspaces/{workspaceId}/streams/{streamId}/members

**List stream members**

Required scope: `streams:read`

#### Path parameters

- `streamId` (string, required) — Stream ID (prefixed ULID)

#### Query parameters

- `after` (string)
- `limit` (integer) — 1–200 · default 50

#### Response 200

- `data` (object[], required)
  - `userId` (string, required)
  - `name` (string, required)
  - `slug` (string, required)
  - `avatarUrl` (string)
  - `joinedAt` (string, required) — date-time
- `hasMore` (boolean, required)
- `cursor` (string | null, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource

*GET /streams/{streamId}/members:*

```bash
curl "https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/streams/STREAM_ID/members" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

*Response 200 · example:*

```json
{
  "data": [
    {
      "userId": "usr_01jd2q4z8kxw9v7r3m5t8n",
      "name": "api-v3",
      "slug": "api-v3",
      "avatarUrl": "https://files.threa.io/attachments/rotation-flow.png",
      "joinedAt": "2026-03-12T11:42:00.000Z"
    }
  ],
  "hasMore": false,
  "cursor": null
}
```

## Messages

Read, send, update, and delete messages

Recipes [Notify a stream from CI →](https://threa.io/developers/recipes.md#ci-notify)[Mirror a search into your own tool →](https://threa.io/developers/recipes.md#mirror-search)

### POST /api/v1/workspaces/{workspaceId}/messages/search

**Search messages**

Full-text and optional semantic search across accessible streams.

Required scope: `messages:search`

#### Request body

- `query` (string, required) — min length 1
- `semantic` (boolean, required) — default false
- `exact` (boolean, required) — default false
- `streams` (string[])
- `from` (string)
- `type` (string[])
- `before` (string) — date-time
- `after` (string) — date-time
- `limit` (integer, required) — 1–50 · default 20

#### Response 200

- `data` (object[], required)
  - `id` (string, required)
  - `streamId` (string, required)
  - `sequence` (string, required) — Numeric sequence as string
  - `content` (string, required)
  - `authorId` (string, required)
  - `authorType` (string, required) — one of: user, persona, system, bot
  - `authorDisplayName` (string)
  - `replyCount` (integer, required) — -9007199254740991–9007199254740991
  - `metadata` (object, required) — External references attached by the sender. Always present; empty when unset.
  - `editedAt` (string) — date-time
  - `createdAt` (string, required) — date-time
  - `rank` (number, required)
- `slots` (object, required) — Hydration for cross-stream shared-message pointers in the returned messages, keyed by `shared:<messageId>`. Always present; empty when no message references a shared source.

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource

*POST /messages/search:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/messages/search \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "query": "auth",
  "semantic": true,
  "exact": false,
  "limit": 5
}'
```

*Response 200 · example:*

```json
{
  "data": [
    {
      "id": "msg_01jd2q4z8kxw9v7r3m5t8n",
      "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
      "sequence": "412",
      "content": "Picking the auth refactor back up next sprint.",
      "authorId": "usr_01jd2q4z8kxw9v7r3m5t8n",
      "authorType": "user",
      "authorDisplayName": "Maya Reyes",
      "replyCount": 2,
      "metadata": {},
      "editedAt": "2026-03-12T11:42:00.000Z",
      "createdAt": "2026-03-12T11:42:00.000Z",
      "rank": 1
    }
  ],
  "slots": {}
}
```

### GET /api/v1/workspaces/{workspaceId}/streams/{streamId}/messages

**List messages in a stream**

Cursor-paginated message list. Use \`before\` or \`after\` sequence numbers.

Required scope: `messages:read`

#### Path parameters

- `streamId` (string, required) — Stream ID (prefixed ULID)

#### Query parameters

- `before` (string)
- `after` (string)
- `limit` (integer) — 1–100 · default 50

#### Response 200

- `data` (object[], required)
  - `id` (string, required)
  - `streamId` (string, required)
  - `sequence` (string, required) — Numeric sequence as string
  - `authorId` (string, required)
  - `authorType` (string, required) — one of: user, persona, system, bot
  - `authorDisplayName` (string)
  - `content` (string, required)
  - `replyCount` (integer, required) — -9007199254740991–9007199254740991
  - `threadStreamId` (string)
  - `clientMessageId` (string)
  - `sentVia` (string) — Present when message was sent via API on behalf of a user
  - `metadata` (object, required) — External references attached by the sender. Always present; empty when unset.
  - `attachments` (object[])
    - `id` (string, required)
    - `filename` (string, required)
    - `mimeType` (string, required)
    - `sizeBytes` (integer, required) — -9007199254740991–9007199254740991
    - `processingStatus` (string) — one of: pending, processing, completed, failed, skipped
    - `width` (integer) — -9007199254740991–9007199254740991
    - `height` (integer) — -9007199254740991–9007199254740991
  - `editedAt` (string) — date-time
  - `createdAt` (string, required) — date-time
- `hasMore` (boolean, required)
- `slots` (object, required) — Hydration for cross-stream shared-message pointers in the returned messages, keyed by `shared:<messageId>`. Always present; empty when no message references a shared source.

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource

*GET /streams/{streamId}/messages:*

```bash
curl "https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/streams/STREAM_ID/messages" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

*Response 200 · example:*

```json
{
  "data": [
    {
      "id": "msg_01jd2q4z8kxw9v7r3m5t8n",
      "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
      "sequence": "412",
      "authorId": "usr_01jd2q4z8kxw9v7r3m5t8n",
      "authorType": "user",
      "authorDisplayName": "Maya Reyes",
      "content": "Picking the auth refactor back up next sprint.",
      "replyCount": 2,
      "threadStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
      "clientMessageId": "ci-deploy-2.4.1",
      "sentVia": "…",
      "metadata": {},
      "attachments": [
        {
          "id": "msg_01jd2q4z8kxw9v7r3m5t8n",
          "filename": "rotation-flow.png",
          "mimeType": "image/png",
          "sizeBytes": 48213,
          "processingStatus": "pending",
          "width": 1,
          "height": 1
        }
      ],
      "editedAt": "2026-03-12T11:42:00.000Z",
      "createdAt": "2026-03-12T11:42:00.000Z"
    }
  ],
  "hasMore": false,
  "slots": {}
}
```

### POST /api/v1/workspaces/{workspaceId}/streams/{streamId}/messages

**Send a message**

Send a message. Workspace-scoped keys send as a bot; user-scoped keys send on behalf of the key owner. Optionally declare the message's conversation via \`conversation\`: \`{intent: "new"}\` starts a fresh conversation, \`{intent: "existing", conversationId}\` posts into one under the same root stream.

Required scope: `messages:write`

#### Path parameters

- `streamId` (string, required) — Stream ID (prefixed ULID)

#### Request body

- `content` (string, required) — min length 1
- `clientMessageId` (string) — max length 128
- `metadata` (object)
- `conversation` (object)

#### Response 201

- `data` (object, required)
  - `id` (string, required)
  - `streamId` (string, required)
  - `sequence` (string, required) — Numeric sequence as string
  - `authorId` (string, required)
  - `authorType` (string, required) — one of: user, persona, system, bot
  - `authorDisplayName` (string)
  - `content` (string, required)
  - `replyCount` (integer, required) — -9007199254740991–9007199254740991
  - `threadStreamId` (string)
  - `clientMessageId` (string)
  - `sentVia` (string) — Present when message was sent via API on behalf of a user
  - `metadata` (object, required) — External references attached by the sender. Always present; empty when unset.
  - `attachments` (object[])
    - `id` (string, required)
    - `filename` (string, required)
    - `mimeType` (string, required)
    - `sizeBytes` (integer, required) — -9007199254740991–9007199254740991
    - `processingStatus` (string) — one of: pending, processing, completed, failed, skipped
    - `width` (integer) — -9007199254740991–9007199254740991
    - `height` (integer) — -9007199254740991–9007199254740991
  - `editedAt` (string) — date-time
  - `createdAt` (string, required) — date-time
- `conversationId` (string) — The conversation the message was assigned to; present when the request declared a `conversation`.
- `slots` (object, required) — Hydration for cross-stream shared-message pointers in the returned messages, keyed by `shared:<messageId>`. Always present; empty when no message references a shared source.

#### Status codes

-   `201` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource

*POST /streams/{streamId}/messages:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/streams/STREAM_ID/messages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "content": "string"
}'
```

*Response 201 · example:*

```json
{
  "data": {
    "id": "msg_01jd2q4z8kxw9v7r3m5t8n",
    "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "sequence": "412",
    "authorId": "usr_01jd2q4z8kxw9v7r3m5t8n",
    "authorType": "user",
    "authorDisplayName": "Maya Reyes",
    "content": "Picking the auth refactor back up next sprint.",
    "replyCount": 2,
    "threadStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "clientMessageId": "ci-deploy-2.4.1",
    "sentVia": "…",
    "metadata": {},
    "attachments": [
      {
        "id": "msg_01jd2q4z8kxw9v7r3m5t8n",
        "filename": "rotation-flow.png",
        "mimeType": "image/png",
        "sizeBytes": 48213,
        "processingStatus": "pending",
        "width": 1,
        "height": 1
      }
    ],
    "editedAt": "2026-03-12T11:42:00.000Z",
    "createdAt": "2026-03-12T11:42:00.000Z"
  },
  "conversationId": "conversation_01jd2q4z8kxw9v7r3m5t8n",
  "slots": {}
}
```

### POST /api/v1/workspaces/{workspaceId}/messages/find-by-metadata

**Find messages by metadata**

Find non-deleted messages whose \`metadata\` contains all the given key/value pairs (AND-containment). Useful for dedup flows, e.g. 'has a message already been posted for this GitHub PR event?'.

Required scope: `messages:read`

#### Request body

- `metadata` (object, required)
- `streamId` (string) — min length 1
- `limit` (integer, required) — 1–100 · default 20

#### Response 200

- `data` (object[], required)
  - `id` (string, required)
  - `streamId` (string, required)
  - `sequence` (string, required) — Numeric sequence as string
  - `authorId` (string, required)
  - `authorType` (string, required) — one of: user, persona, system, bot
  - `authorDisplayName` (string)
  - `content` (string, required)
  - `replyCount` (integer, required) — -9007199254740991–9007199254740991
  - `threadStreamId` (string)
  - `clientMessageId` (string)
  - `sentVia` (string) — Present when message was sent via API on behalf of a user
  - `metadata` (object, required) — External references attached by the sender. Always present; empty when unset.
  - `attachments` (object[])
    - `id` (string, required)
    - `filename` (string, required)
    - `mimeType` (string, required)
    - `sizeBytes` (integer, required) — -9007199254740991–9007199254740991
    - `processingStatus` (string) — one of: pending, processing, completed, failed, skipped
    - `width` (integer) — -9007199254740991–9007199254740991
    - `height` (integer) — -9007199254740991–9007199254740991
  - `editedAt` (string) — date-time
  - `createdAt` (string, required) — date-time
- `slots` (object, required) — Hydration for cross-stream shared-message pointers in the returned messages, keyed by `shared:<messageId>`. Always present; empty when no message references a shared source.

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource

*POST /messages/find-by-metadata:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/messages/find-by-metadata \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "metadata": {},
  "limit": 20
}'
```

*Response 200 · example:*

```json
{
  "data": [
    {
      "id": "msg_01jd2q4z8kxw9v7r3m5t8n",
      "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
      "sequence": "412",
      "authorId": "usr_01jd2q4z8kxw9v7r3m5t8n",
      "authorType": "user",
      "authorDisplayName": "Maya Reyes",
      "content": "Picking the auth refactor back up next sprint.",
      "replyCount": 2,
      "threadStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
      "clientMessageId": "ci-deploy-2.4.1",
      "sentVia": "…",
      "metadata": {},
      "attachments": [
        {
          "id": "msg_01jd2q4z8kxw9v7r3m5t8n",
          "filename": "rotation-flow.png",
          "mimeType": "image/png",
          "sizeBytes": 48213,
          "processingStatus": "pending",
          "width": 1,
          "height": 1
        }
      ],
      "editedAt": "2026-03-12T11:42:00.000Z",
      "createdAt": "2026-03-12T11:42:00.000Z"
    }
  ],
  "slots": {}
}
```

### PATCH /api/v1/workspaces/{workspaceId}/messages/{messageId}

**Update a message**

Update a message you previously sent via API.

Required scope: `messages:write`

#### Path parameters

- `messageId` (string, required) — Message ID (prefixed ULID)

#### Request body

- `content` (string, required) — min length 1

#### Response 200

- `data` (object, required)
  - `id` (string, required)
  - `streamId` (string, required)
  - `sequence` (string, required) — Numeric sequence as string
  - `authorId` (string, required)
  - `authorType` (string, required) — one of: user, persona, system, bot
  - `authorDisplayName` (string)
  - `content` (string, required)
  - `replyCount` (integer, required) — -9007199254740991–9007199254740991
  - `threadStreamId` (string)
  - `clientMessageId` (string)
  - `sentVia` (string) — Present when message was sent via API on behalf of a user
  - `metadata` (object, required) — External references attached by the sender. Always present; empty when unset.
  - `attachments` (object[])
    - `id` (string, required)
    - `filename` (string, required)
    - `mimeType` (string, required)
    - `sizeBytes` (integer, required) — -9007199254740991–9007199254740991
    - `processingStatus` (string) — one of: pending, processing, completed, failed, skipped
    - `width` (integer) — -9007199254740991–9007199254740991
    - `height` (integer) — -9007199254740991–9007199254740991
  - `editedAt` (string) — date-time
  - `createdAt` (string, required) — date-time
- `slots` (object, required) — Hydration for cross-stream shared-message pointers in the returned messages, keyed by `shared:<messageId>`. Always present; empty when no message references a shared source.

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*PATCH /messages/{messageId}:*

```bash
curl -X PATCH https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/messages/MESSAGE_ID \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "content": "string"
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "id": "msg_01jd2q4z8kxw9v7r3m5t8n",
    "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "sequence": "412",
    "authorId": "usr_01jd2q4z8kxw9v7r3m5t8n",
    "authorType": "user",
    "authorDisplayName": "Maya Reyes",
    "content": "Picking the auth refactor back up next sprint.",
    "replyCount": 2,
    "threadStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "clientMessageId": "ci-deploy-2.4.1",
    "sentVia": "…",
    "metadata": {},
    "attachments": [
      {
        "id": "msg_01jd2q4z8kxw9v7r3m5t8n",
        "filename": "rotation-flow.png",
        "mimeType": "image/png",
        "sizeBytes": 48213,
        "processingStatus": "pending",
        "width": 1,
        "height": 1
      }
    ],
    "editedAt": "2026-03-12T11:42:00.000Z",
    "createdAt": "2026-03-12T11:42:00.000Z"
  },
  "slots": {}
}
```

### DELETE /api/v1/workspaces/{workspaceId}/messages/{messageId}

**Delete a message**

Delete a message you previously sent via API.

Required scope: `messages:write`

#### Path parameters

- `messageId` (string, required) — Message ID (prefixed ULID)

#### Status codes

-   `204` No content
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*DELETE /messages/{messageId}:*

```bash
curl -X DELETE https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/messages/MESSAGE_ID \
  -H "Authorization: Bearer YOUR_API_KEY"
```

## Conversations

Browse first-class conversations — topic-level groupings of messages under a root stream and its threads

### GET /api/v1/workspaces/{workspaceId}/conversations

**List conversations**

Cursor-paginated conversation feed across accessible streams, newest activity first. Filter with \`streamId\` (scopes to that stream's root and its threads) and \`status\`.

Required scope: `messages:read`

#### Query parameters

- `streamId` (string)
- `status` (string) — one of: active, stalled, resolved
- `after` (string)
- `limit` (integer) — 1–100 · default 50

#### Response 200

- `data` (object[], required)
  - `id` (string, required)
  - `streamId` (string, required) — Anchor stream the conversation lives in (may be a thread)
  - `rootStreamId` (string, required) — Effective root of the anchor — the stream whose access governs the conversation
  - `topicSummary` (string | null, required)
  - `summary` (string | null, required)
  - `status` (string, required) — one of: active, stalled, resolved
  - `messageCount` (integer, required) — -9007199254740991–9007199254740991. Number of primary member messages
  - `participantIds` (string[], required) — Distinct author ids of the member messages
  - `lastActivityAt` (string, required) — date-time
  - `createdAt` (string, required) — date-time
  - `updatedAt` (string, required) — date-time
- `hasMore` (boolean, required)
- `cursor` (string | null, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*GET /conversations:*

```bash
curl "https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/conversations" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

*Response 200 · example:*

```json
{
  "data": [
    {
      "id": "id_01jd2q4z8kxw9v7r3m5t8n",
      "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
      "rootStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
      "topicSummary": "Jordan asked to pause the v3 auth boundary until the rotation flow is reviewed.",
      "summary": "Jordan asked to pause the v3 auth boundary until the rotation flow is reviewed.",
      "status": "active",
      "messageCount": 2,
      "participantIds": [
        "…"
      ],
      "lastActivityAt": "2026-03-12T11:42:00.000Z",
      "createdAt": "2026-03-12T11:42:00.000Z",
      "updatedAt": "2026-03-12T11:42:00.000Z"
    }
  ],
  "hasMore": false,
  "cursor": null
}
```

### GET /api/v1/workspaces/{workspaceId}/conversations/{conversationId}

**Get a conversation**

Required scope: `messages:read`

#### Path parameters

- `conversationId` (string, required) — Conversation ID (prefixed ULID)

#### Response 200

- `data` (object, required)
  - `id` (string, required)
  - `streamId` (string, required) — Anchor stream the conversation lives in (may be a thread)
  - `rootStreamId` (string, required) — Effective root of the anchor — the stream whose access governs the conversation
  - `topicSummary` (string | null, required)
  - `summary` (string | null, required)
  - `status` (string, required) — one of: active, stalled, resolved
  - `messageCount` (integer, required) — -9007199254740991–9007199254740991. Number of primary member messages
  - `participantIds` (string[], required) — Distinct author ids of the member messages
  - `lastActivityAt` (string, required) — date-time
  - `createdAt` (string, required) — date-time
  - `updatedAt` (string, required) — date-time

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*GET /conversations/{conversationId}:*

```bash
curl "https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/conversations/CONVERSATION_ID" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

*Response 200 · example:*

```json
{
  "data": {
    "id": "id_01jd2q4z8kxw9v7r3m5t8n",
    "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "rootStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "topicSummary": "Jordan asked to pause the v3 auth boundary until the rotation flow is reviewed.",
    "summary": "Jordan asked to pause the v3 auth boundary until the rotation flow is reviewed.",
    "status": "active",
    "messageCount": 2,
    "participantIds": [
      "…"
    ],
    "lastActivityAt": "2026-03-12T11:42:00.000Z",
    "createdAt": "2026-03-12T11:42:00.000Z",
    "updatedAt": "2026-03-12T11:42:00.000Z"
  }
}
```

### GET /api/v1/workspaces/{workspaceId}/conversations/{conversationId}/messages

**List a conversation's messages**

The conversation's member messages in chronological order, cursor-paginated. Messages can span the conversation's root stream and its threads; each message carries its own \`streamId\`.

Required scope: `messages:read`

#### Path parameters

- `conversationId` (string, required) — Conversation ID (prefixed ULID)

#### Query parameters

- `after` (string)
- `limit` (integer) — 1–100 · default 50

#### Response 200

- `data` (object[], required)
  - `id` (string, required)
  - `streamId` (string, required)
  - `sequence` (string, required) — Numeric sequence as string
  - `authorId` (string, required)
  - `authorType` (string, required) — one of: user, persona, system, bot
  - `authorDisplayName` (string)
  - `content` (string, required)
  - `replyCount` (integer, required) — -9007199254740991–9007199254740991
  - `threadStreamId` (string)
  - `clientMessageId` (string)
  - `sentVia` (string) — Present when message was sent via API on behalf of a user
  - `metadata` (object, required) — External references attached by the sender. Always present; empty when unset.
  - `attachments` (object[])
    - `id` (string, required)
    - `filename` (string, required)
    - `mimeType` (string, required)
    - `sizeBytes` (integer, required) — -9007199254740991–9007199254740991
    - `processingStatus` (string) — one of: pending, processing, completed, failed, skipped
    - `width` (integer) — -9007199254740991–9007199254740991
    - `height` (integer) — -9007199254740991–9007199254740991
  - `editedAt` (string) — date-time
  - `createdAt` (string, required) — date-time
- `hasMore` (boolean, required)
- `cursor` (string | null, required)
- `slots` (object, required) — Hydration for cross-stream shared-message pointers in the returned messages, keyed by `shared:<messageId>`. Always present; empty when no message references a shared source.

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*GET /conversations/{conversationId}/messages:*

```bash
curl "https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/conversations/CONVERSATION_ID/messages" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

*Response 200 · example:*

```json
{
  "data": [
    {
      "id": "id_01jd2q4z8kxw9v7r3m5t8n",
      "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
      "sequence": "412",
      "authorId": "usr_01jd2q4z8kxw9v7r3m5t8n",
      "authorType": "user",
      "authorDisplayName": "Maya Reyes",
      "content": "Picking the auth refactor back up next sprint.",
      "replyCount": 2,
      "threadStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
      "clientMessageId": "ci-deploy-2.4.1",
      "sentVia": "…",
      "metadata": {},
      "attachments": [
        {
          "id": "id_01jd2q4z8kxw9v7r3m5t8n",
          "filename": "rotation-flow.png",
          "mimeType": "image/png",
          "sizeBytes": 48213,
          "processingStatus": "pending",
          "width": 1,
          "height": 1
        }
      ],
      "editedAt": "2026-03-12T11:42:00.000Z",
      "createdAt": "2026-03-12T11:42:00.000Z"
    }
  ],
  "hasMore": false,
  "cursor": null,
  "slots": {}
}
```

## Memos

Search preserved workspace knowledge and inspect memo provenance

Recipes [Find out what was decided, and why →](https://threa.io/developers/recipes.md#decisions)

### POST /api/v1/workspaces/{workspaceId}/memos/search

**Search memos**

Search preserved workspace memos with semantic, exact, or recent-first retrieval.

Required scope: `memos:read`

#### Request body

- `query` (string, required) — default ""
- `exact` (boolean)
- `streams` (string[])
- `memoType` (string[])
- `knowledgeType` (string[])
- `tags` (string[])
- `scope` (string) — one of: user, stream, workspace
- `before` (string) — date-time
- `after` (string) — date-time
- `limit` (integer, required) — 1–100 · default 20

#### Response 200

- `data` (object[], required)
  - `memo` (object, required)
    - `id` (string, required)
    - `workspaceId` (string, required)
    - `memoType` (string, required) — one of: message, conversation
    - `sourceMessageId` (string | null, required)
    - `sourceConversationId` (string | null, required)
    - `title` (string, required)
    - `abstract` (string, required)
    - `keyPoints` (string[], required)
    - `sourceMessageIds` (string[], required)
    - `participantIds` (string[], required)
    - `knowledgeType` (string, required) — one of: decision, learning, procedure, context, reference
    - `tags` (string[], required)
    - `parentMemoId` (string | null, required)
    - `status` (string, required)
    - `version` (integer, required) — -9007199254740991–9007199254740991
    - `revisionReason` (string | null, required)
    - `authoredByKind` (string, required) — one of: pipeline, agent
    - `sourceSessionId` (string | null, required)
    - `scope` (string, required) — one of: user, stream, workspace
    - `scopeUserId` (string | null, required)
    - `createdAt` (string, required) — date-time
    - `updatedAt` (string, required) — date-time
    - `archivedAt` (string | null, required)
  - `distance` (number, required)
  - `sourceStream` (object | null, required)
  - `rootStream` (object | null, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource

*POST /memos/search:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/memos/search \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "query": "auth",
  "limit": 5
}'
```

*Response 200 · example:*

```json
{
  "data": [
    {
      "memo": {
        "id": "memo_01jd2q4z8kxw9v7r3m5t8n",
        "workspaceId": "ws_01jd2q4z8kxw9v7r3m5t8n",
        "memoType": "message",
        "sourceMessageId": "msg_01jd2q4z8kxw9v7r3m5t8n",
        "sourceConversationId": "conversation_01jd2q4z8kxw9v7r3m5t8n",
        "title": "Auth refactor held pending token-rotation review",
        "abstract": "Jordan asked to pause the v3 auth boundary until the rotation flow is reviewed.",
        "keyPoints": [
          "…"
        ],
        "sourceMessageIds": [
          "…"
        ],
        "participantIds": [
          "…"
        ],
        "knowledgeType": "decision",
        "tags": [
          "api-v3"
        ],
        "parentMemoId": "memo_01jd2q4z8kxw9v7r3m5t8n",
        "status": "available",
        "version": 1,
        "revisionReason": "…",
        "authoredByKind": "pipeline",
        "sourceSessionId": "session_01jd2q4z8kxw9v7r3m5t8n",
        "scope": "user",
        "scopeUserId": "usr_01jd2q4z8kxw9v7r3m5t8n",
        "createdAt": "2026-03-12T11:42:00.000Z",
        "updatedAt": "2026-03-12T11:42:00.000Z",
        "archivedAt": "2026-03-12T11:42:00.000Z"
      },
      "distance": 1,
      "sourceStream": {
        "id": "memo_01jd2q4z8kxw9v7r3m5t8n",
        "type": "…",
        "name": "api-v3"
      },
      "rootStream": {
        "id": "memo_01jd2q4z8kxw9v7r3m5t8n",
        "type": "…",
        "name": "api-v3"
      }
    }
  ]
}
```

### GET /api/v1/workspaces/{workspaceId}/memos/{memoId}

**Get a memo**

Retrieve a memo together with source stream and source message provenance.

Required scope: `memos:read`

#### Path parameters

- `memoId` (string, required) — Memo ID (prefixed ULID)

#### Response 200

- `data` (object, required)
  - `memo` (object, required)
    - `id` (string, required)
    - `workspaceId` (string, required)
    - `memoType` (string, required) — one of: message, conversation
    - `sourceMessageId` (string | null, required)
    - `sourceConversationId` (string | null, required)
    - `title` (string, required)
    - `abstract` (string, required)
    - `keyPoints` (string[], required)
    - `sourceMessageIds` (string[], required)
    - `participantIds` (string[], required)
    - `knowledgeType` (string, required) — one of: decision, learning, procedure, context, reference
    - `tags` (string[], required)
    - `parentMemoId` (string | null, required)
    - `status` (string, required)
    - `version` (integer, required) — -9007199254740991–9007199254740991
    - `revisionReason` (string | null, required)
    - `authoredByKind` (string, required) — one of: pipeline, agent
    - `sourceSessionId` (string | null, required)
    - `scope` (string, required) — one of: user, stream, workspace
    - `scopeUserId` (string | null, required)
    - `createdAt` (string, required) — date-time
    - `updatedAt` (string, required) — date-time
    - `archivedAt` (string | null, required)
  - `distance` (number, required)
  - `sourceStream` (object | null, required)
  - `rootStream` (object | null, required)
  - `sourceMessages` (object[], required)
    - `id` (string, required)
    - `streamId` (string, required)
    - `streamName` (string, required)
    - `authorId` (string, required)
    - `authorType` (string, required) — one of: user, persona, system, bot
    - `authorName` (string, required)
    - `content` (string, required)
    - `createdAt` (string, required) — date-time
  - `successorMemoId` (string | null, required)
  - `capturedByPersonaName` (string | null, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*GET /memos/{memoId}:*

```bash
curl "https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/memos/MEMO_ID" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

*Response 200 · example:*

```json
{
  "data": {
    "memo": {
      "id": "memo_01jd2q4z8kxw9v7r3m5t8n",
      "workspaceId": "ws_01jd2q4z8kxw9v7r3m5t8n",
      "memoType": "message",
      "sourceMessageId": "msg_01jd2q4z8kxw9v7r3m5t8n",
      "sourceConversationId": "conversation_01jd2q4z8kxw9v7r3m5t8n",
      "title": "Auth refactor held pending token-rotation review",
      "abstract": "Jordan asked to pause the v3 auth boundary until the rotation flow is reviewed.",
      "keyPoints": [
        "…"
      ],
      "sourceMessageIds": [
        "…"
      ],
      "participantIds": [
        "…"
      ],
      "knowledgeType": "decision",
      "tags": [
        "api-v3"
      ],
      "parentMemoId": "memo_01jd2q4z8kxw9v7r3m5t8n",
      "status": "available",
      "version": 1,
      "revisionReason": "…",
      "authoredByKind": "pipeline",
      "sourceSessionId": "session_01jd2q4z8kxw9v7r3m5t8n",
      "scope": "user",
      "scopeUserId": "usr_01jd2q4z8kxw9v7r3m5t8n",
      "createdAt": "2026-03-12T11:42:00.000Z",
      "updatedAt": "2026-03-12T11:42:00.000Z",
      "archivedAt": "2026-03-12T11:42:00.000Z"
    },
    "distance": 1,
    "sourceStream": {
      "id": "memo_01jd2q4z8kxw9v7r3m5t8n",
      "type": "…",
      "name": "api-v3"
    },
    "rootStream": {
      "id": "memo_01jd2q4z8kxw9v7r3m5t8n",
      "type": "…",
      "name": "api-v3"
    },
    "sourceMessages": [
      {
        "id": "memo_01jd2q4z8kxw9v7r3m5t8n",
        "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
        "streamName": "…",
        "authorId": "usr_01jd2q4z8kxw9v7r3m5t8n",
        "authorType": "user",
        "authorName": "…",
        "content": "Picking the auth refactor back up next sprint.",
        "createdAt": "2026-03-12T11:42:00.000Z"
      }
    ],
    "successorMemoId": "memo_01jd2q4z8kxw9v7r3m5t8n",
    "capturedByPersonaName": "…"
  }
}
```

## Attachments

Search attachments, inspect extracted content, and fetch download URLs

### POST /api/v1/workspaces/{workspaceId}/attachments

**Upload an attachment**

Upload a file as multipart/form-data using field \`file\`. Include the returned attachment id in message markdown as \`attachment:<id>\` to attach it to a message.

Required scope: `attachments:write`

#### Response 201

- `data` (object, required)
  - `id` (string, required)
  - `filename` (string, required)
  - `mimeType` (string, required)
  - `sizeBytes` (integer, required) — -9007199254740991–9007199254740991
  - `processingStatus` (string, required) — one of: pending, processing, completed, failed, skipped
  - `createdAt` (string, required) — date-time

#### Status codes

-   `201` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource

*POST /attachments:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/attachments \
  -H "Authorization: Bearer YOUR_API_KEY"
```

*Response 201 · example:*

```json
{
  "data": {
    "id": "attach_01jd2q4z8kxw9v7r3m5t8n",
    "filename": "rotation-flow.png",
    "mimeType": "image/png",
    "sizeBytes": 48213,
    "processingStatus": "pending",
    "createdAt": "2026-03-12T11:42:00.000Z"
  }
}
```

### POST /api/v1/workspaces/{workspaceId}/attachments/search

**Search attachments**

Search accessible attachments by filename or extracted content. Omit query to browse the most recent attachments.

Required scope: `attachments:read`

#### Request body

- `query` (string) — min length 1
- `streams` (string[])
- `contentTypes` (string[])
- `limit` (integer, required) — 1–50 · default 20

#### Response 200

- `data` (object[], required)
  - `id` (string, required)
  - `filename` (string, required)
  - `mimeType` (string, required)
  - `contentType` (string | null, required)
  - `summary` (string | null, required)
  - `streamId` (string)
  - `messageId` (string)
  - `createdAt` (string, required) — date-time

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource

*POST /attachments/search:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/attachments/search \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "query": "auth",
  "limit": 5
}'
```

*Response 200 · example:*

```json
{
  "data": [
    {
      "id": "attach_01jd2q4z8kxw9v7r3m5t8n",
      "filename": "rotation-flow.png",
      "mimeType": "image/png",
      "contentType": "chart",
      "summary": "Jordan asked to pause the v3 auth boundary until the rotation flow is reviewed.",
      "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
      "messageId": "msg_01jd2q4z8kxw9v7r3m5t8n",
      "createdAt": "2026-03-12T11:42:00.000Z"
    }
  ]
}
```

### GET /api/v1/workspaces/{workspaceId}/attachments/{attachmentId}

**Get an attachment**

Retrieve attachment metadata and extracted content for an accessible attachment.

Required scope: `attachments:read`

#### Path parameters

- `attachmentId` (string, required) — Attachment ID (prefixed ULID)

#### Response 200

- `data` (object, required)
  - `id` (string, required)
  - `filename` (string, required)
  - `mimeType` (string, required)
  - `sizeBytes` (integer, required) — -9007199254740991–9007199254740991
  - `processingStatus` (string, required) — one of: pending, processing, completed, failed, skipped
  - `createdAt` (string, required) — date-time
  - `extraction` (object | null, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*GET /attachments/{attachmentId}:*

```bash
curl "https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/attachments/ATTACHMENT_ID" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

*Response 200 · example:*

```json
{
  "data": {
    "id": "attach_01jd2q4z8kxw9v7r3m5t8n",
    "filename": "rotation-flow.png",
    "mimeType": "image/png",
    "sizeBytes": 48213,
    "processingStatus": "pending",
    "createdAt": "2026-03-12T11:42:00.000Z",
    "extraction": {
      "contentType": "chart",
      "summary": "Jordan asked to pause the v3 auth boundary until the rotation flow is reviewed.",
      "fullText": "Picking the auth refactor back up next sprint.",
      "structuredData": null,
      "pdfMetadata": null,
      "textMetadata": null,
      "wordMetadata": null,
      "excelMetadata": null
    }
  }
}
```

### GET /api/v1/workspaces/{workspaceId}/attachments/{attachmentId}/url

**Get an attachment download URL**

Create a short-lived signed URL for an accessible attachment.

Required scope: `attachments:read`

#### Path parameters

- `attachmentId` (string, required) — Attachment ID (prefixed ULID)

#### Response 200

- `data` (object, required)
  - `url` (string, required) — uri
  - `expiresIn` (integer, required) — -9007199254740991–9007199254740991

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*GET /attachments/{attachmentId}/url:*

```bash
curl "https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/attachments/ATTACHMENT_ID/url" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

*Response 200 · example:*

```json
{
  "data": {
    "url": "https://files.threa.io/attachments/rotation-flow.png",
    "expiresIn": 1
  }
}
```

## Users

List workspace users

### GET /api/v1/workspaces/{workspaceId}/users

**List workspace users**

List users in the workspace with optional text search and cursor pagination.

Required scope: `users:read`

#### Query parameters

- `query` (string)
- `after` (string)
- `limit` (integer) — 1–200 · default 50

#### Response 200

- `data` (object[], required)
  - `id` (string, required)
  - `name` (string, required)
  - `slug` (string, required)
  - `email` (string, required)
  - `avatarUrl` (string)
  - `role` (string, required)
- `hasMore` (boolean, required)
- `cursor` (string | null, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource

*GET /users:*

```bash
curl "https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/users?limit=20" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

*Response 200 · example:*

```json
{
  "data": [
    {
      "id": "usr_01jd2q4z8kxw9v7r3m5t8n",
      "name": "Maya Reyes",
      "slug": "api-v3",
      "email": "maya@acme.dev",
      "avatarUrl": "https://files.threa.io/attachments/rotation-flow.png",
      "role": "member"
    }
  ],
  "hasMore": false,
  "cursor": null
}
```

## Labels

List, create, edit, archive, join, and apply workspace labels

### GET /api/v1/workspaces/{workspaceId}/labels

**List labels**

The key actor's labels (every label is private to its owner) and their resource assignments.

Required scope: `labels:read`

#### Response 200

- `data` (object, required)
  - `labels` (object[], required)
    - `id` (string, required)
    - `workspaceId` (string, required)
    - `creatorActorType` (string, required) — one of: user, bot
    - `creatorActorId` (string, required)
    - `name` (string, required)
    - `slug` (string, required)
    - `color` (string, required)
    - `emoji` (string | null, required)
    - `description` (string | null, required)
    - `createdAt` (string, required) — date-time
    - `updatedAt` (string, required) — date-time
    - `archivedAt` (string | null, required)
  - `assignments` (object[], required)
    - `labelId` (string, required)
    - `resourceType` (string, required) — one of: stream, message
    - `resourceId` (string, required)
    - `actorType` (string, required) — one of: user, bot
    - `actorId` (string, required)
    - `workspaceId` (string, required)
    - `assignedAt` (string, required) — date-time

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource

*GET /labels:*

```bash
curl "https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/labels" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

*Response 200 · example:*

```json
{
  "data": {
    "labels": [
      {
        "id": "label_01jd2q4z8kxw9v7r3m5t8n",
        "workspaceId": "ws_01jd2q4z8kxw9v7r3m5t8n",
        "creatorActorType": "user",
        "creatorActorId": "creatoractor_01jd2q4z8kxw9v7r3m5t8n",
        "name": "api-v3",
        "slug": "api-v3",
        "color": "…",
        "emoji": "…",
        "description": "Jordan asked to pause the v3 auth boundary until the rotation flow is reviewed.",
        "createdAt": "2026-03-12T11:42:00.000Z",
        "updatedAt": "2026-03-12T11:42:00.000Z",
        "archivedAt": "2026-03-12T11:42:00.000Z"
      }
    ],
    "assignments": [
      {
        "labelId": "label_01jd2q4z8kxw9v7r3m5t8n",
        "resourceType": "stream",
        "resourceId": "resource_01jd2q4z8kxw9v7r3m5t8n",
        "actorType": "user",
        "actorId": "actor_01jd2q4z8kxw9v7r3m5t8n",
        "workspaceId": "ws_01jd2q4z8kxw9v7r3m5t8n",
        "assignedAt": "2026-03-12T11:42:00.000Z"
      }
    ]
  }
}
```

### POST /api/v1/workspaces/{workspaceId}/labels

**Create or update a label by name**

Find-or-create a label owned by the key actor (a user or a bot), keyed by its name. Posting an existing name returns that label and applies any appearance fields supplied; labels are identified by their text, so this is idempotent.

Required scope: `labels:write`

#### Request body

- `name` (string, required) — min length 1 · max length 100
- `color` (string)
- `emoji` (string | null)
- `description` (string | null)

#### Response 201

- `data` (object, required)
  - `id` (string, required)
  - `workspaceId` (string, required)
  - `creatorActorType` (string, required) — one of: user, bot
  - `creatorActorId` (string, required)
  - `name` (string, required)
  - `slug` (string, required)
  - `color` (string, required)
  - `emoji` (string | null, required)
  - `description` (string | null, required)
  - `createdAt` (string, required) — date-time
  - `updatedAt` (string, required) — date-time
  - `archivedAt` (string | null, required)

#### Status codes

-   `201` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource

*POST /labels:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/labels \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "string"
}'
```

*Response 201 · example:*

```json
{
  "data": {
    "id": "label_01jd2q4z8kxw9v7r3m5t8n",
    "workspaceId": "ws_01jd2q4z8kxw9v7r3m5t8n",
    "creatorActorType": "user",
    "creatorActorId": "creatoractor_01jd2q4z8kxw9v7r3m5t8n",
    "name": "api-v3",
    "slug": "api-v3",
    "color": "…",
    "emoji": "…",
    "description": "Jordan asked to pause the v3 auth boundary until the rotation flow is reviewed.",
    "createdAt": "2026-03-12T11:42:00.000Z",
    "updatedAt": "2026-03-12T11:42:00.000Z",
    "archivedAt": "2026-03-12T11:42:00.000Z"
  }
}
```

### POST /api/v1/workspaces/{workspaceId}/labels/assignments

**Apply a label to a resource by name**

Attach a label to a resource the key actor can reach, identifying the label by its text: the label is found-or-created for the actor, then assigned. \`resourceType\` is the polymorphic target (\`stream\` today) so the same endpoint labels any future resource without a wire change.

Required scope: `labels:write`

#### Request body

- `name` (string, required) — min length 1 · max length 100
- `color` (string)
- `emoji` (string | null)
- `description` (string | null)
- `resourceType` (string, required) — one of: stream, message
- `resourceId` (string, required) — min length 1 · max length 64

#### Response 201

- `data` (object, required)
  - `label` (object, required)
    - `id` (string, required)
    - `workspaceId` (string, required)
    - `creatorActorType` (string, required) — one of: user, bot
    - `creatorActorId` (string, required)
    - `name` (string, required)
    - `slug` (string, required)
    - `color` (string, required)
    - `emoji` (string | null, required)
    - `description` (string | null, required)
    - `createdAt` (string, required) — date-time
    - `updatedAt` (string, required) — date-time
    - `archivedAt` (string | null, required)
  - `assignment` (object, required)
    - `labelId` (string, required)
    - `resourceType` (string, required) — one of: stream, message
    - `resourceId` (string, required)
    - `actorType` (string, required) — one of: user, bot
    - `actorId` (string, required)
    - `workspaceId` (string, required)
    - `assignedAt` (string, required) — date-time

#### Status codes

-   `201` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*POST /labels/assignments:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/labels/assignments \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "string",
  "resourceType": "stream",
  "resourceId": "string"
}'
```

*Response 201 · example:*

```json
{
  "data": {
    "label": {
      "id": "label_01jd2q4z8kxw9v7r3m5t8n",
      "workspaceId": "ws_01jd2q4z8kxw9v7r3m5t8n",
      "creatorActorType": "user",
      "creatorActorId": "creatoractor_01jd2q4z8kxw9v7r3m5t8n",
      "name": "api-v3",
      "slug": "api-v3",
      "color": "…",
      "emoji": "…",
      "description": "Jordan asked to pause the v3 auth boundary until the rotation flow is reviewed.",
      "createdAt": "2026-03-12T11:42:00.000Z",
      "updatedAt": "2026-03-12T11:42:00.000Z",
      "archivedAt": "2026-03-12T11:42:00.000Z"
    },
    "assignment": {
      "labelId": "label_01jd2q4z8kxw9v7r3m5t8n",
      "resourceType": "stream",
      "resourceId": "resource_01jd2q4z8kxw9v7r3m5t8n",
      "actorType": "user",
      "actorId": "actor_01jd2q4z8kxw9v7r3m5t8n",
      "workspaceId": "ws_01jd2q4z8kxw9v7r3m5t8n",
      "assignedAt": "2026-03-12T11:42:00.000Z"
    }
  }
}
```

### DELETE /api/v1/workspaces/{workspaceId}/labels/assignments

**Remove a label from a resource by name**

Remove the key actor's assignment of a label (identified by its text) from a resource.

Required scope: `labels:write`

#### Query parameters

- `name` (string, required) — min length 1 · max length 100
- `resourceType` (string, required) — one of: stream, message
- `resourceId` (string, required) — min length 1 · max length 64

#### Status codes

-   `204` No content
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*DELETE /labels/assignments:*

```bash
curl -X DELETE https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/labels/assignments?name=NAME&resourceType=stream&resourceId=RESOURCEID \
  -H "Authorization: Bearer YOUR_API_KEY"
```

### PATCH /api/v1/workspaces/{workspaceId}/labels/{labelId}

**Update a label**

Update a label the key actor created.

Required scope: `labels:write`

#### Path parameters

- `labelId` (string, required) — Label ID (prefixed ULID)

#### Request body

- `name` (string) — min length 1 · max length 100
- `color` (string)
- `emoji` (string | null)
- `description` (string | null)

#### Response 200

- `data` (object, required)
  - `id` (string, required)
  - `workspaceId` (string, required)
  - `creatorActorType` (string, required) — one of: user, bot
  - `creatorActorId` (string, required)
  - `name` (string, required)
  - `slug` (string, required)
  - `color` (string, required)
  - `emoji` (string | null, required)
  - `description` (string | null, required)
  - `createdAt` (string, required) — date-time
  - `updatedAt` (string, required) — date-time
  - `archivedAt` (string | null, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*PATCH /labels/{labelId}:*

```bash
curl -X PATCH https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/labels/LABEL_ID \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "string",
  "color": "string",
  "emoji": null
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "id": "label_01jd2q4z8kxw9v7r3m5t8n",
    "workspaceId": "ws_01jd2q4z8kxw9v7r3m5t8n",
    "creatorActorType": "user",
    "creatorActorId": "creatoractor_01jd2q4z8kxw9v7r3m5t8n",
    "name": "api-v3",
    "slug": "api-v3",
    "color": "…",
    "emoji": "…",
    "description": "Jordan asked to pause the v3 auth boundary until the rotation flow is reviewed.",
    "createdAt": "2026-03-12T11:42:00.000Z",
    "updatedAt": "2026-03-12T11:42:00.000Z",
    "archivedAt": "2026-03-12T11:42:00.000Z"
  }
}
```

### DELETE /api/v1/workspaces/{workspaceId}/labels/{labelId}

**Delete a label**

Archive a label the key actor created and remove its assignments.

Required scope: `labels:write`

#### Path parameters

- `labelId` (string, required) — Label ID (prefixed ULID)

#### Status codes

-   `204` No content
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*DELETE /labels/{labelId}:*

```bash
curl -X DELETE https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/labels/LABEL_ID \
  -H "Authorization: Bearer YOUR_API_KEY"
```

## Bot runtimes

Register a runtime and keep its presence alive so it can be assigned work.

Recipes [Connect your local agent →](https://threa.io/developers/recipes.md#local-agent)

### POST /api/v1/workspaces/{workspaceId}/bot-runtime/presence

**Heartbeat bot runtime presence**

Required scope: `bot-runtime:write`

#### Request body

- `runtimeKind` (string, required) — one of: pi-local, hermes, openclaw, claude-code-channel, custom
- `instanceId` (string, required) — min length 1 · max length 128
- `runtimeSessionId` (string) — min length 1 · max length 256
- `displayName` (string) — max length 100
- `status` (string, required) — one of: available, busy, offline, error
- `acceptingInvocations` (boolean, required)
- `capabilities` (object, required) — default {}
- `statusText` (string) — max length 200
- `publicKey` (string) — min length 44 · max length 44
- `publicKeyId` (string) — min length 1 · max length 128

#### Response 200

- `data` (object, required)
  - `id` (string, required)
  - `workspaceId` (string, required)
  - `botId` (string, required)
  - `runtimeKind` (string, required) — one of: pi-local, hermes, openclaw, claude-code-channel, custom
  - `instanceId` (string, required)
  - `displayName` (string | null, required)
  - `status` (string, required) — one of: available, busy, offline, error
  - `acceptingInvocations` (boolean, required)
  - `capabilities` (object, required)
  - `statusText` (string | null, required)
  - `lastSeenAt` (string, required) — date-time
  - `createdAt` (string, required) — date-time
  - `updatedAt` (string, required) — date-time

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource

*POST /bot-runtime/presence:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/bot-runtime/presence \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "runtimeKind": "pi-local",
  "instanceId": "string",
  "status": "available",
  "acceptingInvocations": false,
  "capabilities": {}
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "id": "bot_01jd2q4z8kxw9v7r3m5t8n",
    "workspaceId": "ws_01jd2q4z8kxw9v7r3m5t8n",
    "botId": "bot_01jd2q4z8kxw9v7r3m5t8n",
    "runtimeKind": "pi-local",
    "instanceId": "my-laptop-1",
    "displayName": "Maya Reyes",
    "status": "available",
    "acceptingInvocations": true,
    "capabilities": {},
    "statusText": "Picking the auth refactor back up next sprint.",
    "lastSeenAt": "2026-03-12T11:42:00.000Z",
    "createdAt": "2026-03-12T11:42:00.000Z",
    "updatedAt": "2026-03-12T11:42:00.000Z"
  }
}
```

### POST /api/v1/workspaces/{workspaceId}/bot-runtime/sessions

**Create or link a bot runtime session**

Required scope: `bot-runtime:write`

#### Request body

- `runtimeKind` (string, required) — one of: pi-local, claude-code-channel
- `instanceId` (string, required) — min length 1 · max length 128
- `runtimeSessionId` (string, required) — min length 1 · max length 256
- `displayName` (string, required) — min length 1 · max length 100
- `localCwd` (string) — max length 1000
- `memoryMode` (string) — one of: auto, off
- `labelName` (string) — min length 1 · max length 100
- `description` (string) — max length 10000
- `e2e` (object)
  - `ownerKeyId` (string, required) — min length 1 · max length 128
- `ifArchived` (string) — one of: wait, replace
- `ifMissing` (string) — one of: create, error

#### Response 200

- `data` (object, required)
  - `linkId` (string, required)
  - `rootStreamId` (string, required)
  - `activeStreamId` (string, required)
  - `runtimeSessionId` (string, required)
  - `streamUrlPath` (string, required)
  - `e2eEnabled` (boolean)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource

*POST /bot-runtime/sessions:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/bot-runtime/sessions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "runtimeKind": "pi-local",
  "instanceId": "string",
  "runtimeSessionId": "string",
  "displayName": "string"
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "linkId": "link_01jd2q4z8kxw9v7r3m5t8n",
    "rootStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "activeStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "runtimeSessionId": "runtimesession_01jd2q4z8kxw9v7r3m5t8n",
    "streamUrlPath": "https://files.threa.io/attachments/rotation-flow.png",
    "e2eEnabled": true
  }
}
```

### GET /api/v1/workspaces/{workspaceId}/bot-runtime/owner-e2e-key

**Get the bot owner's active encryption public key**

The bot owner's active UIK (key id + base64 X25519 public key). A sealed harness fetches this before creating an end-to-end-encrypted session so it can wrap the generation-0 stream key to the owner. 404 when the owner has not set up encryption.

Required scope: `bot-runtime:write`

#### Response 200

- `data` (object, required)
  - `keyId` (string, required)
  - `publicKey` (string, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*GET /bot-runtime/owner-e2e-key:*

```bash
curl "https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/bot-runtime/owner-e2e-key" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

*Response 200 · example:*

```json
{
  "data": {
    "keyId": "key_01jd2q4z8kxw9v7r3m5t8n",
    "publicKey": "…"
  }
}
```

### POST /api/v1/workspaces/{workspaceId}/streams/{streamId}/e2e/key-wraps

**Provision the generation-0 key wraps for a harness-created encrypted scratchpad**

Phase two of harness-created E2E scratchpads: stores the stream-key wraps (owner UIK + the harness's own BIK) minted against the stream id returned by session create. Bot-actor-only, current generation only, and only while the generation has no wraps. Slots are immutable, so a replay cannot splice keys.

Required scope: `bot-runtime:write`

#### Path parameters

- `streamId` (string, required) — Stream ID

#### Request body

- `keyGeneration` (integer, required) — 0–9007199254740991
- `wraps` (object[], required)
  - `recipientKind` (string, required) — one of: user, bot
  - `recipientKeyId` (string, required) — min length 1 · max length 128
  - `wrapEnc` (string, required) — base64 · min length 1
  - `wrapCt` (string, required) — base64 · min length 1

#### Response 200

- `data` (object, required)
  - `stored` (integer, required) — -9007199254740991–9007199254740991

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*POST /streams/{streamId}/e2e/key-wraps:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/streams/STREAM_ID/e2e/key-wraps \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "keyGeneration": 0,
  "wraps": [
    {
      "recipientKind": "user",
      "recipientKeyId": "string",
      "wrapEnc": "string",
      "wrapCt": "string"
    }
  ]
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "stored": 1
  }
}
```

### POST /api/v1/workspaces/{workspaceId}/bot-runtime/sessions/rename

**Rename the scratchpad linked to a bot runtime session**

Required scope: `bot-runtime:write`

#### Request body

- `instanceId` (string, required) — min length 1 · max length 128
- `runtimeSessionId` (string, required) — min length 1 · max length 256
- `displayName` (string, required) — min length 1 · max length 100

#### Response 200

- `data` (object, required)
  - `linkId` (string, required)
  - `rootStreamId` (string, required)
  - `activeStreamId` (string, required)
  - `runtimeSessionId` (string, required)
  - `streamUrlPath` (string, required)
  - `e2eEnabled` (boolean)
  - `displayName` (string, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*POST /bot-runtime/sessions/rename:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/bot-runtime/sessions/rename \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "instanceId": "string",
  "runtimeSessionId": "string",
  "displayName": "string"
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "linkId": "link_01jd2q4z8kxw9v7r3m5t8n",
    "rootStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "activeStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "runtimeSessionId": "runtimesession_01jd2q4z8kxw9v7r3m5t8n",
    "streamUrlPath": "https://files.threa.io/attachments/rotation-flow.png",
    "e2eEnabled": true,
    "displayName": "Maya Reyes"
  }
}
```

### POST /api/v1/workspaces/{workspaceId}/bot-runtime/sessions/rebind

**Move an existing bot runtime session link to a new runtime instance id**

Required scope: `bot-runtime:write`

#### Request body

- `linkId` (string, required) — min length 1 · max length 128
- `instanceId` (string, required) — min length 1 · max length 128
- `runtimeSessionId` (string, required) — min length 1 · max length 256
- `newInstanceId` (string, required) — min length 1 · max length 128

#### Response 200

- `data` (object, required)
  - `linkId` (string, required)
  - `rootStreamId` (string, required)
  - `activeStreamId` (string, required)
  - `runtimeSessionId` (string, required)
  - `streamUrlPath` (string, required)
  - `e2eEnabled` (boolean)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*POST /bot-runtime/sessions/rebind:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/bot-runtime/sessions/rebind \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "linkId": "string",
  "instanceId": "string",
  "runtimeSessionId": "string",
  "newInstanceId": "string"
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "linkId": "link_01jd2q4z8kxw9v7r3m5t8n",
    "rootStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "activeStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "runtimeSessionId": "runtimesession_01jd2q4z8kxw9v7r3m5t8n",
    "streamUrlPath": "https://files.threa.io/attachments/rotation-flow.png",
    "e2eEnabled": true
  }
}
```

## Bot invocations

Claim, renew, step through, complete, or fail the work a bot is summoned to do.

Recipes [Connect your local agent →](https://threa.io/developers/recipes.md#local-agent)

### POST /api/v1/workspaces/{workspaceId}/bot-invocations/claim

**Claim one pending bot invocation**

Required scope: `bot-invocations:write`

#### Request body

- `runtimeKind` (string, required) — one of: pi-local, hermes, openclaw, claude-code-channel, custom
- `instanceId` (string, required) — min length 1 · max length 128
- `runtimeSessionId` (string) — min length 1 · max length 256
- `supportedCapabilities` (string[], required)
- `claimTtlSeconds` (integer, required) — 15–300 · default 60
- `responseStreamId` (string) — min length 1 · max length 64

#### Response 200

- `data` (object | null, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource

*POST /bot-invocations/claim:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/bot-invocations/claim \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "runtimeKind": "pi-local",
  "instanceId": "string",
  "supportedCapabilities": [
    "mentionable"
  ],
  "claimTtlSeconds": 60
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "id": "inv_01jd2q4z8kxw9v7r3m5t8n",
    "workspaceId": "ws_01jd2q4z8kxw9v7r3m5t8n",
    "rootStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "activeStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "sourceMessageId": "msg_01jd2q4z8kxw9v7r3m5t8n",
    "responseStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "actor": {
      "type": "…",
      "id": "inv_01jd2q4z8kxw9v7r3m5t8n",
      "slug": "api-v3"
    },
    "trigger": "mention",
    "requiredCapability": "mentionable",
    "promptMarkdown": "Picking the auth refactor back up next sprint.",
    "authorUserId": "usr_01jd2q4z8kxw9v7r3m5t8n",
    "mentionedActorSlugs": [
      "api-v3"
    ],
    "claimToken": "clm_7f3kq9w2…",
    "claimExpiresAt": "2026-03-12T11:42:00.000Z",
    "runtimeSessionId": "runtimesession_01jd2q4z8kxw9v7r3m5t8n",
    "metadata": {},
    "context": {
      "kind": "user",
      "messages": [
        {
          "messageId": "msg_01jd2q4z8kxw9v7r3m5t8n",
          "role": "user",
          "authorId": "usr_01jd2q4z8kxw9v7r3m5t8n",
          "authorType": "user",
          "authorDisplayName": "Maya Reyes",
          "contentMarkdown": "Picking the auth refactor back up next sprint.",
          "createdAt": "2026-03-12T11:42:00.000Z"
        }
      ]
    },
    "sealedContext": {
      "callbackToken": "clm_7f3kq9w2…",
      "wraps": [
        {
          "keyGeneration": 1,
          "wrapEnc": "…",
          "wrapCt": "…"
        }
      ],
      "history": [
        {
          "ciphertext": "Picking the auth refactor back up next sprint.",
          "envelope": {
            "v": 1,
            "keyGeneration": 1,
            "iv": "…",
            "aad": "…"
          },
          "role": "user",
          "sequence": "412"
        }
      ],
      "prompt": {
        "ciphertext": "Picking the auth refactor back up next sprint.",
        "envelope": {
          "v": 1,
          "keyGeneration": 1,
          "iv": "…",
          "aad": "…"
        }
      },
      "reply": {
        "keyGeneration": 1,
        "senderId": "sender_01jd2q4z8kxw9v7r3m5t8n"
      },
      "trigger": {
        "messageId": "msg_01jd2q4z8kxw9v7r3m5t8n",
        "authorName": "…",
        "authorType": "user",
        "createdAt": "2026-03-12T11:42:00.000Z"
      }
    }
  }
}
```

### POST /api/v1/workspaces/{workspaceId}/bot-invocations/{invocationId}/renew

**Renew a claimed bot invocation**

Required scope: `bot-invocations:write`

#### Path parameters

- `invocationId` (string, required) — Invocation ID

#### Request body

- `instanceId` (string, required) — min length 1 · max length 128
- `claimToken` (string, required) — min length 1 · max length 256
- `claimTtlSeconds` (integer, required) — 15–300 · default 60

#### Response 200

- `data` (object, required)
  - `invocationId` (string, required)
  - `status` (string, required)
  - `claimExpiresAt` (string | null, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*POST /bot-invocations/{invocationId}/renew:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/bot-invocations/INVOCATION_ID/renew \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "instanceId": "string",
  "claimToken": "string",
  "claimTtlSeconds": 60
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "invocationId": "inv_01jd2q4z8kxw9v7r3m5t8n",
    "status": "available",
    "claimExpiresAt": "2026-03-12T11:42:00.000Z"
  }
}
```

### POST /api/v1/workspaces/{workspaceId}/bot-invocations/{invocationId}/steps

**Record a bot invocation trace step**

Required scope: `bot-invocations:write`

#### Path parameters

- `invocationId` (string, required) — Invocation ID

#### Request body

- `instanceId` (string, required) — min length 1 · max length 128
- `claimToken` (string, required) — min length 1 · max length 256
- `stepType` (string, required) — one of: context_received, thinking, reconsidering, steer, web_search, visit_page, workspace_search, research, github_access, linear_access, message_sent, message_edited, response, tool_call, tool_error, rate_limited, rate_limit_retry, turn_digest, model_escalated
- `content` (string, required) — min length 1 · max length 10000
- `statusText` (string) — max length 200
- `clientStepId` (string) — min length 1 · max length 128

#### Response 200

- `data` (object, required)
  - `invocationId` (string, required)
  - `sessionId` (string, required)
  - `stepId` (string, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*POST /bot-invocations/{invocationId}/steps:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/bot-invocations/INVOCATION_ID/steps \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "instanceId": "string",
  "claimToken": "string",
  "stepType": "context_received",
  "content": "string"
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "invocationId": "inv_01jd2q4z8kxw9v7r3m5t8n",
    "sessionId": "session_01jd2q4z8kxw9v7r3m5t8n",
    "stepId": "step_01jd2q4z8kxw9v7r3m5t8n"
  }
}
```

### POST /api/v1/workspaces/{workspaceId}/bot-invocations/{invocationId}/sealed-steps/started

**Open an in-flight sealed bot invocation trace step**

Sealed variant of the trace-step start, for an owner-granted E2E bot harness: the content is ciphertext the server never decrypts. Authenticated with the per-claim callback token in the X-Threa-Callback-Token header.

Required scope: `bot-invocations:write`

#### Path parameters

- `invocationId` (string, required) — Invocation ID

#### Request body

- `stepId` (string, required) — min length 1 · max length 128
- `stepType` (string, required) — one of: context_received, thinking, reconsidering, steer, web_search, visit_page, workspace_search, research, github_access, linear_access, message_sent, message_edited, response, tool_call, tool_error, rate_limited, rate_limit_retry, turn_digest, model_escalated
- `messageId` (string) — min length 1 · max length 128
- `ciphertext` (string) — base64 · min length 1
- `envelope` (object)
  - `v` (number, required)
  - `keyGeneration` (integer, required) — 0–9007199254740991
  - `iv` (string, required) — base64 · min length 1
  - `aad` (string, required) — base64 · min length 1

#### Response 200

- `data` (object, required)
  - `invocationId` (string, required)
  - `sessionId` (string, required)
  - `stepId` (string, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*POST /bot-invocations/{invocationId}/sealed-steps/started:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/bot-invocations/INVOCATION_ID/sealed-steps/started \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "stepId": "string",
  "stepType": "context_received"
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "invocationId": "inv_01jd2q4z8kxw9v7r3m5t8n",
    "sessionId": "session_01jd2q4z8kxw9v7r3m5t8n",
    "stepId": "step_01jd2q4z8kxw9v7r3m5t8n"
  }
}
```

### POST /api/v1/workspaces/{workspaceId}/bot-invocations/{invocationId}/sealed-steps

**Finalize a sealed bot invocation trace step**

Sealed variant of the trace-step finalize, for an owner-granted E2E bot harness: sets the sealed content + completion on the step opened at sealed-steps/started (or inserts a completed row if the start was dropped). Authenticated with the per-claim callback token in the X-Threa-Callback-Token header.

Required scope: `bot-invocations:write`

#### Path parameters

- `invocationId` (string, required) — Invocation ID

#### Request body

- `stepId` (string, required) — min length 1 · max length 128
- `stepType` (string, required) — one of: context_received, thinking, reconsidering, steer, web_search, visit_page, workspace_search, research, github_access, linear_access, message_sent, message_edited, response, tool_call, tool_error, rate_limited, rate_limit_retry, turn_digest, model_escalated
- `messageId` (string) — min length 1 · max length 128
- `ciphertext` (string, required) — base64 · min length 1
- `envelope` (object, required)
  - `v` (number, required)
  - `keyGeneration` (integer, required) — 0–9007199254740991
  - `iv` (string, required) — base64 · min length 1
  - `aad` (string, required) — base64 · min length 1
- `durationMs` (integer) — 0–9007199254740991

#### Response 200

- `data` (object, required)
  - `invocationId` (string, required)
  - `sessionId` (string, required)
  - `stepId` (string, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*POST /bot-invocations/{invocationId}/sealed-steps:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/bot-invocations/INVOCATION_ID/sealed-steps \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "stepId": "string",
  "stepType": "context_received",
  "ciphertext": "string",
  "envelope": {
    "v": 1,
    "keyGeneration": 0,
    "iv": "string",
    "aad": "string"
  }
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "invocationId": "inv_01jd2q4z8kxw9v7r3m5t8n",
    "sessionId": "session_01jd2q4z8kxw9v7r3m5t8n",
    "stepId": "step_01jd2q4z8kxw9v7r3m5t8n"
  }
}
```

### POST /api/v1/workspaces/{workspaceId}/bot-invocations/{invocationId}/sealed-messages

**Post a sealed interim message from an in-flight sealed bot invocation**

Sealed variant of a mid-turn bot message, for an owner-granted E2E bot harness: posts one sealed interim message (ciphertext the server never decrypts) into the claim's stream before the turn completes: progress notes, permission prompts, early acks. The client-minted messageId binds the seal AAD and dedupes retries. Authenticated with the per-claim callback token in the X-Threa-Callback-Token header.

Required scope: `bot-invocations:write`

#### Path parameters

- `invocationId` (string, required) — Invocation ID

#### Request body

- `messageId` (string, required) — min length 1 · max length 128
- `ciphertext` (string, required) — base64 · min length 1
- `envelope` (object, required)
  - `v` (number, required)
  - `keyGeneration` (integer, required) — 0–9007199254740991
  - `iv` (string, required) — base64 · min length 1
  - `aad` (string, required) — base64 · min length 1
- `attachmentIds` (string[])

#### Response 200

- `data` (object, required)
  - `messageId` (string, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*POST /bot-invocations/{invocationId}/sealed-messages:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/bot-invocations/INVOCATION_ID/sealed-messages \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "messageId": "string",
  "ciphertext": "string",
  "envelope": {
    "v": 1,
    "keyGeneration": 0,
    "iv": "string",
    "aad": "string"
  }
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "messageId": "msg_01jd2q4z8kxw9v7r3m5t8n"
  }
}
```

### POST /api/v1/workspaces/{workspaceId}/bot-invocations/{invocationId}/sealed-complete

**Complete a sealed bot invocation**

Sealed variant of the completion, for an owner-granted E2E bot harness: persists the turn's final sealed reply (ciphertext the server never decrypts) or noResponse, flips the claim, and finalizes the agent session. Authenticated with the per-claim callback token in the X-Threa-Callback-Token header.

Required scope: `bot-invocations:write`

#### Path parameters

- `invocationId` (string, required) — Invocation ID

#### Request body

- `reply` (object)
  - `messageId` (string, required) — min length 1 · max length 128
  - `ciphertext` (string, required) — base64 · min length 1
  - `envelope` (object, required)
    - `v` (number, required)
    - `keyGeneration` (integer, required) — 0–9007199254740991
    - `iv` (string, required) — base64 · min length 1
    - `aad` (string, required) — base64 · min length 1
  - `attachmentIds` (string[])
- `noResponse` (boolean)

#### Response 200

- `data` (object, required)
  - `invocationId` (string, required)
  - `sessionId` (string, required)
  - `messageId` (string | null, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*POST /bot-invocations/{invocationId}/sealed-complete:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/bot-invocations/INVOCATION_ID/sealed-complete \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "reply": {
    "messageId": "string",
    "ciphertext": "string",
    "envelope": {
      "v": 1,
      "keyGeneration": 0,
      "iv": "string",
      "aad": "string"
    }
  },
  "noResponse": false
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "invocationId": "inv_01jd2q4z8kxw9v7r3m5t8n",
    "sessionId": "session_01jd2q4z8kxw9v7r3m5t8n",
    "messageId": "msg_01jd2q4z8kxw9v7r3m5t8n"
  }
}
```

### POST /api/v1/workspaces/{workspaceId}/bot-invocations/{invocationId}/complete

**Complete a claimed bot invocation**

Required scope: `bot-invocations:write`

#### Path parameters

- `invocationId` (string, required) — Invocation ID

#### Request body

- `instanceId` (string, required) — min length 1 · max length 128
- `claimToken` (string, required) — min length 1 · max length 256
- `finalMessageMarkdown` (string) — min length 1 · max length 50000
- `noResponse` (boolean)
- `sources` (object[])
  - `type` (string) — one of: web, workspace, github
  - `title` (string, required) — min length 1 · max length 500
  - `url` (string, required) — min length 1 · max length 2000
  - `snippet` (string) — max length 2000
- `metadata` (object)
- `sealedReply` (object)
  - `messageId` (string, required) — min length 1 · max length 128
  - `ciphertext` (string, required) — base64 · min length 1
  - `envelope` (object, required)
    - `v` (number, required)
    - `keyGeneration` (integer, required) — 0–9007199254740991
    - `iv` (string, required) — base64 · min length 1
    - `aad` (string, required) — base64 · min length 1

#### Response 200

- `data` (object, required)
  - `invocationId` (string, required)
  - `message` (object | null, required)
- `slots` (object, required) — Hydration for cross-stream shared-message pointers in the returned messages, keyed by `shared:<messageId>`. Always present; empty when no message references a shared source.

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*POST /bot-invocations/{invocationId}/complete:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/bot-invocations/INVOCATION_ID/complete \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "instanceId": "string",
  "claimToken": "string"
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "invocationId": "inv_01jd2q4z8kxw9v7r3m5t8n",
    "message": {
      "id": "inv_01jd2q4z8kxw9v7r3m5t8n",
      "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
      "sequence": "412",
      "authorId": "usr_01jd2q4z8kxw9v7r3m5t8n",
      "authorType": "user",
      "authorDisplayName": "Maya Reyes",
      "content": "Picking the auth refactor back up next sprint.",
      "replyCount": 2,
      "threadStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
      "clientMessageId": "ci-deploy-2.4.1",
      "sentVia": "…",
      "metadata": {},
      "attachments": [
        {
          "id": "inv_01jd2q4z8kxw9v7r3m5t8n",
          "filename": "rotation-flow.png",
          "mimeType": "image/png",
          "sizeBytes": 48213,
          "processingStatus": "pending",
          "width": 1,
          "height": 1
        }
      ],
      "editedAt": "2026-03-12T11:42:00.000Z",
      "createdAt": "2026-03-12T11:42:00.000Z"
    }
  },
  "slots": {}
}
```

### POST /api/v1/workspaces/{workspaceId}/bot-invocations/{invocationId}/fail

**Fail a claimed bot invocation**

Required scope: `bot-invocations:write`

#### Path parameters

- `invocationId` (string, required) — Invocation ID

#### Request body

- `instanceId` (string, required) — min length 1 · max length 128
- `claimToken` (string, required) — min length 1 · max length 256
- `errorMessage` (string, required) — min length 1 · max length 1000

#### Response 200

- `data` (object, required)
  - `invocationId` (string, required)
  - `status` (string, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*POST /bot-invocations/{invocationId}/fail:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/bot-invocations/INVOCATION_ID/fail \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "instanceId": "string",
  "claimToken": "string",
  "errorMessage": "string"
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "invocationId": "inv_01jd2q4z8kxw9v7r3m5t8n",
    "status": "available"
  }
}
```

## Delegations

Close the loop on delegated tasks: your local agent lists the open queue, claims a task, reports progress, and completes it with a result posted back to the stream.

### GET /api/v1/workspaces/{workspaceId}/delegations

**List open delegations**

List delegated tasks that are open to claim, filtered to what the key can access: a user-scoped key sees streams its user can access, a workspace key sees the bot's channel grants.

Required scope: `delegations:read`

#### Query parameters

- `status` (string) — default "open"
- `since` (string) — date-time

#### Response 200

- `data` (object[], required)
  - `id` (string, required)
  - `streamId` (string, required)
  - `title` (string, required)
  - `status` (string, required) — one of: open, claimed, running, completed, failed, cancelled, expired
  - `claimedByLabel` (string)
  - `statusNote` (string)
  - `resultMessageId` (string)
  - `sourceConversationId` (string)
  - `createdAt` (string, required) — date-time
  - `statusChangedAt` (string, required) — date-time

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource

*GET /delegations:*

```bash
curl "https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/delegations" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

*Response 200 · example:*

```json
{
  "data": [
    {
      "id": "id_01jd2q4z8kxw9v7r3m5t8n",
      "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
      "title": "Auth refactor held pending token-rotation review",
      "status": "open",
      "claimedByLabel": "…",
      "statusNote": "available",
      "resultMessageId": "msg_01jd2q4z8kxw9v7r3m5t8n",
      "sourceConversationId": "conversation_01jd2q4z8kxw9v7r3m5t8n",
      "createdAt": "2026-03-12T11:42:00.000Z",
      "statusChangedAt": "2026-03-12T11:42:00.000Z"
    }
  ]
}
```

### POST /api/v1/workspaces/{workspaceId}/delegations/{delegationId}/claim

**Claim an open delegation**

Atomically claim an open delegation. Returns the brief, context refs, and the claim token (cleartext, exactly once; send it as X-Threa-Callback-Token on every later lifecycle call). A delegation that is no longer open returns 409.

Required scope: `delegations:write`

#### Path parameters

- `delegationId` (string, required) — Delegation ID (prefixed ULID)

#### Request body

- `claimedByLabel` (string, required) — min length 1 · max length 200
- `idempotencyKey` (string) — min length 8 · max length 128

#### Response 200

- `data` (object, required)
  - `id` (string, required)
  - `streamId` (string, required)
  - `title` (string, required)
  - `status` (string, required) — one of: open, claimed, running, completed, failed, cancelled, expired
  - `claimedByLabel` (string)
  - `statusNote` (string)
  - `resultMessageId` (string)
  - `sourceConversationId` (string)
  - `createdAt` (string, required) — date-time
  - `statusChangedAt` (string, required) — date-time
  - `brief` (string, required)
  - `contextRefs` (string[], required)
  - `claimToken` (string, required)
  - `claimExpiresAt` (string, required) — date-time

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found
-   `409` Conflict: the resource is not in the state the operation requires

*POST /delegations/{delegationId}/claim:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/delegations/DELEGATION_ID/claim \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "claimedByLabel": "string"
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "id": "id_01jd2q4z8kxw9v7r3m5t8n",
    "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "title": "Auth refactor held pending token-rotation review",
    "status": "open",
    "claimedByLabel": "…",
    "statusNote": "available",
    "resultMessageId": "msg_01jd2q4z8kxw9v7r3m5t8n",
    "sourceConversationId": "conversation_01jd2q4z8kxw9v7r3m5t8n",
    "createdAt": "2026-03-12T11:42:00.000Z",
    "statusChangedAt": "2026-03-12T11:42:00.000Z",
    "brief": "…",
    "contextRefs": [
      "Picking the auth refactor back up next sprint."
    ],
    "claimToken": "clm_7f3kq9w2…",
    "claimExpiresAt": "2026-03-12T11:42:00.000Z"
  }
}
```

### POST /api/v1/workspaces/{workspaceId}/delegations/{delegationId}/heartbeat

**Renew a delegation claim**

Push the claim's expiry forward while the local agent is still working. Liveness only; no status change on the card. Authenticated with the per-claim token in the X-Threa-Callback-Token header.

Required scope: `delegations:write`

#### Path parameters

- `delegationId` (string, required) — Delegation ID (prefixed ULID)

#### Response 200

- `data` (object, required)
  - `claimExpiresAt` (string, required) — date-time

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*POST /delegations/{delegationId}/heartbeat:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/delegations/DELEGATION_ID/heartbeat \
  -H "Authorization: Bearer YOUR_API_KEY"
```

*Response 200 · example:*

```json
{
  "data": {
    "claimExpiresAt": "2026-03-12T11:42:00.000Z"
  }
}
```

### POST /api/v1/workspaces/{workspaceId}/delegations/{delegationId}/status

**Report delegation progress**

Mark the delegation running and put a free-text progress note on its card (each report replaces the previous note; the claim TTL renews). Authenticated with the per-claim token in the X-Threa-Callback-Token header.

Required scope: `delegations:write`

#### Path parameters

- `delegationId` (string, required) — Delegation ID (prefixed ULID)

#### Request body

- `statusNote` (string) — min length 1 · max length 2000

#### Response 200

- `data` (object, required)
  - `id` (string, required)
  - `streamId` (string, required)
  - `title` (string, required)
  - `status` (string, required) — one of: open, claimed, running, completed, failed, cancelled, expired
  - `claimedByLabel` (string)
  - `statusNote` (string)
  - `resultMessageId` (string)
  - `sourceConversationId` (string)
  - `createdAt` (string, required) — date-time
  - `statusChangedAt` (string, required) — date-time

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*POST /delegations/{delegationId}/status:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/delegations/DELEGATION_ID/status \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "statusNote": "string"
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "id": "id_01jd2q4z8kxw9v7r3m5t8n",
    "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "title": "Auth refactor held pending token-rotation review",
    "status": "open",
    "claimedByLabel": "…",
    "statusNote": "available",
    "resultMessageId": "msg_01jd2q4z8kxw9v7r3m5t8n",
    "sourceConversationId": "conversation_01jd2q4z8kxw9v7r3m5t8n",
    "createdAt": "2026-03-12T11:42:00.000Z",
    "statusChangedAt": "2026-03-12T11:42:00.000Z"
  }
}
```

### POST /api/v1/workspaces/{workspaceId}/delegations/{delegationId}/complete

**Complete a delegation**

Complete the claimed delegation. When resultMarkdown is given, the result is posted in a thread anchored on the delegation card in the same transaction as the completion, authored as the key's user (with via-API provenance) for a user-scoped key, or as the bot for a workspace key. The response includes resultMessageId and resultThreadId. It enters the normal message pipeline, so workspace memory captures the outcome. Authenticated with the per-claim token in the X-Threa-Callback-Token header.

Required scope: `delegations:write`

#### Path parameters

- `delegationId` (string, required) — Delegation ID (prefixed ULID)

#### Request body

- `resultMarkdown` (string) — min length 1 · max length 50000
- `metadata` (object)

#### Response 200

- `data` (object, required)
  - `id` (string, required)
  - `streamId` (string, required)
  - `title` (string, required)
  - `status` (string, required) — one of: open, claimed, running, completed, failed, cancelled, expired
  - `claimedByLabel` (string)
  - `statusNote` (string)
  - `resultMessageId` (string)
  - `sourceConversationId` (string)
  - `createdAt` (string, required) — date-time
  - `statusChangedAt` (string, required) — date-time
  - `resultThreadId` (string)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*POST /delegations/{delegationId}/complete:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/delegations/DELEGATION_ID/complete \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "resultMarkdown": "string",
  "metadata": {}
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "id": "id_01jd2q4z8kxw9v7r3m5t8n",
    "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "title": "Auth refactor held pending token-rotation review",
    "status": "open",
    "claimedByLabel": "…",
    "statusNote": "available",
    "resultMessageId": "msg_01jd2q4z8kxw9v7r3m5t8n",
    "sourceConversationId": "conversation_01jd2q4z8kxw9v7r3m5t8n",
    "createdAt": "2026-03-12T11:42:00.000Z",
    "statusChangedAt": "2026-03-12T11:42:00.000Z",
    "resultThreadId": "resultthread_01jd2q4z8kxw9v7r3m5t8n"
  }
}
```

### POST /api/v1/workspaces/{workspaceId}/delegations/{delegationId}/fail

**Fail a delegation**

Mark the claimed delegation failed, recording why on its card. Authenticated with the per-claim token in the X-Threa-Callback-Token header.

Required scope: `delegations:write`

#### Path parameters

- `delegationId` (string, required) — Delegation ID (prefixed ULID)

#### Request body

- `errorMessage` (string, required) — min length 1 · max length 2000

#### Response 200

- `data` (object, required)
  - `id` (string, required)
  - `streamId` (string, required)
  - `title` (string, required)
  - `status` (string, required) — one of: open, claimed, running, completed, failed, cancelled, expired
  - `claimedByLabel` (string)
  - `statusNote` (string)
  - `resultMessageId` (string)
  - `sourceConversationId` (string)
  - `createdAt` (string, required) — date-time
  - `statusChangedAt` (string, required) — date-time

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*POST /delegations/{delegationId}/fail:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/delegations/DELEGATION_ID/fail \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "errorMessage": "string"
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "id": "id_01jd2q4z8kxw9v7r3m5t8n",
    "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
    "title": "Auth refactor held pending token-rotation review",
    "status": "open",
    "claimedByLabel": "…",
    "statusNote": "available",
    "resultMessageId": "msg_01jd2q4z8kxw9v7r3m5t8n",
    "sourceConversationId": "conversation_01jd2q4z8kxw9v7r3m5t8n",
    "createdAt": "2026-03-12T11:42:00.000Z",
    "statusChangedAt": "2026-03-12T11:42:00.000Z"
  }
}
```

### POST /api/v1/workspaces/{workspaceId}/delegations/{delegationId}/request-access

**Request access to a delegation's stream**

For a workspace (bot) key that received the delegation:available nudge but cannot claim it (no channel grant): file an access request that renders as a card in the delegation's stream for a member to approve or deny. Returns already\_granted (no card) when the bot already has access; otherwise the request is idempotent per (bot, stream). 404 for an unknown delegation id — the existence-hiding carve-out is scoped to ids the workspace bot plane already saw on the nudge. A user-scoped key gets 400 (USER\_KEY\_CANNOT\_REQUEST\_ACCESS): a user key's access follows its user, who should join the stream directly.

Required scope: `delegations:write`

#### Path parameters

- `delegationId` (string, required) — Delegation ID (prefixed ULID)

#### Request body

- `requestedByLabel` (string) — max length 200

#### Response 200

- `data` (object, required)
  - `requestId` (string)
  - `status` (string, required)

#### Status codes

-   `200` Successful response
-   `400` Validation error
-   `401` Missing or invalid API key
-   `403` Insufficient permissions or inaccessible resource
-   `404` Resource not found

*POST /delegations/{delegationId}/request-access:*

```bash
curl -X POST https://app.threa.io/api/v1/workspaces/YOUR_WORKSPACE_ID/delegations/DELEGATION_ID/request-access \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "requestedByLabel": "string"
}'
```

*Response 200 · example:*

```json
{
  "data": {
    "requestId": "request_01jd2q4z8kxw9v7r3m5t8n",
    "status": "available"
  }
}
```
