Threa Developers

    API reference.

    Every endpoint, generated from the OpenAPI spec 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. Every request authenticates with Authorization: Bearer <key>. Error shapes, paging, idempotency, and rate limits live in Operations.

    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.

    Scope none · any valid key

    Response 200

    dataobjectrequired

    Status codes

    • 200 Successful response
    • 400 Validation error
    • 401 Missing or invalid API key
    • 403 Insufficient permissions or inaccessible resource
    GET /me
    curl "/api/v1/workspaces//me" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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.

    Scope none · any valid key

    Query parameters

    traitsstring
    one of: mentionable, active-scratchpad

    Response 200

    dataobject[]required
    idstringrequired
    workspaceIdstringrequired
    traitsstring[]required
    slugstring | nullrequired
    namestringrequired
    descriptionstring | nullrequired
    avatarEmojistring | nullrequired
    avatarUrlstring | nullrequired
    archivedAtstring | nullrequired
    createdAtstringrequired
    date-time
    updatedAtstringrequired
    date-time
    typestringrequired
    ownerUserIdstringrequired
    readsAsOwnerbooleanrequired

    Status codes

    • 200 Successful response
    • 400 Validation error
    • 401 Missing or invalid API key
    • 403 Insufficient permissions or inaccessible resource
    GET /me/bots
    curl "/api/v1/workspaces//me/bots" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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",
          "readsAsOwner": true
        }
      ]
    }
    GET /api/v1/workspaces/{workspaceId}/me/e2e-key

    Get my encryption key, including the sealed private half

    For user-scoped keys: the calling user's active UIK. Unlike the bot-facing owner key route, this returns the passphrase-sealed private bundle too, so a client with nothing but an API key and the passphrase can recover the private key and read the user's own sealed streams. Derive the KEK with Argon2id over `kdfSalt`/`kdfParams`, then open `encryptedPrivateBundle`, laid out as `[version (1 byte) | iv (12 bytes) | AES-GCM ciphertext]`. Bot-scoped keys receive 403. 404 when the user has not set up encryption.

    Scope none · any valid key

    Response 200

    dataobjectrequired
    keyIdstringrequired
    publicKeystringrequired

    Base64 X25519 public key

    encryptedPrivateBundlestringrequired

    Base64 `[version | iv | AES-GCM ciphertext]` of the private key

    kdfSaltstringrequired

    Base64 Argon2id salt

    kdfParamsobjectrequired
    algorithmstringrequired
    mintegerrequired
    -9007199254740991–9007199254740991

    Memory cost in kibibytes

    tintegerrequired
    -9007199254740991–9007199254740991

    Iterations

    pintegerrequired
    -9007199254740991–9007199254740991

    Parallelism

    versionintegerrequired
    -9007199254740991–9007199254740991

    Argon2 version (19 = 0x13)

    createdAtstringrequired

    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 /me/e2e-key
    curl "/api/v1/workspaces//me/e2e-key" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "data": {
        "keyId": "key_01jd2q4z8kxw9v7r3m5t8n",
        "publicKey": "…",
        "encryptedPrivateBundle": "…",
        "kdfSalt": "…",
        "kdfParams": {
          "algorithm": "…",
          "m": 1,
          "t": 1,
          "p": 1,
          "version": 1
        },
        "createdAt": "…"
      }
    }

    Streams

    List and inspect accessible streams

    Recipes Notify a stream from CI →

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

    List the key wraps for an end-to-end-encrypted stream

    Every wrap of the stream's symmetric key, so a recipient can recover it and read the stream. A wrap is HPKE ciphertext addressed to one public key; open the one whose `recipientKeyId` you hold the private half of, then decrypt each message's `sealed.ciphertext` under the generation its envelope names. Threads inherit their root's key, so ask for the root. 400 when the stream is not encrypted.

    Scope streams:read

    Path parameters

    streamIdstringrequired

    Stream ID (prefixed ULID)

    Response 200

    dataobjectrequired
    currentKeyGenerationintegerrequired
    0–9007199254740991

    Generation new messages are sealed under

    ownerUserIdstringrequired
    wrapsobject[]required
    keyGenerationintegerrequired
    0–9007199254740991
    recipientKeyIdstringrequired
    recipientKindstringrequired
    one of: user, bot, enclave
    wrapEncstringrequired

    Base64 HPKE encapsulation

    wrapCtstringrequired

    Base64 HPKE-wrapped stream key

    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}/e2e/key-wraps
    curl "/api/v1/workspaces//streams/STREAM_ID/e2e/key-wraps" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "data": {
        "currentKeyGeneration": 1,
        "ownerUserId": "usr_01jd2q4z8kxw9v7r3m5t8n",
        "wraps": [
          {
            "keyGeneration": 1,
            "recipientKeyId": "recipientkey_01jd2q4z8kxw9v7r3m5t8n",
            "recipientKind": "user",
            "wrapEnc": "…",
            "wrapCt": "…"
          }
        ]
      }
    }
    GET /api/v1/workspaces/{workspaceId}/streams

    List streams

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

    Scope streams:read

    Query parameters

    typeanyrequired
    querystring
    afterstring
    limitinteger
    1–200 · default 50
    includeArchivedstring
    one of: true, false

    Response 200

    dataobject[]required
    idstringrequired
    typestringrequired
    one of: scratchpad, channel, dm, thread, system, aside
    displayNamestringrequired
    slugstring
    descriptionstring
    visibilitystringrequired
    memoryModestringrequired
    one of: auto, off

    GAM memory automation gate: 'auto' extracts memos, 'off' disables it

    parentStreamIdstring
    rootStreamIdstring
    anchorIdstring

    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.

    e2eEnabledboolean

    True when the stream is end-to-end encrypted. Its message bodies arrive as `sealed` ciphertext with an opaque `content` placeholder, and a plaintext send is rejected with E2E_STREAM_REQUIRES_CIPHERTEXT.

    createdAtstringrequired
    date-time
    archivedAtstring
    date-time
    hasMorebooleanrequired
    cursorstring | nullrequired

    Status codes

    • 200 Successful response
    • 400 Validation error
    • 401 Missing or invalid API key
    • 403 Insufficient permissions or inaccessible resource
    GET /streams
    curl "/api/v1/workspaces//streams?type=channel&limit=20" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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",
          "e2eEnabled": true,
          "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

    Scope streams:read

    Path parameters

    streamIdstringrequired

    Stream ID (prefixed ULID)

    Response 200

    dataobjectrequired
    idstringrequired
    typestringrequired
    one of: scratchpad, channel, dm, thread, system, aside
    displayNamestringrequired
    slugstring
    descriptionstring
    visibilitystringrequired
    memoryModestringrequired
    one of: auto, off

    GAM memory automation gate: 'auto' extracts memos, 'off' disables it

    parentStreamIdstring
    rootStreamIdstring
    anchorIdstring

    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.

    e2eEnabledboolean

    True when the stream is end-to-end encrypted. Its message bodies arrive as `sealed` ciphertext with an opaque `content` placeholder, and a plaintext send is rejected with E2E_STREAM_REQUIRES_CIPHERTEXT.

    createdAtstringrequired
    date-time
    archivedAtstring
    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}
    curl "/api/v1/workspaces//streams/STREAM_ID" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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",
        "e2eEnabled": true,
        "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.

    Scope streams:write

    Path parameters

    streamIdstringrequired

    Stream ID (prefixed ULID)

    Request body

    descriptionstringrequired
    max length 10000

    Response 200

    dataobjectrequired
    idstringrequired
    typestringrequired
    one of: scratchpad, channel, dm, thread, system, aside
    displayNamestringrequired
    slugstring
    descriptionstring
    visibilitystringrequired
    memoryModestringrequired
    one of: auto, off

    GAM memory automation gate: 'auto' extracts memos, 'off' disables it

    parentStreamIdstring
    rootStreamIdstring
    anchorIdstring

    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.

    e2eEnabledboolean

    True when the stream is end-to-end encrypted. Its message bodies arrive as `sealed` ciphertext with an opaque `content` placeholder, and a plaintext send is rejected with E2E_STREAM_REQUIRES_CIPHERTEXT.

    createdAtstringrequired
    date-time
    archivedAtstring
    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}
    curl -X PATCH /api/v1/workspaces//streams/STREAM_ID \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "description": "string"
    }'
    Response 200 · example
    {
      "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",
        "e2eEnabled": true,
        "createdAt": "2026-03-12T11:42:00.000Z",
        "archivedAt": "2026-03-12T11:42:00.000Z"
      }
    }
    POST /api/v1/workspaces/{workspaceId}/streams/{streamId}/archive

    Archive a stream

    Archive a stream — its scratchpads, channels and threads go read-only, and every thread beneath it is sealed with it. Open to the stream's creator and, for user-scoped keys, the creator of its access root; a workspace-scoped key archives only streams its own bot opened. Archiving an already-archived stream returns it unchanged.

    Scope streams:write

    Path parameters

    streamIdstringrequired

    Stream ID (prefixed ULID)

    Response 200

    dataobjectrequired
    idstringrequired
    typestringrequired
    one of: scratchpad, channel, dm, thread, system, aside
    displayNamestringrequired
    slugstring
    descriptionstring
    visibilitystringrequired
    memoryModestringrequired
    one of: auto, off

    GAM memory automation gate: 'auto' extracts memos, 'off' disables it

    parentStreamIdstring
    rootStreamIdstring
    anchorIdstring

    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.

    e2eEnabledboolean

    True when the stream is end-to-end encrypted. Its message bodies arrive as `sealed` ciphertext with an opaque `content` placeholder, and a plaintext send is rejected with E2E_STREAM_REQUIRES_CIPHERTEXT.

    createdAtstringrequired
    date-time
    archivedAtstring
    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 /streams/{streamId}/archive
    curl -X POST /api/v1/workspaces//streams/STREAM_ID/archive \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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",
        "e2eEnabled": true,
        "createdAt": "2026-03-12T11:42:00.000Z",
        "archivedAt": "2026-03-12T11:42:00.000Z"
      }
    }
    POST /api/v1/workspaces/{workspaceId}/streams/{streamId}/unarchive

    Unarchive a stream

    Reopen an archived stream. Same authority as archive. Clearing a stream's own flag does not lift an archived ancestor — the subtree stays sealed until that ancestor is reopened too. Unarchiving a live stream returns it unchanged.

    Scope streams:write

    Path parameters

    streamIdstringrequired

    Stream ID (prefixed ULID)

    Response 200

    dataobjectrequired
    idstringrequired
    typestringrequired
    one of: scratchpad, channel, dm, thread, system, aside
    displayNamestringrequired
    slugstring
    descriptionstring
    visibilitystringrequired
    memoryModestringrequired
    one of: auto, off

    GAM memory automation gate: 'auto' extracts memos, 'off' disables it

    parentStreamIdstring
    rootStreamIdstring
    anchorIdstring

    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.

    e2eEnabledboolean

    True when the stream is end-to-end encrypted. Its message bodies arrive as `sealed` ciphertext with an opaque `content` placeholder, and a plaintext send is rejected with E2E_STREAM_REQUIRES_CIPHERTEXT.

    createdAtstringrequired
    date-time
    archivedAtstring
    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 /streams/{streamId}/unarchive
    curl -X POST /api/v1/workspaces//streams/STREAM_ID/unarchive \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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",
        "e2eEnabled": true,
        "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

    Scope streams:read

    Path parameters

    streamIdstringrequired

    Stream ID (prefixed ULID)

    Query parameters

    afterstring
    limitinteger
    1–200 · default 50

    Response 200

    dataobject[]required
    userIdstringrequired
    namestringrequired
    slugstringrequired
    avatarUrlstring
    joinedAtstringrequired
    date-time
    hasMorebooleanrequired
    cursorstring | nullrequired

    Status codes

    • 200 Successful response
    • 400 Validation error
    • 401 Missing or invalid API key
    • 403 Insufficient permissions or inaccessible resource
    GET /streams/{streamId}/members
    curl "/api/v1/workspaces//streams/STREAM_ID/members" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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 →Mirror a search into your own tool →

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

    Search messages

    Full-text and optional semantic search across accessible streams.

    Scope messages:search

    Request body

    querystringrequired
    min length 1
    semanticbooleanrequired
    default false
    exactbooleanrequired
    default false
    streamsstring[]
    fromstring
    typestring[]
    beforestring
    date-time
    afterstring
    date-time
    limitintegerrequired
    1–50 · default 20

    Response 200

    dataobject[]required
    idstringrequired
    streamIdstringrequired
    sequencestringrequired

    Numeric sequence as string

    contentstringrequired
    authorIdstringrequired
    authorTypestringrequired
    one of: user, persona, system, bot
    authorDisplayNamestring
    replyCountintegerrequired
    -9007199254740991–9007199254740991
    metadataobjectrequired

    External references attached by the sender. Always present; empty when unset.

    editedAtstring
    date-time
    createdAtstringrequired
    date-time
    ranknumberrequired
    slotsobjectrequired

    Hydration for shared-message pointers in the returned messages, keyed by the pointer's reference: `shared:<messageId>` (legacy, current revision), `shared:<messageId>@<version>` (pinned revision) or `shared:<messageId>@<version>:<from>-<to>` (pinned span). 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
    curl -X POST /api/v1/workspaces//messages/search \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "query": "auth",
      "semantic": true,
      "exact": false,
      "limit": 5
    }'
    Response 200 · example
    {
      "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.

    Scope messages:read

    Path parameters

    streamIdstringrequired

    Stream ID (prefixed ULID)

    Query parameters

    beforestring
    afterstring
    limitinteger
    1–100 · default 50

    Response 200

    dataobject[]required
    idstringrequired
    streamIdstringrequired
    sequencestringrequired

    Numeric sequence as string

    authorIdstringrequired
    authorTypestringrequired
    one of: user, persona, system, bot
    authorDisplayNamestring
    contentstringrequired
    replyCountintegerrequired
    -9007199254740991–9007199254740991
    threadStreamIdstring
    clientMessageIdstring
    sentViastring

    Present when the message was sent through the API with a user or bot key; "api_key:<id>" names the key

    metadataobjectrequired

    External references attached by the sender. Always present; empty when unset.

    attachmentsobject[]
    idstringrequired
    filenamestringrequired
    mimeTypestringrequired
    sizeBytesintegerrequired
    -9007199254740991–9007199254740991
    processingStatusstring
    one of: pending, processing, completed, failed, skipped
    widthinteger
    -9007199254740991–9007199254740991
    heightinteger
    -9007199254740991–9007199254740991
    revisionintegerrequired
    ≤ 9007199254740991

    1 for the original body, +1 per edit

    editedAtstring
    date-time
    createdAtstringrequired
    date-time
    sealedobject

    Present only on messages in an end-to-end-encrypted stream. `content` is the placeholder the server stores in place of the body; this is the real one, which only a holder of a key the stream is wrapped to can open. Recover the stream key from GET /streams/{streamId}/e2e/key-wraps, then decrypt under the generation named in the envelope. Absent on the handful of messages sealed under the pre-stream-key scheme, which this wire does not describe.

    ciphertextstringrequired
    envelopeobjectrequired
    vnumberrequired
    keyGenerationintegerrequired
    0–9007199254740991
    ivstringrequired
    aadstringrequired
    hasMorebooleanrequired
    slotsobjectrequired

    Hydration for shared-message pointers in the returned messages, keyed by the pointer's reference: `shared:<messageId>` (legacy, current revision), `shared:<messageId>@<version>` (pinned revision) or `shared:<messageId>@<version>:<from>-<to>` (pinned span). 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
    curl "/api/v1/workspaces//streams/STREAM_ID/messages" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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
            }
          ],
          "revision": 1,
          "editedAt": "2026-03-12T11:42:00.000Z",
          "createdAt": "2026-03-12T11:42:00.000Z",
          "sealed": {
            "ciphertext": "Picking the auth refactor back up next sprint.",
            "envelope": {
              "v": 1,
              "keyGeneration": 1,
              "iv": "…",
              "aad": "…"
            }
          }
        }
      ],
      "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. In an end-to-end-encrypted stream send `sealed` in place of `content`: the body sealed under the stream key from GET /streams/{streamId}/e2e/key-wraps. Sending the wrong one of the two is a 400 (`E2E_STREAM_REQUIRES_CIPHERTEXT` / `E2E_PAYLOAD_REQUIRES_E2E_STREAM`); `conversation` is not supported for a sealed body.

    Scope messages:write

    Path parameters

    streamIdstringrequired

    Stream ID (prefixed ULID)

    Request body

    contentstring
    min length 1
    sealedobject
    ciphertextstringrequired
    min length 1 · max length 1000000
    envelopeobjectrequired
    vintegerrequired
    ≤ 9007199254740991
    keyGenerationintegerrequired
    0–9007199254740991
    ivstringrequired
    min length 1 · max length 64
    aadstringrequired
    min length 1 · max length 4096
    clientMessageIdstring
    max length 128
    metadataobject
    conversationobject

    Response 201

    dataobjectrequired
    idstringrequired
    streamIdstringrequired
    sequencestringrequired

    Numeric sequence as string

    authorIdstringrequired
    authorTypestringrequired
    one of: user, persona, system, bot
    authorDisplayNamestring
    contentstringrequired
    replyCountintegerrequired
    -9007199254740991–9007199254740991
    threadStreamIdstring
    clientMessageIdstring
    sentViastring

    Present when the message was sent through the API with a user or bot key; "api_key:<id>" names the key

    metadataobjectrequired

    External references attached by the sender. Always present; empty when unset.

    attachmentsobject[]
    idstringrequired
    filenamestringrequired
    mimeTypestringrequired
    sizeBytesintegerrequired
    -9007199254740991–9007199254740991
    processingStatusstring
    one of: pending, processing, completed, failed, skipped
    widthinteger
    -9007199254740991–9007199254740991
    heightinteger
    -9007199254740991–9007199254740991
    revisionintegerrequired
    ≤ 9007199254740991

    1 for the original body, +1 per edit

    editedAtstring
    date-time
    createdAtstringrequired
    date-time
    sealedobject

    Present only on messages in an end-to-end-encrypted stream. `content` is the placeholder the server stores in place of the body; this is the real one, which only a holder of a key the stream is wrapped to can open. Recover the stream key from GET /streams/{streamId}/e2e/key-wraps, then decrypt under the generation named in the envelope. Absent on the handful of messages sealed under the pre-stream-key scheme, which this wire does not describe.

    ciphertextstringrequired
    envelopeobjectrequired
    vnumberrequired
    keyGenerationintegerrequired
    0–9007199254740991
    ivstringrequired
    aadstringrequired
    conversationIdstring

    The conversation the message was assigned to; present when the request declared a `conversation`.

    slotsobjectrequired

    Hydration for shared-message pointers in the returned messages, keyed by the pointer's reference: `shared:<messageId>` (legacy, current revision), `shared:<messageId>@<version>` (pinned revision) or `shared:<messageId>@<version>:<from>-<to>` (pinned span). 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
    curl -X POST /api/v1/workspaces//streams/STREAM_ID/messages \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "content": "string",
      "sealed": {
        "ciphertext": "string",
        "envelope": {
          "v": 1,
          "keyGeneration": 0,
          "iv": "string",
          "aad": "string"
        }
      },
      "clientMessageId": "string"
    }'
    Response 201 · example
    {
      "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
          }
        ],
        "revision": 1,
        "editedAt": "2026-03-12T11:42:00.000Z",
        "createdAt": "2026-03-12T11:42:00.000Z",
        "sealed": {
          "ciphertext": "Picking the auth refactor back up next sprint.",
          "envelope": {
            "v": 1,
            "keyGeneration": 1,
            "iv": "…",
            "aad": "…"
          }
        }
      },
      "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?'.

    Scope messages:read

    Request body

    metadataobjectrequired
    streamIdstring
    min length 1
    limitintegerrequired
    1–100 · default 20

    Response 200

    dataobject[]required
    idstringrequired
    streamIdstringrequired
    sequencestringrequired

    Numeric sequence as string

    authorIdstringrequired
    authorTypestringrequired
    one of: user, persona, system, bot
    authorDisplayNamestring
    contentstringrequired
    replyCountintegerrequired
    -9007199254740991–9007199254740991
    threadStreamIdstring
    clientMessageIdstring
    sentViastring

    Present when the message was sent through the API with a user or bot key; "api_key:<id>" names the key

    metadataobjectrequired

    External references attached by the sender. Always present; empty when unset.

    attachmentsobject[]
    idstringrequired
    filenamestringrequired
    mimeTypestringrequired
    sizeBytesintegerrequired
    -9007199254740991–9007199254740991
    processingStatusstring
    one of: pending, processing, completed, failed, skipped
    widthinteger
    -9007199254740991–9007199254740991
    heightinteger
    -9007199254740991–9007199254740991
    revisionintegerrequired
    ≤ 9007199254740991

    1 for the original body, +1 per edit

    editedAtstring
    date-time
    createdAtstringrequired
    date-time
    sealedobject

    Present only on messages in an end-to-end-encrypted stream. `content` is the placeholder the server stores in place of the body; this is the real one, which only a holder of a key the stream is wrapped to can open. Recover the stream key from GET /streams/{streamId}/e2e/key-wraps, then decrypt under the generation named in the envelope. Absent on the handful of messages sealed under the pre-stream-key scheme, which this wire does not describe.

    ciphertextstringrequired
    envelopeobjectrequired
    vnumberrequired
    keyGenerationintegerrequired
    0–9007199254740991
    ivstringrequired
    aadstringrequired
    slotsobjectrequired

    Hydration for shared-message pointers in the returned messages, keyed by the pointer's reference: `shared:<messageId>` (legacy, current revision), `shared:<messageId>@<version>` (pinned revision) or `shared:<messageId>@<version>:<from>-<to>` (pinned span). 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
    curl -X POST /api/v1/workspaces//messages/find-by-metadata \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "metadata": {},
      "limit": 20
    }'
    Response 200 · example
    {
      "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
            }
          ],
          "revision": 1,
          "editedAt": "2026-03-12T11:42:00.000Z",
          "createdAt": "2026-03-12T11:42:00.000Z",
          "sealed": {
            "ciphertext": "Picking the auth refactor back up next sprint.",
            "envelope": {
              "v": 1,
              "keyGeneration": 1,
              "iv": "…",
              "aad": "…"
            }
          }
        }
      ],
      "slots": {}
    }
    PATCH /api/v1/workspaces/{workspaceId}/messages/{messageId}

    Update a message

    Update a message you previously sent via API.

    Scope messages:write

    Path parameters

    messageIdstringrequired

    Message ID (prefixed ULID)

    Request body

    contentstringrequired
    min length 1

    Response 200

    dataobjectrequired
    idstringrequired
    streamIdstringrequired
    sequencestringrequired

    Numeric sequence as string

    authorIdstringrequired
    authorTypestringrequired
    one of: user, persona, system, bot
    authorDisplayNamestring
    contentstringrequired
    replyCountintegerrequired
    -9007199254740991–9007199254740991
    threadStreamIdstring
    clientMessageIdstring
    sentViastring

    Present when the message was sent through the API with a user or bot key; "api_key:<id>" names the key

    metadataobjectrequired

    External references attached by the sender. Always present; empty when unset.

    attachmentsobject[]
    idstringrequired
    filenamestringrequired
    mimeTypestringrequired
    sizeBytesintegerrequired
    -9007199254740991–9007199254740991
    processingStatusstring
    one of: pending, processing, completed, failed, skipped
    widthinteger
    -9007199254740991–9007199254740991
    heightinteger
    -9007199254740991–9007199254740991
    revisionintegerrequired
    ≤ 9007199254740991

    1 for the original body, +1 per edit

    editedAtstring
    date-time
    createdAtstringrequired
    date-time
    sealedobject

    Present only on messages in an end-to-end-encrypted stream. `content` is the placeholder the server stores in place of the body; this is the real one, which only a holder of a key the stream is wrapped to can open. Recover the stream key from GET /streams/{streamId}/e2e/key-wraps, then decrypt under the generation named in the envelope. Absent on the handful of messages sealed under the pre-stream-key scheme, which this wire does not describe.

    ciphertextstringrequired
    envelopeobjectrequired
    vnumberrequired
    keyGenerationintegerrequired
    0–9007199254740991
    ivstringrequired
    aadstringrequired
    slotsobjectrequired

    Hydration for shared-message pointers in the returned messages, keyed by the pointer's reference: `shared:<messageId>` (legacy, current revision), `shared:<messageId>@<version>` (pinned revision) or `shared:<messageId>@<version>:<from>-<to>` (pinned span). 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}
    curl -X PATCH /api/v1/workspaces//messages/MESSAGE_ID \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "content": "string"
    }'
    Response 200 · example
    {
      "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
          }
        ],
        "revision": 1,
        "editedAt": "2026-03-12T11:42:00.000Z",
        "createdAt": "2026-03-12T11:42:00.000Z",
        "sealed": {
          "ciphertext": "Picking the auth refactor back up next sprint.",
          "envelope": {
            "v": 1,
            "keyGeneration": 1,
            "iv": "…",
            "aad": "…"
          }
        }
      },
      "slots": {}
    }
    DELETE /api/v1/workspaces/{workspaceId}/messages/{messageId}

    Delete a message

    Delete a message you previously sent via API.

    Scope messages:write

    Path parameters

    messageIdstringrequired

    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}
    curl -X DELETE /api/v1/workspaces//messages/MESSAGE_ID \
      -H "Authorization: Bearer "

    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`.

    Scope messages:read

    Query parameters

    streamIdstring
    statusstring
    one of: active, stalled, resolved
    afterstring
    limitinteger
    1–100 · default 50

    Response 200

    dataobject[]required
    idstringrequired
    streamIdstringrequired

    Anchor stream the conversation lives in (may be a thread)

    rootStreamIdstringrequired

    Effective root of the anchor — the stream whose access governs the conversation

    topicSummarystring | nullrequired
    summarystring | nullrequired
    statusstringrequired
    one of: active, stalled, resolved
    messageCountintegerrequired
    -9007199254740991–9007199254740991

    Number of primary member messages

    participantIdsstring[]required

    Distinct author ids of the member messages

    lastActivityAtstringrequired
    date-time
    createdAtstringrequired
    date-time
    updatedAtstringrequired
    date-time
    hasMorebooleanrequired
    cursorstring | nullrequired

    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
    curl "/api/v1/workspaces//conversations" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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

    Scope messages:read

    Path parameters

    conversationIdstringrequired

    Conversation ID (prefixed ULID)

    Response 200

    dataobjectrequired
    idstringrequired
    streamIdstringrequired

    Anchor stream the conversation lives in (may be a thread)

    rootStreamIdstringrequired

    Effective root of the anchor — the stream whose access governs the conversation

    topicSummarystring | nullrequired
    summarystring | nullrequired
    statusstringrequired
    one of: active, stalled, resolved
    messageCountintegerrequired
    -9007199254740991–9007199254740991

    Number of primary member messages

    participantIdsstring[]required

    Distinct author ids of the member messages

    lastActivityAtstringrequired
    date-time
    createdAtstringrequired
    date-time
    updatedAtstringrequired
    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}
    curl "/api/v1/workspaces//conversations/CONVERSATION_ID" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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`.

    Scope messages:read

    Path parameters

    conversationIdstringrequired

    Conversation ID (prefixed ULID)

    Query parameters

    afterstring
    limitinteger
    1–100 · default 50

    Response 200

    dataobject[]required
    idstringrequired
    streamIdstringrequired
    sequencestringrequired

    Numeric sequence as string

    authorIdstringrequired
    authorTypestringrequired
    one of: user, persona, system, bot
    authorDisplayNamestring
    contentstringrequired
    replyCountintegerrequired
    -9007199254740991–9007199254740991
    threadStreamIdstring
    clientMessageIdstring
    sentViastring

    Present when the message was sent through the API with a user or bot key; "api_key:<id>" names the key

    metadataobjectrequired

    External references attached by the sender. Always present; empty when unset.

    attachmentsobject[]
    idstringrequired
    filenamestringrequired
    mimeTypestringrequired
    sizeBytesintegerrequired
    -9007199254740991–9007199254740991
    processingStatusstring
    one of: pending, processing, completed, failed, skipped
    widthinteger
    -9007199254740991–9007199254740991
    heightinteger
    -9007199254740991–9007199254740991
    revisionintegerrequired
    ≤ 9007199254740991

    1 for the original body, +1 per edit

    editedAtstring
    date-time
    createdAtstringrequired
    date-time
    sealedobject

    Present only on messages in an end-to-end-encrypted stream. `content` is the placeholder the server stores in place of the body; this is the real one, which only a holder of a key the stream is wrapped to can open. Recover the stream key from GET /streams/{streamId}/e2e/key-wraps, then decrypt under the generation named in the envelope. Absent on the handful of messages sealed under the pre-stream-key scheme, which this wire does not describe.

    ciphertextstringrequired
    envelopeobjectrequired
    vnumberrequired
    keyGenerationintegerrequired
    0–9007199254740991
    ivstringrequired
    aadstringrequired
    hasMorebooleanrequired
    cursorstring | nullrequired
    slotsobjectrequired

    Hydration for shared-message pointers in the returned messages, keyed by the pointer's reference: `shared:<messageId>` (legacy, current revision), `shared:<messageId>@<version>` (pinned revision) or `shared:<messageId>@<version>:<from>-<to>` (pinned span). 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
    curl "/api/v1/workspaces//conversations/CONVERSATION_ID/messages" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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
            }
          ],
          "revision": 1,
          "editedAt": "2026-03-12T11:42:00.000Z",
          "createdAt": "2026-03-12T11:42:00.000Z",
          "sealed": {
            "ciphertext": "Picking the auth refactor back up next sprint.",
            "envelope": {
              "v": 1,
              "keyGeneration": 1,
              "iv": "…",
              "aad": "…"
            }
          }
        }
      ],
      "hasMore": false,
      "cursor": null,
      "slots": {}
    }

    Memos

    Search preserved workspace knowledge and inspect memo provenance

    Recipes Find out what was decided, and why →

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

    Search memos

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

    Scope memos:read

    Request body

    querystringrequired
    default ""
    exactboolean
    streamsstring[]
    memoTypestring[]
    knowledgeTypestring[]
    tagsstring[]
    scopestring
    one of: user, stream, workspace
    beforestring
    date-time
    afterstring
    date-time
    limitintegerrequired
    1–100 · default 20

    Response 200

    dataobject[]required
    memoobjectrequired
    idstringrequired
    workspaceIdstringrequired
    memoTypestringrequired
    one of: message, conversation
    sourceMessageIdstring | nullrequired
    sourceConversationIdstring | nullrequired
    titlestringrequired
    abstractstringrequired
    keyPointsstring[]required
    sourceMessageIdsstring[]required
    participantIdsstring[]required
    knowledgeTypestringrequired
    one of: decision, learning, procedure, context, reference
    tagsstring[]required
    parentMemoIdstring | nullrequired
    statusstringrequired
    versionintegerrequired
    -9007199254740991–9007199254740991
    revisionReasonstring | nullrequired
    authoredByKindstringrequired
    one of: pipeline, agent
    sourceSessionIdstring | nullrequired
    scopestringrequired
    one of: user, stream, workspace
    scopeUserIdstring | nullrequired
    createdAtstringrequired
    date-time
    updatedAtstringrequired
    date-time
    archivedAtstring | nullrequired
    distancenumberrequired
    sourceStreamobject | nullrequired
    rootStreamobject | nullrequired

    Status codes

    • 200 Successful response
    • 400 Validation error
    • 401 Missing or invalid API key
    • 403 Insufficient permissions or inaccessible resource
    POST /memos/search
    curl -X POST /api/v1/workspaces//memos/search \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "query": "auth",
      "limit": 5
    }'
    Response 200 · example
    {
      "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.

    Scope memos:read

    Path parameters

    memoIdstringrequired

    Memo ID (prefixed ULID)

    Response 200

    dataobjectrequired
    memoobjectrequired
    idstringrequired
    workspaceIdstringrequired
    memoTypestringrequired
    one of: message, conversation
    sourceMessageIdstring | nullrequired
    sourceConversationIdstring | nullrequired
    titlestringrequired
    abstractstringrequired
    keyPointsstring[]required
    sourceMessageIdsstring[]required
    participantIdsstring[]required
    knowledgeTypestringrequired
    one of: decision, learning, procedure, context, reference
    tagsstring[]required
    parentMemoIdstring | nullrequired
    statusstringrequired
    versionintegerrequired
    -9007199254740991–9007199254740991
    revisionReasonstring | nullrequired
    authoredByKindstringrequired
    one of: pipeline, agent
    sourceSessionIdstring | nullrequired
    scopestringrequired
    one of: user, stream, workspace
    scopeUserIdstring | nullrequired
    createdAtstringrequired
    date-time
    updatedAtstringrequired
    date-time
    archivedAtstring | nullrequired
    distancenumberrequired
    sourceStreamobject | nullrequired
    rootStreamobject | nullrequired
    sourceMessagesobject[]required
    idstringrequired
    streamIdstringrequired
    streamNamestringrequired
    authorIdstringrequired
    authorTypestringrequired
    one of: user, persona, system, bot
    authorNamestringrequired
    contentstringrequired
    createdAtstringrequired
    date-time
    successorMemoIdstring | nullrequired
    capturedByPersonaNamestring | nullrequired

    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}
    curl "/api/v1/workspaces//memos/MEMO_ID" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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.

    Scope attachments:write

    Response 201

    dataobjectrequired
    idstringrequired
    filenamestringrequired
    mimeTypestringrequired
    sizeBytesintegerrequired
    -9007199254740991–9007199254740991
    processingStatusstringrequired
    one of: pending, processing, completed, failed, skipped
    createdAtstringrequired
    date-time

    Status codes

    • 201 Successful response
    • 400 Validation error
    • 401 Missing or invalid API key
    • 403 Insufficient permissions or inaccessible resource
    POST /attachments
    curl -X POST /api/v1/workspaces//attachments \
      -H "Authorization: Bearer "
    Response 201 · example
    {
      "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.

    Scope attachments:read

    Request body

    querystring
    min length 1
    streamsstring[]
    contentTypesstring[]
    limitintegerrequired
    1–50 · default 20

    Response 200

    dataobject[]required
    idstringrequired
    filenamestringrequired
    mimeTypestringrequired
    contentTypestring | nullrequired
    summarystring | nullrequired
    streamIdstring
    messageIdstring
    createdAtstringrequired
    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
    curl -X POST /api/v1/workspaces//attachments/search \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "query": "auth",
      "limit": 5
    }'
    Response 200 · example
    {
      "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.

    Scope attachments:read

    Path parameters

    attachmentIdstringrequired

    Attachment ID (prefixed ULID)

    Response 200

    dataobjectrequired
    idstringrequired
    filenamestringrequired
    mimeTypestringrequired
    sizeBytesintegerrequired
    -9007199254740991–9007199254740991
    processingStatusstringrequired
    one of: pending, processing, completed, failed, skipped
    createdAtstringrequired
    date-time
    extractionobject | nullrequired

    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}
    curl "/api/v1/workspaces//attachments/ATTACHMENT_ID" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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.

    Scope attachments:read

    Path parameters

    attachmentIdstringrequired

    Attachment ID (prefixed ULID)

    Response 200

    dataobjectrequired
    urlstringrequired
    uri
    expiresInintegerrequired
    -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
    curl "/api/v1/workspaces//attachments/ATTACHMENT_ID/url" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "data": {
        "url": "https://files.threa.io/attachments/rotation-flow.png",
        "expiresIn": 1
      }
    }
    GET /api/v1/workspaces/{workspaceId}/attachments/{attachmentId}/content

    Download an attachment

    Stream the bytes of an accessible attachment, with its stored content type and filename.

    Scope attachments:read

    Path parameters

    attachmentIdstringrequired

    Attachment ID (prefixed ULID)

    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}/content
    curl "/api/v1/workspaces//attachments/ATTACHMENT_ID/content" \
      -H "Authorization: Bearer "

    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.

    Scope users:read

    Query parameters

    querystring
    afterstring
    limitinteger
    1–200 · default 50

    Response 200

    dataobject[]required
    idstringrequired
    namestringrequired
    slugstringrequired
    emailstringrequired
    avatarUrlstring
    rolestringrequired
    hasMorebooleanrequired
    cursorstring | nullrequired

    Status codes

    • 200 Successful response
    • 400 Validation error
    • 401 Missing or invalid API key
    • 403 Insufficient permissions or inaccessible resource
    GET /users
    curl "/api/v1/workspaces//users?limit=20" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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.

    Scope labels:read

    Response 200

    dataobjectrequired
    labelsobject[]required
    idstringrequired
    workspaceIdstringrequired
    creatorActorTypestringrequired
    one of: user, bot
    creatorActorIdstringrequired
    namestringrequired
    slugstringrequired
    colorstringrequired
    emojistring | nullrequired
    descriptionstring | nullrequired
    createdAtstringrequired
    date-time
    updatedAtstringrequired
    date-time
    archivedAtstring | nullrequired
    assignmentsobject[]required
    labelIdstringrequired
    resourceTypestringrequired
    one of: stream, message
    resourceIdstringrequired
    actorTypestringrequired
    one of: user, bot
    actorIdstringrequired
    workspaceIdstringrequired
    assignedAtstringrequired
    date-time

    Status codes

    • 200 Successful response
    • 400 Validation error
    • 401 Missing or invalid API key
    • 403 Insufficient permissions or inaccessible resource
    GET /labels
    curl "/api/v1/workspaces//labels" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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.

    Scope labels:write

    Request body

    namestringrequired
    min length 1 · max length 100
    colorstring
    emojistring | null
    descriptionstring | null

    Response 201

    dataobjectrequired
    idstringrequired
    workspaceIdstringrequired
    creatorActorTypestringrequired
    one of: user, bot
    creatorActorIdstringrequired
    namestringrequired
    slugstringrequired
    colorstringrequired
    emojistring | nullrequired
    descriptionstring | nullrequired
    createdAtstringrequired
    date-time
    updatedAtstringrequired
    date-time
    archivedAtstring | nullrequired

    Status codes

    • 201 Successful response
    • 400 Validation error
    • 401 Missing or invalid API key
    • 403 Insufficient permissions or inaccessible resource
    POST /labels
    curl -X POST /api/v1/workspaces//labels \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "name": "string"
    }'
    Response 201 · example
    {
      "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.

    Scope labels:write

    Request body

    namestringrequired
    min length 1 · max length 100
    colorstring
    emojistring | null
    descriptionstring | null
    resourceTypestringrequired
    one of: stream, message
    resourceIdstringrequired
    min length 1 · max length 64

    Response 201

    dataobjectrequired
    labelobjectrequired
    idstringrequired
    workspaceIdstringrequired
    creatorActorTypestringrequired
    one of: user, bot
    creatorActorIdstringrequired
    namestringrequired
    slugstringrequired
    colorstringrequired
    emojistring | nullrequired
    descriptionstring | nullrequired
    createdAtstringrequired
    date-time
    updatedAtstringrequired
    date-time
    archivedAtstring | nullrequired
    assignmentobjectrequired
    labelIdstringrequired
    resourceTypestringrequired
    one of: stream, message
    resourceIdstringrequired
    actorTypestringrequired
    one of: user, bot
    actorIdstringrequired
    workspaceIdstringrequired
    assignedAtstringrequired
    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
    curl -X POST /api/v1/workspaces//labels/assignments \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "name": "string",
      "resourceType": "stream",
      "resourceId": "string"
    }'
    Response 201 · example
    {
      "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.

    Scope labels:write

    Query parameters

    namestringrequired
    min length 1 · max length 100
    resourceTypestringrequired
    one of: stream, message
    resourceIdstringrequired
    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
    curl -X DELETE /api/v1/workspaces//labels/assignments?name=NAME&resourceType=stream&resourceId=RESOURCEID \
      -H "Authorization: Bearer "
    PATCH /api/v1/workspaces/{workspaceId}/labels/{labelId}

    Update a label

    Update a label the key actor created.

    Scope labels:write

    Path parameters

    labelIdstringrequired

    Label ID (prefixed ULID)

    Request body

    namestring
    min length 1 · max length 100
    colorstring
    emojistring | null
    descriptionstring | null

    Response 200

    dataobjectrequired
    idstringrequired
    workspaceIdstringrequired
    creatorActorTypestringrequired
    one of: user, bot
    creatorActorIdstringrequired
    namestringrequired
    slugstringrequired
    colorstringrequired
    emojistring | nullrequired
    descriptionstring | nullrequired
    createdAtstringrequired
    date-time
    updatedAtstringrequired
    date-time
    archivedAtstring | nullrequired

    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}
    curl -X PATCH /api/v1/workspaces//labels/LABEL_ID \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "name": "string",
      "color": "string",
      "emoji": null
    }'
    Response 200 · example
    {
      "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.

    Scope labels:write

    Path parameters

    labelIdstringrequired

    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}
    curl -X DELETE /api/v1/workspaces//labels/LABEL_ID \
      -H "Authorization: Bearer "

    Bot runtimes

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

    Recipes Connect your local agent →

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

    Heartbeat bot runtime presence

    Scope bot-runtime:write

    Request body

    runtimeKindstringrequired
    one of: pi-local, hermes, openclaw, claude-code-channel, custom
    instanceIdstringrequired
    min length 1 · max length 128
    runtimeSessionIdstring
    min length 1 · max length 256
    displayNamestring
    max length 100
    statusstringrequired
    one of: available, busy, offline, error
    acceptingInvocationsbooleanrequired
    capabilitiesobjectrequired
    default {}
    manifestobject | null
    statusTextstring
    max length 200
    publicKeystring
    min length 44 · max length 44
    publicKeyIdstring
    min length 1 · max length 128
    e2eKeysobject[]
    keyIdstringrequired
    min length 1 · max length 128
    publicKeystringrequired
    min length 44 · max length 44
    streamIdstring
    min length 1 · max length 64

    Response 200

    dataobjectrequired
    idstringrequired
    workspaceIdstringrequired
    botIdstringrequired
    runtimeKindstringrequired
    one of: pi-local, hermes, openclaw, claude-code-channel, custom
    instanceIdstringrequired
    displayNamestring | nullrequired
    statusstringrequired
    one of: available, busy, offline, error
    acceptingInvocationsbooleanrequired
    capabilitiesobjectrequired
    statusTextstring | nullrequired
    lastSeenAtstringrequired
    date-time
    createdAtstringrequired
    date-time
    updatedAtstringrequired
    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
    curl -X POST /api/v1/workspaces//bot-runtime/presence \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "runtimeKind": "pi-local",
      "instanceId": "string",
      "status": "available",
      "acceptingInvocations": false,
      "capabilities": {}
    }'
    Response 200 · example
    {
      "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

    Creates a fresh scratchpad session by default. Pass `attachTo` to link the session to a new thread under an existing scratchpad the bot already has access to instead. An identity that is already linked resumes its existing link in either mode; compare the returned `rootStreamId` and `activeStreamId` with the request to tell the two apart.

    Scope bot-runtime:write

    Request body

    runtimeKindstringrequired
    one of: pi-local, claude-code-channel, hermes, custom
    instanceIdstringrequired
    min length 1 · max length 128
    runtimeSessionIdstringrequired
    min length 1 · max length 256
    displayNamestringrequired
    min length 1 · max length 100
    localCwdstring
    max length 1000
    memoryModestring
    one of: auto, off
    labelNamestring
    min length 1 · max length 100
    descriptionstring
    max length 10000
    e2eobject
    ownerKeyIdstringrequired
    min length 1 · max length 128
    ifArchivedstring
    one of: wait, replace
    ifMissingstring
    one of: create, error
    attachToobject
    rootStreamIdstringrequired
    min length 1
    anchorIdstringrequired
    min length 1

    Response 200

    dataobjectrequired
    linkIdstringrequired
    rootStreamIdstringrequired
    activeStreamIdstringrequired
    runtimeSessionIdstringrequired
    streamUrlPathstringrequired
    e2eEnabledboolean

    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 /bot-runtime/sessions
    curl -X POST /api/v1/workspaces//bot-runtime/sessions \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "runtimeKind": "pi-local",
      "instanceId": "string",
      "runtimeSessionId": "string",
      "displayName": "string"
    }'
    Response 200 · example
    {
      "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.

    Scope bot-runtime:write

    Response 200

    dataobjectrequired
    keyIdstringrequired
    publicKeystringrequired

    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
    curl "/api/v1/workspaces//bot-runtime/owner-e2e-key" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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.

    Scope bot-runtime:write

    Path parameters

    streamIdstringrequired

    Stream ID

    Request body

    keyGenerationintegerrequired
    0–9007199254740991
    wrapsobject[]required
    recipientKindstringrequired
    one of: user, bot
    recipientKeyIdstringrequired
    min length 1 · max length 128
    wrapEncstringrequired
    base64 · min length 1
    wrapCtstringrequired
    base64 · min length 1

    Response 200

    dataobjectrequired
    storedintegerrequired
    -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
    curl -X POST /api/v1/workspaces//streams/STREAM_ID/e2e/key-wraps \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "keyGeneration": 0,
      "wraps": [
        {
          "recipientKind": "user",
          "recipientKeyId": "string",
          "wrapEnc": "string",
          "wrapCt": "string"
        }
      ]
    }'
    Response 200 · example
    {
      "data": {
        "stored": 1
      }
    }
    POST /api/v1/workspaces/{workspaceId}/bot-runtime/sessions/rename

    Rename the scratchpad linked to a bot runtime session

    Scope bot-runtime:write

    Request body

    instanceIdstringrequired
    min length 1 · max length 128
    runtimeSessionIdstringrequired
    min length 1 · max length 256
    displayNamestringrequired
    min length 1 · max length 100

    Response 200

    dataobjectrequired
    linkIdstringrequired
    rootStreamIdstringrequired
    activeStreamIdstringrequired
    runtimeSessionIdstringrequired
    streamUrlPathstringrequired
    e2eEnabledboolean
    displayNamestringrequired

    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
    curl -X POST /api/v1/workspaces//bot-runtime/sessions/rename \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "instanceId": "string",
      "runtimeSessionId": "string",
      "displayName": "string"
    }'
    Response 200 · example
    {
      "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

    Scope bot-runtime:write

    Request body

    linkIdstringrequired
    min length 1 · max length 128
    instanceIdstringrequired
    min length 1 · max length 128
    runtimeSessionIdstringrequired
    min length 1 · max length 256
    newInstanceIdstringrequired
    min length 1 · max length 128

    Response 200

    dataobjectrequired
    linkIdstringrequired
    rootStreamIdstringrequired
    activeStreamIdstringrequired
    runtimeSessionIdstringrequired
    streamUrlPathstringrequired
    e2eEnabledboolean

    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
    curl -X POST /api/v1/workspaces//bot-runtime/sessions/rebind \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "linkId": "string",
      "instanceId": "string",
      "runtimeSessionId": "string",
      "newInstanceId": "string"
    }'
    Response 200 · example
    {
      "data": {
        "linkId": "link_01jd2q4z8kxw9v7r3m5t8n",
        "rootStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
        "activeStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
        "runtimeSessionId": "runtimesession_01jd2q4z8kxw9v7r3m5t8n",
        "streamUrlPath": "https://files.threa.io/attachments/rotation-flow.png",
        "e2eEnabled": true
      }
    }
    POST /api/v1/workspaces/{workspaceId}/bot-runtime/sessions/end

    End the runtime session link on purpose

    Ends the link without archiving any stream and frees the runtime identity for reuse. Cancels any pending invocation still routed at this runtime session, except the one named by `exceptInvocationId` so the command that ends the link can finish reporting on it.

    Scope bot-runtime:write

    Request body

    instanceIdstringrequired
    min length 1 · max length 128
    runtimeSessionIdstringrequired
    min length 1 · max length 256
    exceptInvocationIdstring
    min length 1 · max length 128

    Response 200

    dataobjectrequired
    linkIdstringrequired
    rootStreamIdstringrequired
    activeStreamIdstringrequired
    statusstringrequired

    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/end
    curl -X POST /api/v1/workspaces//bot-runtime/sessions/end \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "instanceId": "string",
      "runtimeSessionId": "string"
    }'
    Response 200 · example
    {
      "data": {
        "linkId": "link_01jd2q4z8kxw9v7r3m5t8n",
        "rootStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
        "activeStreamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
        "status": "available"
      }
    }
    POST /api/v1/workspaces/{workspaceId}/bot-runtime/sessions/brief

    Brief the linked runtime session with a prompt

    Creates the invocation that delivers a prompt as a turn to the linked runtime session. The turn is sourced from the thread's anchor message and answered into the thread, so the brief writes no message of its own. That anchor is the brief's identity: repeating a brief with the same prompt returns the invocation already in flight, and a different prompt for the same anchor is refused with 409.

    Scope bot-runtime:write

    Request body

    instanceIdstringrequired
    min length 1 · max length 128
    runtimeSessionIdstringrequired
    min length 1 · max length 256
    contentstringrequired
    min length 1 · max length 50000

    Response 201

    dataobjectrequired
    invocationIdstringrequired
    streamIdstringrequired

    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 /bot-runtime/sessions/brief
    curl -X POST /api/v1/workspaces//bot-runtime/sessions/brief \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "instanceId": "string",
      "runtimeSessionId": "string",
      "content": "string"
    }'
    Response 201 · example
    {
      "data": {
        "invocationId": "inv_01jd2q4z8kxw9v7r3m5t8n",
        "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n"
      }
    }

    Bot invocations

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

    Recipes Connect your local agent →

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

    Claim one pending bot invocation

    Scope bot-invocations:write

    Request body

    runtimeKindstringrequired
    one of: pi-local, hermes, openclaw, claude-code-channel, custom
    instanceIdstringrequired
    min length 1 · max length 128
    runtimeSessionIdstring
    min length 1 · max length 256
    supportedCapabilitiesstring[]required
    claimTtlSecondsintegerrequired
    15–300 · default 60
    responseStreamIdstring
    min length 1 · max length 64
    excludeResponseStreamIdsstring[]
    invocationIdstring
    min length 1 · max length 128

    Response 200

    dataobject | nullrequired

    Status codes

    • 200 Successful response
    • 400 Validation error
    • 401 Missing or invalid API key
    • 403 Insufficient permissions or inaccessible resource
    POST /bot-invocations/claim
    curl -X POST /api/v1/workspaces//bot-invocations/claim \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "runtimeKind": "pi-local",
      "instanceId": "string",
      "supportedCapabilities": [
        "mentionable"
      ],
      "claimTtlSeconds": 60
    }'
    Response 200 · example
    {
      "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.",
        "sourceRevision": 1,
        "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

    Scope bot-invocations:write

    Path parameters

    invocationIdstringrequired

    Invocation ID

    Request body

    instanceIdstringrequired
    min length 1 · max length 128
    claimTokenstringrequired
    min length 1 · max length 256
    claimTtlSecondsintegerrequired
    15–300 · default 60
    knownSourceRevisioninteger
    0–9007199254740991
    restartRequiredRevisioninteger
    0–9007199254740991

    Response 200

    dataobjectrequired

    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 /bot-invocations/{invocationId}/renew
    curl -X POST /api/v1/workspaces//bot-invocations/INVOCATION_ID/renew \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "instanceId": "string",
      "claimToken": "string",
      "claimTtlSeconds": 60
    }'
    Response 200 · example
    {
      "data": {
        "invocationId": "inv_01jd2q4z8kxw9v7r3m5t8n",
        "status": "available",
        "claimExpiresAt": "2026-03-12T11:42:00.000Z",
        "sourceRevision": 1,
        "update": {
          "delivery": "…",
          "sourceRevision": 1,
          "promptMarkdown": "Picking the auth refactor back up next sprint.",
          "mentionedActorSlugs": [
            "api-v3"
          ]
        }
      }
    }
    POST /api/v1/workspaces/{workspaceId}/bot-invocations/{invocationId}/progress

    Report a step of a claimed slash command

    Appends a progress step to the slash command the claimed invocation carries. The step renders inside the command's timeline entry while it runs; it is not a message. Refused with 409 when the invocation is not a slash command.

    Scope bot-invocations:write

    Path parameters

    invocationIdstringrequired

    Invocation ID

    Request body

    instanceIdstringrequired
    min length 1 · max length 128
    claimTokenstringrequired
    min length 1 · max length 256
    stepstringrequired
    min length 1 · max length 200

    Response 200

    dataobjectrequired
    invocationIdstringrequired
    statusstringrequired

    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 /bot-invocations/{invocationId}/progress
    curl -X POST /api/v1/workspaces//bot-invocations/INVOCATION_ID/progress \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "instanceId": "string",
      "claimToken": "string",
      "step": "string"
    }'
    Response 200 · example
    {
      "data": {
        "invocationId": "inv_01jd2q4z8kxw9v7r3m5t8n",
        "status": "available"
      }
    }
    POST /api/v1/workspaces/{workspaceId}/bot-invocations/{invocationId}/steps

    Record a bot invocation trace step

    Scope bot-invocations:write

    Path parameters

    invocationIdstringrequired

    Invocation ID

    Request body

    instanceIdstringrequired
    min length 1 · max length 128
    claimTokenstringrequired
    min length 1 · max length 256
    stepTypestringrequired
    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
    contentstringrequired
    min length 1 · max length 10000
    statusTextstring
    max length 200
    clientStepIdstring
    min length 1 · max length 128
    phasestring
    one of: started
    durationMsinteger
    0–9007199254740991

    Response 200

    dataobjectrequired
    invocationIdstringrequired
    sessionIdstringrequired
    stepIdstringrequired

    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
    curl -X POST /api/v1/workspaces//bot-invocations/INVOCATION_ID/steps \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "instanceId": "string",
      "claimToken": "string",
      "stepType": "context_received",
      "content": "string"
    }'
    Response 200 · example
    {
      "data": {
        "invocationId": "inv_01jd2q4z8kxw9v7r3m5t8n",
        "sessionId": "session_01jd2q4z8kxw9v7r3m5t8n",
        "stepId": "step_01jd2q4z8kxw9v7r3m5t8n"
      }
    }
    POST /api/v1/workspaces/{workspaceId}/bot-invocations/{invocationId}/messages

    Post a message from a bot invocation

    Posts one plaintext message into the invocation's own response stream — progress notes and permission prompts mid-turn, follow-ups after the turn completed (a claim-bound completed invocation may still post; completion itself stays terminal and is never reopened). The claim decides the stream and the session the message is attributed to, so a harness never has to name either. Rejects end-to-end encrypted streams (use sealed-messages). Authenticated with the bot API key plus the claim's instanceId and claimToken; clientMessageId dedupes a retried post.

    Scope bot-invocations:write

    Path parameters

    invocationIdstringrequired

    Invocation ID

    Request body

    instanceIdstringrequired
    min length 1 · max length 128
    claimTokenstringrequired
    min length 1 · max length 256
    contentstringrequired
    min length 1 · max length 50000
    clientMessageIdstring
    min length 1 · max length 128
    metadataobject

    Response 200

    dataobjectrequired
    invocationIdstringrequired
    sessionIdstringrequired
    messageIdstringrequired

    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 /bot-invocations/{invocationId}/messages
    curl -X POST /api/v1/workspaces//bot-invocations/INVOCATION_ID/messages \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "instanceId": "string",
      "claimToken": "string",
      "content": "string"
    }'
    Response 200 · example
    {
      "data": {
        "invocationId": "inv_01jd2q4z8kxw9v7r3m5t8n",
        "sessionId": "session_01jd2q4z8kxw9v7r3m5t8n",
        "messageId": "msg_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.

    Scope bot-invocations:write

    Path parameters

    invocationIdstringrequired

    Invocation ID

    Request body

    stepIdstringrequired
    min length 1 · max length 128
    stepTypestringrequired
    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
    messageIdstring
    min length 1 · max length 128
    ciphertextstring
    base64 · min length 1
    envelopeobject
    vnumberrequired
    keyGenerationintegerrequired
    0–9007199254740991
    ivstringrequired
    base64 · min length 1
    aadstringrequired
    base64 · min length 1

    Response 200

    dataobjectrequired
    invocationIdstringrequired
    sessionIdstringrequired
    stepIdstringrequired

    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
    curl -X POST /api/v1/workspaces//bot-invocations/INVOCATION_ID/sealed-steps/started \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "stepId": "string",
      "stepType": "context_received"
    }'
    Response 200 · example
    {
      "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.

    Scope bot-invocations:write

    Path parameters

    invocationIdstringrequired

    Invocation ID

    Request body

    stepIdstringrequired
    min length 1 · max length 128
    stepTypestringrequired
    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
    messageIdstring
    min length 1 · max length 128
    ciphertextstringrequired
    base64 · min length 1
    envelopeobjectrequired
    vnumberrequired
    keyGenerationintegerrequired
    0–9007199254740991
    ivstringrequired
    base64 · min length 1
    aadstringrequired
    base64 · min length 1
    durationMsinteger
    0–9007199254740991

    Response 200

    dataobjectrequired
    invocationIdstringrequired
    sessionIdstringrequired
    stepIdstringrequired

    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
    curl -X POST /api/v1/workspaces//bot-invocations/INVOCATION_ID/sealed-steps \
      -H "Authorization: Bearer " \
      -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
    {
      "data": {
        "invocationId": "inv_01jd2q4z8kxw9v7r3m5t8n",
        "sessionId": "session_01jd2q4z8kxw9v7r3m5t8n",
        "stepId": "step_01jd2q4z8kxw9v7r3m5t8n"
      }
    }
    POST /api/v1/workspaces/{workspaceId}/bot-invocations/{invocationId}/sealed-messages

    Post a sealed message from a sealed bot invocation

    Sealed variant of a bot message, for an owner-granted E2E bot harness: posts one sealed message (ciphertext the server never decrypts) into the claim's stream. Mid-turn that is a progress note, permission prompt, or early ack; after the turn completed it is a follow-up, which a callback-token-bound completed invocation may still post — completion itself stays terminal and is never reopened. 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.

    Scope bot-invocations:write

    Path parameters

    invocationIdstringrequired

    Invocation ID

    Request body

    messageIdstringrequired
    min length 1 · max length 128
    ciphertextstringrequired
    base64 · min length 1
    envelopeobjectrequired
    vnumberrequired
    keyGenerationintegerrequired
    0–9007199254740991
    ivstringrequired
    base64 · min length 1
    aadstringrequired
    base64 · min length 1
    attachmentIdsstring[]

    Response 200

    dataobjectrequired
    messageIdstringrequired

    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 /bot-invocations/{invocationId}/sealed-messages
    curl -X POST /api/v1/workspaces//bot-invocations/INVOCATION_ID/sealed-messages \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "messageId": "string",
      "ciphertext": "string",
      "envelope": {
        "v": 1,
        "keyGeneration": 0,
        "iv": "string",
        "aad": "string"
      }
    }'
    Response 200 · example
    {
      "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. A still-active claim may recover a session marked failed by orphan cleanup. Authenticated with the per-claim callback token in the X-Threa-Callback-Token header.

    Scope bot-invocations:write

    Path parameters

    invocationIdstringrequired

    Invocation ID

    Request body

    sourceRevisioninteger
    0–9007199254740991
    replyobject
    messageIdstringrequired
    min length 1 · max length 128
    ciphertextstringrequired
    base64 · min length 1
    envelopeobjectrequired
    vnumberrequired
    keyGenerationintegerrequired
    0–9007199254740991
    ivstringrequired
    base64 · min length 1
    aadstringrequired
    base64 · min length 1
    attachmentIdsstring[]
    noResponseboolean

    Response 200

    dataobjectrequired
    invocationIdstringrequired
    sessionIdstringrequired
    messageIdstring | nullrequired

    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 /bot-invocations/{invocationId}/sealed-complete
    curl -X POST /api/v1/workspaces//bot-invocations/INVOCATION_ID/sealed-complete \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "sourceRevision": 0,
      "reply": {
        "messageId": "string",
        "ciphertext": "string",
        "envelope": {
          "v": 1,
          "keyGeneration": 0,
          "iv": "string",
          "aad": "string"
        }
      },
      "noResponse": false
    }'
    Response 200 · example
    {
      "data": {
        "invocationId": "inv_01jd2q4z8kxw9v7r3m5t8n",
        "sessionId": "session_01jd2q4z8kxw9v7r3m5t8n",
        "messageId": "msg_01jd2q4z8kxw9v7r3m5t8n"
      }
    }
    POST /api/v1/workspaces/{workspaceId}/bot-invocations/{invocationId}/complete

    Complete a claimed bot invocation

    Closes the claim, posting the reply the body carries. Completing the `/done` command also archives the thread it ran in, when that thread is one the bot opened.

    Scope bot-invocations:write

    Path parameters

    invocationIdstringrequired

    Invocation ID

    Request body

    instanceIdstringrequired
    min length 1 · max length 128
    claimTokenstringrequired
    min length 1 · max length 256
    sourceRevisioninteger
    0–9007199254740991
    finalMessageMarkdownstring
    min length 1 · max length 50000
    noResponseboolean
    summarystring
    min length 1 · max length 500
    sourcesobject[]
    typestring
    one of: web, workspace, github
    titlestringrequired
    min length 1 · max length 500
    urlstringrequired
    min length 1 · max length 2000
    snippetstring
    max length 2000
    metadataobject
    sealedReplyobject
    messageIdstringrequired
    min length 1 · max length 128
    ciphertextstringrequired
    base64 · min length 1
    envelopeobjectrequired
    vnumberrequired
    keyGenerationintegerrequired
    0–9007199254740991
    ivstringrequired
    base64 · min length 1
    aadstringrequired
    base64 · min length 1

    Response 200

    dataobjectrequired
    invocationIdstringrequired
    messageobject | nullrequired
    slotsobjectrequired

    Hydration for shared-message pointers in the returned messages, keyed by the pointer's reference: `shared:<messageId>` (legacy, current revision), `shared:<messageId>@<version>` (pinned revision) or `shared:<messageId>@<version>:<from>-<to>` (pinned span). 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
    • 409 Conflict: the resource is not in the state the operation requires
    POST /bot-invocations/{invocationId}/complete
    curl -X POST /api/v1/workspaces//bot-invocations/INVOCATION_ID/complete \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "instanceId": "string",
      "claimToken": "string"
    }'
    Response 200 · example
    {
      "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
            }
          ],
          "revision": 1,
          "editedAt": "2026-03-12T11:42:00.000Z",
          "createdAt": "2026-03-12T11:42:00.000Z",
          "sealed": {
            "ciphertext": "Picking the auth refactor back up next sprint.",
            "envelope": {
              "v": 1,
              "keyGeneration": 1,
              "iv": "…",
              "aad": "…"
            }
          }
        }
      },
      "slots": {}
    }
    POST /api/v1/workspaces/{workspaceId}/bot-invocations/{invocationId}/fail

    Fail a claimed bot invocation

    Scope bot-invocations:write

    Path parameters

    invocationIdstringrequired

    Invocation ID

    Request body

    instanceIdstringrequired
    min length 1 · max length 128
    claimTokenstringrequired
    min length 1 · max length 256
    errorMessagestringrequired
    min length 1 · max length 1000

    Response 200

    dataobjectrequired
    invocationIdstringrequired
    statusstringrequired

    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
    curl -X POST /api/v1/workspaces//bot-invocations/INVOCATION_ID/fail \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "instanceId": "string",
      "claimToken": "string",
      "errorMessage": "string"
    }'
    Response 200 · example
    {
      "data": {
        "invocationId": "inv_01jd2q4z8kxw9v7r3m5t8n",
        "status": "available"
      }
    }

    Delegations

    Inspect delegated work, claim accepted tasks, report progress or renew the lease, then complete, fail, or release the claim.

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

    List open delegations

    List accessible delegations with status `open`. When `since` is provided, filters by `statusChangedAt`; returns newly created and reopened tasks whose availability changed after that instant.

    Scope delegations:read

    Query parameters

    statusstring
    default "open"
    sincestring
    date-time

    Response 200

    dataobject[]required
    idstringrequired
    streamIdstringrequired
    titlestringrequired
    statusstringrequired
    one of: open, claimed, running, completed, failed, cancelled, expired
    claimedByLabelstring
    statusNotestring
    resultMessageIdstring
    sourceConversationIdstring
    createdAtstringrequired
    date-time
    statusChangedAtstringrequired
    date-time

    Status codes

    • 200 Successful response
    • 400 Validation error
    • 401 Missing or invalid API key
    • 403 Insufficient permissions or inaccessible resource
    GET /delegations
    curl "/api/v1/workspaces//delegations" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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"
        }
      ]
    }
    GET /api/v1/workspaces/{workspaceId}/delegations/{delegationId}

    Get a delegation

    Inspect an accessible delegation before claiming it. Returns its brief, context references, status, and current claim expiry, but never a claim token or other claim secret.

    Scope delegations:read

    Path parameters

    delegationIdstringrequired

    Delegation ID (prefixed ULID)

    Response 200

    dataobjectrequired
    idstringrequired
    streamIdstringrequired
    titlestringrequired
    statusstringrequired
    one of: open, claimed, running, completed, failed, cancelled, expired
    claimedByLabelstring
    statusNotestring
    resultMessageIdstring
    sourceConversationIdstring
    createdAtstringrequired
    date-time
    statusChangedAtstringrequired
    date-time
    briefstringrequired
    contextRefsstring[]required
    claimExpiresAtstring
    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 /delegations/{delegationId}
    curl "/api/v1/workspaces//delegations/DELEGATION_ID" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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."
        ],
        "claimExpiresAt": "2026-03-12T11:42:00.000Z"
      }
    }
    POST /api/v1/workspaces/{workspaceId}/delegations/{delegationId}/claim

    Claim an open or expired delegation

    Atomically claim an open delegation or a historical delegation still stored as `expired`. A matching idempotency key can re-key only the current unexpired claimed or running claim. Returns the brief, context references, and a cleartext claim token exactly once; send the token as X-Threa-Callback-Token on later lifecycle calls. Other non-open states return 409; a missing or inaccessible delegation returns 404.

    Scope delegations:write

    Path parameters

    delegationIdstringrequired

    Delegation ID (prefixed ULID)

    Request body

    claimedByLabelstringrequired
    min length 1 · max length 200
    idempotencyKeystring
    min length 8 · max length 128

    Response 200

    dataobjectrequired
    idstringrequired
    streamIdstringrequired
    titlestringrequired
    statusstringrequired
    one of: open, claimed, running, completed, failed, cancelled, expired
    claimedByLabelstring
    statusNotestring
    resultMessageIdstring
    sourceConversationIdstring
    createdAtstringrequired
    date-time
    statusChangedAtstringrequired
    date-time
    briefstringrequired
    contextRefsstring[]required
    claimTokenstringrequired
    claimExpiresAtstringrequired
    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
    curl -X POST /api/v1/workspaces//delegations/DELEGATION_ID/claim \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "claimedByLabel": "string"
    }'
    Response 200 · example
    {
      "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}/release

    Release a delegation claim

    Release an unexpired claimed or running delegation back to the open queue. Requires the per-claim token in X-Threa-Callback-Token. A missing, inaccessible, lapsed, stale, or replaced claim returns 404 without revealing which condition applied.

    Scope delegations:write

    Path parameters

    delegationIdstringrequired

    Delegation ID (prefixed ULID)

    Response 200

    dataobjectrequired
    idstringrequired
    streamIdstringrequired
    titlestringrequired
    statusstringrequired
    one of: open, claimed, running, completed, failed, cancelled, expired
    claimedByLabelstring
    statusNotestring
    resultMessageIdstring
    sourceConversationIdstring
    createdAtstringrequired
    date-time
    statusChangedAtstringrequired
    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}/release
    curl -X POST /api/v1/workspaces//delegations/DELEGATION_ID/release \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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}/heartbeat

    Renew a delegation claim

    Renew a live claim without changing the card status. Any HTTP client can call this endpoint directly while working; send the per-claim token in X-Threa-Callback-Token.

    Scope delegations:write

    Path parameters

    delegationIdstringrequired

    Delegation ID (prefixed ULID)

    Response 200

    dataobjectrequired
    claimExpiresAtstringrequired
    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
    curl -X POST /api/v1/workspaces//delegations/DELEGATION_ID/heartbeat \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "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.

    Scope delegations:write

    Path parameters

    delegationIdstringrequired

    Delegation ID (prefixed ULID)

    Request body

    statusNotestring
    min length 1 · max length 2000

    Response 200

    dataobjectrequired
    idstringrequired
    streamIdstringrequired
    titlestringrequired
    statusstringrequired
    one of: open, claimed, running, completed, failed, cancelled, expired
    claimedByLabelstring
    statusNotestring
    resultMessageIdstring
    sourceConversationIdstring
    createdAtstringrequired
    date-time
    statusChangedAtstringrequired
    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
    curl -X POST /api/v1/workspaces//delegations/DELEGATION_ID/status \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "statusNote": "string"
    }'
    Response 200 · example
    {
      "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 provided, the result is posted as a reply in the thread anchored on the delegation card, in the same transaction as the completion; it is 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 then includes `resultMessageId` and `resultThreadId`, and the result enters the normal message pipeline so workspace memory can capture it. Without `resultMarkdown`, the response contains only the delegation summary and no result ids. Send the per-claim token in X-Threa-Callback-Token.

    Scope delegations:write

    Path parameters

    delegationIdstringrequired

    Delegation ID (prefixed ULID)

    Request body

    resultMarkdownstring
    min length 1 · max length 50000
    metadataobject

    Response 200

    dataobjectrequired
    idstringrequired
    streamIdstringrequired
    titlestringrequired
    statusstringrequired
    one of: open, claimed, running, completed, failed, cancelled, expired
    claimedByLabelstring
    statusNotestring
    resultMessageIdstring
    sourceConversationIdstring
    createdAtstringrequired
    date-time
    statusChangedAtstringrequired
    date-time
    resultThreadIdstring

    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
    curl -X POST /api/v1/workspaces//delegations/DELEGATION_ID/complete \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "resultMarkdown": "string",
      "metadata": {}
    }'
    Response 200 · example
    {
      "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.

    Scope delegations:write

    Path parameters

    delegationIdstringrequired

    Delegation ID (prefixed ULID)

    Request body

    errorMessagestringrequired
    min length 1 · max length 2000

    Response 200

    dataobjectrequired
    idstringrequired
    streamIdstringrequired
    titlestringrequired
    statusstringrequired
    one of: open, claimed, running, completed, failed, cancelled, expired
    claimedByLabelstring
    statusNotestring
    resultMessageIdstring
    sourceConversationIdstring
    createdAtstringrequired
    date-time
    statusChangedAtstringrequired
    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
    curl -X POST /api/v1/workspaces//delegations/DELEGATION_ID/fail \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "errorMessage": "string"
    }'
    Response 200 · example
    {
      "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.

    Scope delegations:write

    Path parameters

    delegationIdstringrequired

    Delegation ID (prefixed ULID)

    Request body

    requestedByLabelstring
    max length 200

    Response 200

    dataobjectrequired
    requestIdstring
    statusstringrequired

    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
    curl -X POST /api/v1/workspaces//delegations/DELEGATION_ID/request-access \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "requestedByLabel": "string"
    }'
    Response 200 · example
    {
      "data": {
        "requestId": "request_01jd2q4z8kxw9v7r3m5t8n",
        "status": "available"
      }
    }

    Decisions

    Ask the stream for a call the runtime cannot make on its own, withdraw the question, or read the answer a member gave.

    POST /api/v1/workspaces/{workspaceId}/streams/{streamId}/decisions

    Ask the stream for a decision

    Post a decision card into the stream: a question the runtime cannot answer for itself, with the options a member picks from. Only a bot with a running session link or an in-flight invocation on that stream may open one (409 DECISION_REQUESTER_NOT_ACTIVE otherwise). The answer arrives on the bot socket as decision:resolved; an unanswered card past expiresInMs is cancelled and delivered as decision:cancelled with status expired.

    Scope bot-runtime:write

    Path parameters

    streamIdstringrequired

    Stream ID (prefixed ULID)

    Request body

    decisionIdstring
    titlestring
    min length 1 · max length 200
    bodyMarkdownstring
    max length 8000
    sealedobject
    ciphertextstringrequired
    min length 1 · max length 1000000
    envelopeobjectrequired
    vintegerrequired
    ≤ 9007199254740991
    keyGenerationintegerrequired
    0–9007199254740991
    ivstringrequired
    min length 1 · max length 64
    aadstringrequired
    min length 1 · max length 4096
    optionsobject[]required
    idstringrequired
    min length 1 · max length 64
    labelstring
    min length 1 · max length 80
    tonestringrequired
    one of: primary, neutral, destructive · default "neutral"
    allowNotebooleanrequired
    default false
    externalRefstring
    max length 256
    expiresInMsinteger
    1000–86400000
    runtimeSessionIdstring
    min length 1
    invocationIdstring
    min length 1

    Response 200

    dataobjectrequired
    idstringrequired
    workspaceIdstringrequired
    streamIdstringrequired
    requesterBotIdstring
    requesterRuntimeSessionIdstring
    requesterInvocationIdstring
    titlestringrequired
    bodyMarkdownstring
    optionsobject[]required
    idstringrequired
    labelstringrequired
    tonestringrequired
    one of: primary, neutral, destructive
    allowNotebooleanrequired
    externalRefstring
    statusstringrequired
    one of: open, resolved, cancelled, expired
    resolutionobject
    optionIdstringrequired
    notestring
    decidedBystringrequired
    decidedAtstringrequired
    date-time
    expiresAtstring
    date-time
    versionintegerrequired
    -9007199254740991–9007199254740991
    createdAtstringrequired
    date-time
    updatedAtstringrequired
    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 /streams/{streamId}/decisions
    curl -X POST /api/v1/workspaces//streams/STREAM_ID/decisions \
      -H "Authorization: Bearer " \
      -H "Content-Type: application/json" \
      -d '{
      "options": [
        {
          "id": "string",
          "tone": "neutral"
        }
      ],
      "allowNote": false
    }'
    Response 200 · example
    {
      "data": {
        "id": "id_01jd2q4z8kxw9v7r3m5t8n",
        "workspaceId": "ws_01jd2q4z8kxw9v7r3m5t8n",
        "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
        "requesterBotId": "bot_01jd2q4z8kxw9v7r3m5t8n",
        "requesterRuntimeSessionId": "requesterruntimesession_01jd2q4z8kxw9v7r3m5t8n",
        "requesterInvocationId": "inv_01jd2q4z8kxw9v7r3m5t8n",
        "title": "Auth refactor held pending token-rotation review",
        "bodyMarkdown": "Picking the auth refactor back up next sprint.",
        "options": [
          {
            "id": "id_01jd2q4z8kxw9v7r3m5t8n",
            "label": "…",
            "tone": "primary"
          }
        ],
        "allowNote": true,
        "externalRef": "…",
        "status": "open",
        "resolution": {
          "optionId": "option_01jd2q4z8kxw9v7r3m5t8n",
          "note": "…",
          "decidedBy": "…",
          "decidedAt": "2026-03-12T11:42:00.000Z"
        },
        "expiresAt": "2026-03-12T11:42:00.000Z",
        "version": 1,
        "createdAt": "2026-03-12T11:42:00.000Z",
        "updatedAt": "2026-03-12T11:42:00.000Z"
      }
    }
    POST /api/v1/workspaces/{workspaceId}/decisions/{id}/cancel

    Cancel a decision request

    Withdraw an open decision card — the runtime no longer needs the answer. The card becomes cancelled in the stream. Already-resolved or already-cancelled cards return the current row unchanged.

    Scope bot-runtime:write

    Path parameters

    idstringrequired

    Decision request ID (prefixed ULID)

    Response 200

    dataobjectrequired
    idstringrequired
    workspaceIdstringrequired
    streamIdstringrequired
    requesterBotIdstring
    requesterRuntimeSessionIdstring
    requesterInvocationIdstring
    titlestringrequired
    bodyMarkdownstring
    optionsobject[]required
    idstringrequired
    labelstringrequired
    tonestringrequired
    one of: primary, neutral, destructive
    allowNotebooleanrequired
    externalRefstring
    statusstringrequired
    one of: open, resolved, cancelled, expired
    resolutionobject
    optionIdstringrequired
    notestring
    decidedBystringrequired
    decidedAtstringrequired
    date-time
    expiresAtstring
    date-time
    versionintegerrequired
    -9007199254740991–9007199254740991
    createdAtstringrequired
    date-time
    updatedAtstringrequired
    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 /decisions/{id}/cancel
    curl -X POST /api/v1/workspaces//decisions/ID_ID/cancel \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "data": {
        "id": "id_01jd2q4z8kxw9v7r3m5t8n",
        "workspaceId": "ws_01jd2q4z8kxw9v7r3m5t8n",
        "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
        "requesterBotId": "bot_01jd2q4z8kxw9v7r3m5t8n",
        "requesterRuntimeSessionId": "requesterruntimesession_01jd2q4z8kxw9v7r3m5t8n",
        "requesterInvocationId": "inv_01jd2q4z8kxw9v7r3m5t8n",
        "title": "Auth refactor held pending token-rotation review",
        "bodyMarkdown": "Picking the auth refactor back up next sprint.",
        "options": [
          {
            "id": "id_01jd2q4z8kxw9v7r3m5t8n",
            "label": "…",
            "tone": "primary"
          }
        ],
        "allowNote": true,
        "externalRef": "…",
        "status": "open",
        "resolution": {
          "optionId": "option_01jd2q4z8kxw9v7r3m5t8n",
          "note": "…",
          "decidedBy": "…",
          "decidedAt": "2026-03-12T11:42:00.000Z"
        },
        "expiresAt": "2026-03-12T11:42:00.000Z",
        "version": 1,
        "createdAt": "2026-03-12T11:42:00.000Z",
        "updatedAt": "2026-03-12T11:42:00.000Z"
      }
    }
    GET /api/v1/workspaces/{workspaceId}/decisions/{id}

    Get a decision request

    Read one decision card, including its resolution once a member has answered.

    Scope bot-runtime:read

    Path parameters

    idstringrequired

    Decision request ID (prefixed ULID)

    Response 200

    dataobjectrequired
    idstringrequired
    workspaceIdstringrequired
    streamIdstringrequired
    requesterBotIdstring
    requesterRuntimeSessionIdstring
    requesterInvocationIdstring
    titlestringrequired
    bodyMarkdownstring
    optionsobject[]required
    idstringrequired
    labelstringrequired
    tonestringrequired
    one of: primary, neutral, destructive
    allowNotebooleanrequired
    externalRefstring
    statusstringrequired
    one of: open, resolved, cancelled, expired
    resolutionobject
    optionIdstringrequired
    notestring
    decidedBystringrequired
    decidedAtstringrequired
    date-time
    expiresAtstring
    date-time
    versionintegerrequired
    -9007199254740991–9007199254740991
    createdAtstringrequired
    date-time
    updatedAtstringrequired
    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 /decisions/{id}
    curl "/api/v1/workspaces//decisions/ID_ID" \
      -H "Authorization: Bearer "
    Response 200 · example
    {
      "data": {
        "id": "id_01jd2q4z8kxw9v7r3m5t8n",
        "workspaceId": "ws_01jd2q4z8kxw9v7r3m5t8n",
        "streamId": "stream_01jd2q4z8kxw9v7r3m5t8n",
        "requesterBotId": "bot_01jd2q4z8kxw9v7r3m5t8n",
        "requesterRuntimeSessionId": "requesterruntimesession_01jd2q4z8kxw9v7r3m5t8n",
        "requesterInvocationId": "inv_01jd2q4z8kxw9v7r3m5t8n",
        "title": "Auth refactor held pending token-rotation review",
        "bodyMarkdown": "Picking the auth refactor back up next sprint.",
        "options": [
          {
            "id": "id_01jd2q4z8kxw9v7r3m5t8n",
            "label": "…",
            "tone": "primary"
          }
        ],
        "allowNote": true,
        "externalRef": "…",
        "status": "open",
        "resolution": {
          "optionId": "option_01jd2q4z8kxw9v7r3m5t8n",
          "note": "…",
          "decidedBy": "…",
          "decidedAt": "2026-03-12T11:42:00.000Z"
        },
        "expiresAt": "2026-03-12T11:42:00.000Z",
        "version": 1,
        "createdAt": "2026-03-12T11:42:00.000Z",
        "updatedAt": "2026-03-12T11:42:00.000Z"
      }
    }