{
  "openapi": "3.0.0",
  "info": {
    "title": "API",
    "description": "The Roam API provides chat and user management capabilities for building powerful integrations with your Roam Virtual Office.\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n\nSee the [Migration Guide](/docs/guides/migration-v0-to-v1) for details on upgrading from v0.\n\nActing people and automated actors share one principal contract. See\n[Identity & Principals](/docs/guides/identity-and-principals) for `userId`\ntaxonomy, directory versus hydration behavior, and bot-loop guidance.\n\n## Base URL\n\n```\nhttps://api.ro.am/v1\n```\n\n## API version\n\nWithin the `/v1` generation, response and webhook shapes are pinned by a **dated\nversion string** (`YYYY-MM-DD`). The current default for new integrations is:\n\n```\n2026-08-20\n```\n\nEvery REST response includes a `Roam-Version` header with the version that shaped\nthe body. To pin a request explicitly:\n\n```bash\ncurl -H \"Authorization: Bearer $ROAM_TOKEN\" \\\n  -H \"Roam-Version: 2026-08-20\" \\\n  https://api.ro.am/v1/token.info\n```\n\nEach API client is created on the latest version available at that time and\n**never advances automatically**. Webhook delivery shapes are pinned at\nsubscribe time. Full rules, resolution order, and support window:\n[API Versioning](/docs/guides/api-versioning).\n\n## Authentication\n\nEvery request carries a bearer token — an organization API key, an OAuth access\ntoken, or a Personal Access Token:\n\n```\nAuthorization: Bearer YOUR_TOKEN\n```\n\nCreate API clients in **Roam Administration > Developer**. See\n[OAuth & Authentication](/docs/guides/oauth) for the authorization flows and\n[Scopes](/docs/guides/scopes) for the permission catalog.\n\n### Authentication failures\n\nFailed authentication returns HTTP `401 Unauthorized` with a machine-readable\n`error` catalog code. Distinguish these cases:\n\n| Code | Meaning | Client action |\n|------|---------|---------------|\n| `not_authed` | No `Authorization: Bearer` token was sent | Attach a token and retry |\n| `invalid_token` | Token is unknown, malformed, or expired | Obtain a new token ([refresh OAuth](/docs/guides/oauth#refresh-tokens), create a new API key / PAT) |\n| `token_revoked` | Token is **permanently** unusable — e.g. the owning person was archived or deleted, or the API client was archived | **Discard the token** and re-authenticate; retrying with the same token will never succeed |\n\nResponses with `invalid_token` or `token_revoked` also include:\n\n```http\nWWW-Authenticate: Bearer error=\"invalid_token\"\n```\n\nper [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750). Use that header\n(or the body `error` field) to stop retry loops on dead credentials.\n\n## Access Models\n\nEvery integration is backed by an **API client**, created in **Roam Administration →\nDeveloper** by a workspace admin. A client has one of two authorization types:\n\n- **API Key** — A long-lived secret for server-to-server integrations that act as the app\n  itself. No user authorization step.\n- **OAuth** — A client ID and secret for apps that are authorized per install, by an admin\n  (organization access) or by an individual user (personal access), and that can be revoked\n  or uninstalled.\n\nPersonal Access Tokens are a third credential, created by users under **User Settings →\nDeveloper** when workspace policy allows, without registering an OAuth app.\n\nThose credentials map onto the two access models Roam APIs support:\n\n- **Organization access** — For admin-built integrations that operate across the entire workspace. The integration acts as an app with its own bot persona. Authenticate with an API Key or OAuth (admin consent).\n- **Personal access** — For integrations that act on behalf of a specific user, seeing only that user's data and posting as the user's personal bot. Authenticate with OAuth or a Personal Access Token (user consent).\n\nA single OAuth app can support both models. The access model is selected when the user authorizes the app.\n\nSee the [Access Models guide](/docs/guides/access-models) for a full comparison, endpoint compatibility matrix, and guidance on choosing the right model.\n\n## Response shape\n\nSuccessful JSON responses include `\"ok\": true`. Errors return\n`\"ok\": false` with a machine-readable `error` code. List endpoints use\n**opaque** `cursor` / `nextCursor` values — do not parse or construct them.\nFull details: [Responses and Errors](/docs/guides/responses-and-errors).\n\n```json\n{ \"ok\": true, \"…\": \"…\" }\n{ \"ok\": false, \"error\": \"invalid_token\" }\n```\n\n## Pagination\n\nList endpoints return paginated results. The pagination style varies by endpoint:\n\n**Cursor-based pagination** (most endpoints):\n\n- `cursor`: Opaque string from a previous response's `nextCursor` field. Do not parse or construct cursors yourself.\n- `limit`: Number of results per page. Default is typically 10, maximum is typically 100.\n\nAn invalid or expired cursor returns `400` with `error: \"invalid_cursor\"` — restart\npagination without a cursor.\n\n**Date-range pagination** (some endpoints like `meeting.list`):\n\n- `after`: Return items after this date (RFC3339 or YYYY-MM-DD format)\n- `before`: Return items before this date\n- When `after` is specified, results are returned in ascending order; otherwise descending.\n\nCheck individual endpoint documentation for specific pagination parameters and limits.\n\n## Rate Limiting\n\nAPI requests are rate limited to prevent abuse.\n\n**Global (all credential types)**\n\n- **Burst**: 10 requests\n- **Sustained rate**: 1 request per second\n\n**Personal Access Tokens (additional)**\n\nPATs also have a **daily quota of 1,000 requests per day**.\n\nEvery response — success and `429` — carries quota headers for the burst\nbucket. `Retry-After` is sent only on `429`.\n\nA successful request looks like:\n\n```http\nHTTP/1.1 200 OK\nX-RateLimit-Limit: 10\nX-RateLimit-Remaining: 7\nX-RateLimit-Reset: 1776556801\nRateLimit-Policy: \"burst\";q=10;w=1\nRateLimit: \"burst\";r=7\n```\n\nA burst rejection looks like:\n\n```http\nHTTP/1.1 429 Too Many Requests\nRetry-After: 10\nX-RateLimit-Limit: 10\nX-RateLimit-Remaining: 0\nX-RateLimit-Reset: 1776556810\nRateLimit-Policy: \"burst\";q=10;w=1\nRateLimit: \"burst\";r=0;t=10\n\n{\"ok\":false,\"error\":\"ratelimited\"}\n```\n\n| Header | Meaning | Example |\n| --- | --- | --- |\n| `X-RateLimit-Limit` | Burst size | `10` |\n| `X-RateLimit-Remaining` | Tokens left in the burst bucket (`0`–`10`) | `7` |\n| `X-RateLimit-Reset` | Unix epoch seconds (UTC) when another burst token is available | `1776556801` |\n| `Retry-After` | Seconds to wait after a `429`. Honor this over `Reset`. Typical burst wait is `10`. | `10` |\n| `RateLimit-Policy` | IETF draft policy list (`q` = quota, `w` = window seconds) | `\"burst\";q=10;w=1` |\n| `RateLimit` | IETF draft remaining (`r`). `t` (seconds until more quota) is present only when `r` is 0 | `\"burst\";r=7` |\n\nPATs add a `\"day\"` policy on the IETF headers for the daily quota.\nThere is no custom `X-RateLimit-*-Day` trio — `X-RateLimit-*` is always\nthe burst bucket.\n\n```http\nRateLimit-Policy: \"burst\";q=10;w=1, \"day\";q=1000;w=86400\nRateLimit: \"burst\";r=7, \"day\";r=847\n```\n\n`Remaining` is a hint from the appserver process that handled the request.\nReplicas do not share counters, so a follow-up request may see a different\ncount. `Retry-After` on `429` is the authoritative wait.\n\nRepeated abuse may enter a short **tarpit** (delayed responses) before\nfurther requests are dropped.\n\n**Best practices:**\n\n- Honor `Retry-After` on `429`; do not retry sooner\n- Use `X-RateLimit-Remaining` (or `RateLimit` `r=`) to slow down before you\n  hit the ceiling\n- Batch operations where possible to reduce request count\n- For PATs, stay well under the daily 1000 budget for long-running sync jobs\n\n## Common Error Responses\n\nErrors return `\"ok\": false` and a machine-readable `error` catalog code — not a\nfree-text sentence. Branch on `error` rather than parsing prose.\n\n```json\n{ \"ok\": false, \"error\": \"invalid_parameter\" }\n```\n\nStatus codes follow these conventions:\n\n| Status | Meaning |\n|--------|---------|\n| `400 Bad Request` | Invalid request (malformed or missing parameters) |\n| `401 Unauthorized` | Missing or invalid credentials (see [Authentication failures](#authentication-failures)) |\n| `403 Forbidden` | Valid auth, but the token lacks the required scope or access to the target resource |\n| `404 Not Found` | Resource not found |\n| `413 Payload Too Large` | Request body or message text too large |\n| `429 Too Many Requests` | Rate limited (see [Rate Limiting](#rate-limiting)) |\n| `500 Internal Server Error` | Internal error |\n\nFull envelope and `missing_scope` details: [Responses and\nErrors](/docs/guides/responses-and-errors). The complete code catalog is in the\n[Error Codes guide](/docs/guides/error-codes).\n\n## Addressing\n\nThe API uses two addressing concepts:\n\n- **Chat** (`chatId`): A conversation (DM, multi-DM, or group chat). Use for retrieving history or posting to an existing conversation.\n- **Address** (`userId`, `groupId`): A destination for new messages. Use `userId` to DM a user or `groupId` to post to a group chat.\n\nWhen posting a message, you can use either a `chatId` (to continue an existing conversation) or a `userId`/`groupId` (to start or continue a conversation with that destination).\n\n## Replies & Threads\n\nMessages can be replies to other messages. The API distinguishes between two types:\n\n- **Thread replies** (`threadTimestamp`): In group chats, replies to a message create a thread. The `threadTimestamp` field contains the timestamp of the parent message that started the thread. All replies in that thread share the same `threadTimestamp`.\n\n- **DM replies** (`replyTimestamp`): In DMs, replies reference the specific message being replied to via `replyTimestamp`. This is a direct reply rather than a thread.\n\nWhen receiving webhook events, check for `threadTimestamp` to identify thread replies. When posting a reply, include `threadTimestamp` to reply within an existing thread.\n\n## Endpoints Overview\n\n### Chat & Messaging\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| [`/chat.list`](/docs/api/chat-list) | GET | List all accessible chats (DMs, MultiDMs, Group chats) |\n| [`/chat.post`](/docs/api/chat-post) | POST | Send or schedule a message (text, Block Kit, or poll) to any chat, group, or user |\n| [`/chat.sendMessage`](/docs/api/chat-send-message) | POST | **Legacy** — prefer `/chat.post` |\n| [`/chat.postEphemeral`](/docs/api/chat-post-ephemeral) | POST | Post a private \"only you can see this\" message to one member of a chat |\n| [`/chat.scheduled.list`](/docs/api/chat-scheduled-list) | GET | List pending scheduled messages created via `chat.post`'s `sendAt` |\n| [`/chat.scheduled.cancel`](/docs/api/chat-scheduled-cancel) | POST | Cancel a pending scheduled message before it sends |\n| [`/chat.startStream`](/docs/api/chat-start-stream) | POST | Start a streaming message; returns a stream ID |\n| [`/chat.appendStream`](/docs/api/chat-append-stream) | POST | Append a text chunk to an open stream |\n| [`/chat.stopStream`](/docs/api/chat-stop-stream) | POST | Finalize a stream into a single persisted message |\n| [`/chat.update`](/docs/api/chat-update) | POST | Edit a previously posted bot message |\n| [`/chat.delete`](/docs/api/chat-delete) | POST | Delete a previously posted bot message |\n| [`/chat.typing`](/docs/api/chat-typing) | POST | Show typing indicator to other participants |\n| [`/chat.history`](/docs/api/chat-history) | GET | Retrieve message history for a chat |\n| [`/chat.search`](/docs/api/chat-search) | POST | Full-text search over the user's accessible messages |\n| [`/chat.link.resolve`](/docs/api/chat-link-resolve) | POST | Resolve a Roam chat deep link to a message reference |\n| [`/chat.link.create`](/docs/api/chat-link-create) | POST | Create a shareable Roam link to a chat message |\n| [`/chat.unfurl`](/docs/api/chat-unfurl) | POST | Attach rich previews to links in an existing message |\n| [`/reaction.add`](/docs/api/reaction-add) | POST | Add emoji reaction to a message |\n| [`/reaction.remove`](/docs/api/reaction-remove) | POST | Remove emoji reaction from a message |\n| [`/reaction.list`](/docs/api/reaction-list) | GET | List all reactions on a message |\n| [`/asset.create`](/docs/api/asset-create) | POST | Create a file upload (JSON/MCP-friendly); attach via `assetIds`, or use `purpose: \"story\"` for story media |\n| [`/item.upload`](/docs/api/item-upload) | POST | Upload a file (raw bytes) to attach to a message |\n| [`/story.post`](/docs/api/story-post) | POST | Post a story as the authenticated user (Personal tokens only) |\n\n### Groups & Group Chats\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| [`/group.list`](/docs/api/group-list) | GET | List accessible groups |\n| [`/groups.list`](/docs/api/groups-list) | GET | **Legacy** — prefer `/group.list` (raw array response) |\n| [`/group.info`](/docs/api/group-info) | GET | Get group details by ID or name |\n| [`/group.create`](/docs/api/group-create) | POST | Create a new group chat |\n| [`/group.rename`](/docs/api/group-rename) | POST | Rename an existing group |\n| [`/group.archive`](/docs/api/group-archive) | POST | Archive a group |\n| [`/group.members`](/docs/api/group-members) | GET | List members in a group with roles |\n| [`/group.add`](/docs/api/group-add) | POST | Add members or admins to a group |\n| [`/group.join`](/docs/api/group-join) | POST | Join a public group as the calling identity |\n| [`/group.remove`](/docs/api/group-remove) | POST | Remove members from a group |\n\n### Users\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| [`/user.list`](/docs/api/user-list) | GET | List members or hydrate explicit principal IDs |\n| [`/user.info`](/docs/api/user-info) | GET | Resolve a principal by ID or a member by email |\n| [`/userauditlog.list`](/docs/api/userauditlog-list) | GET | List user audit log entries |\n| [`/messageevent.export`](/docs/api/messageevent-export) | POST | Export daily message archives (JSON Lines) |\n\n### Meetings\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| [`/conversation.list`](/docs/api/conversation-list) | GET | List conversations (meetings) with participants |\n| [`/meeting.list`](/docs/api/meeting-list) | GET | List meetings |\n| [`/recording.list`](/docs/api/recording-list) | GET | **Legacy** — prefer `/meeting.list` / `/meeting.info` |\n| [`/meeting.info`](/docs/api/meeting-info) | GET | Get meeting details with summary, action items, and chapters |\n| [`/meeting.participants`](/docs/api/meeting-participants) | GET | Paginate through meeting participants |\n| [`/meeting.transcript`](/docs/api/meeting-transcript) | GET | Get meeting transcript (JSON or WebVTT) |\n| [`/meeting.search`](/docs/api/meeting-search) | GET | AI-powered meeting search (Personal access only) |\n| [`/meeting.prompt`](/docs/api/meeting-prompt) | POST | Ask AI questions about a meeting transcript |\n| [`/meeting.shareLink`](/docs/api/meeting-share-link) | POST | Get (or create) a shareable link for a meeting |\n| [`/meeting.link.create`](/docs/api/meeting-link-create) | POST | Create a meeting link |\n| [`/meeting.link.info`](/docs/api/meeting-link-info) | POST | Get details for a meeting link |\n| [`/meeting.link.update`](/docs/api/meeting-link-update) | POST | Update a meeting link |\n| [`/calendar.event.create`](/docs/api/calendar-event-create) | POST | Create a calendar event with a Roam meeting link |\n| [`/calendar.list`](/docs/api/calendar-list) | GET | List events from the user's connected calendars |\n| [`/lobby.list`](/docs/api/lobby-list) | GET | List active lobbies |\n| [`/lobby.booking.list`](/docs/api/lobby-booking-list) | GET | List bookings for a lobby |\n\n### Magicasts\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| [`/magicast.list`](/docs/api/magicast-list) | GET | List magicasts |\n| [`/magicast.info`](/docs/api/magicast-info) | GET | Get magicast details, transcript, and video |\n| [`/magicast.shareLink`](/docs/api/magicast-share-link) | POST | Get (or create) a shareable player link |\n\n### App Management\n| Endpoint | Method | Description |\n|----------|--------|-------------|\n| [`/token.info`](/docs/api/token-info) | GET | Get info about the current access token |\n| [`/token.revoke`](/docs/api/token-revoke) | POST | Revoke access token |\n| [`/webhook.list`](/docs/webhooks/webhook-list) | GET | List webhook subscriptions |\n| [`/webhook.subscribe`](/docs/webhooks/webhook-subscribe) | POST | Create or update a webhook subscription (dotted event names) |\n| [`/webhook.unsubscribe`](/docs/webhooks/webhook-unsubscribe) | POST | Remove a webhook subscription |\n| [`/webhook.deliveries`](/docs/webhooks/webhook-deliveries) | GET | List failed deliveries |\n\n## Common Use Cases\n\n### Build a Chat Bot\nCreate an OAuth app with `chat:read` and `chat:send_message` scopes, configure a webhook URL to receive messages, then respond programmatically.\n- Subscribe to [`chat.message`](/docs/webhooks/chat-message) events\n- Use [`/reaction.add`](/docs/api/reaction-add) to acknowledge receipt\n- Use [`/chat.typing`](/docs/api/chat-typing) to show typing indicator\n- Use [`/chat.post`](/docs/api/chat-post) to send a response\n\n#### Responding privately\nTo reply so that only one person sees it — validation errors, permission\nwarnings, \"here's how to use this command\" help — use\n[`/chat.postEphemeral`](/docs/api/chat-post-ephemeral) with the `chatId` and\nthe `userId` of the person to address. The recipient sees the message in the\nshared chat under an \"Only you can see this\" header; nobody else sees\nanything, and nothing is stored in history. Ephemeral messages are\nbest-effort and desktop/web-only — anything the recipient must durably\nreceive belongs in a DM (`/chat.post` with `userIds`) instead.\n\n---\nHave questions? Contact us via [Team Roam Support Chat](https://ro.am/support/contact-us).\n",
    "version": "1.0",
    "termsOfService": "https://ro.am/terms",
    "contact": {
      "name": "Team Roam Support Chat",
      "url": "https://ro.am/support/contact-us"
    }
  },
  "servers": [
    {
      "url": "https://api.ro.am/v1",
      "description": "Production Server"
    }
  ],
  "tags": [
    {
      "name": "Chat",
      "description": "Send and receive messages, manage reactions"
    },
    {
      "name": "Groups",
      "description": "Create and manage groups"
    },
    {
      "name": "Users",
      "description": "List and look up users"
    },
    {
      "name": "Meetings",
      "description": "List meetings, view transcripts, and search meeting content"
    },
    {
      "name": "Magicasts",
      "description": "List and look up magicasts"
    },
    {
      "name": "App",
      "description": "Token info and app management"
    }
  ],
  "externalDocs": {
    "description": "Roam API Documentation",
    "url": "https://developer.ro.am/docs/api"
  },
  "paths": {
    "/chat.list": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "List chats",
        "description": "List accessible chats — DMs, multi-DMs, group chats, all-hands \"team\nRoam\" groups, and meeting chats.\n\n**Personal access tokens** are backed by the user's inbox: chats are\nordered by most recent activity and include `lastMessageTime`,\n`isUnread`, `preview`, `isMuted`, and `isPinned`. Bot threads (where\nthe user has unread replies) are returned as separate rows keyed by\n`threadTimestamp`.\n\n**Organization tokens** receive the chats the bot has access to,\nordered by chat creation time. Inbox-derived fields\n(`lastMessageTime`, `isUnread`, `preview`, `isMuted`, `isPinned`) are\nnot populated, since bot addresses do not accumulate inbox state for\nnormal messages — those are delivered via webhooks.\n\nTimestamps are returned in the caller's timezone (see\n[Timezone handling](/docs/guides/migration-v0-to-v1#timezone-handling)).\n\n**Required scope:** `chat:read`\n\nPass `expand=addresses` to include an address sidecar for chat participants\nand preview senders. See [Identity & Principals](/docs/guides/identity-and-principals).\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "chat.list",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 50,
              "default": 10
            },
            "description": "Number of chats to return per response. Default 10, max 50."
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually."
          },
          {
            "name": "expand",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated fields to expand. Supported: `addresses` — include an\n`addresses` map resolving chat participants and preview sender IDs.\n"
          }
        ],
        "responses": {
          "200": {
            "description": "Chats retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "chats": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "created",
                          "name"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid",
                            "description": "The Chat ID."
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "dm",
                              "group"
                            ],
                            "description": "The kind of chat, using the same vocabulary as the\n`chat.message` webhook `chatType` filter: `dm` for\ndirect and multi-person DMs, `group` for group chats\n(including all-hands and meeting channels). Omitted\nfor chat kinds outside that vocabulary (e.g. meeting\nchats).\n"
                          },
                          "threadTimestamp": {
                            "type": "integer",
                            "description": "Unix-microsecond timestamp of the parent message\nwhen this row represents a thread (Personal tokens\nonly). Absent for top-level chat rows.\n"
                          },
                          "name": {
                            "type": "string",
                            "description": "Descriptive name for the chat:\n\n* Group chat — the group name\n* DM — the name of the other party\n* Multi-DM — comma-separated list of participant names\n"
                          },
                          "groupId": {
                            "type": "string",
                            "format": "uuid",
                            "description": "The Group ID. Only present for group chats\n(including all-hands and meeting chats); absent for\nDMs and Multi-DMs.\n"
                          },
                          "created": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When the chat was created (RFC3339, caller's timezone)."
                          },
                          "lastMessageTime": {
                            "type": "string",
                            "format": "date-time",
                            "description": "Time of the most recent activity. Personal access\nonly; absent on Organization responses.\n"
                          },
                          "isUnread": {
                            "type": "boolean",
                            "description": "`true` if the chat has unread messages. Personal\naccess only.\n"
                          },
                          "preview": {
                            "type": "object",
                            "description": "Preview of the most recent message. Personal access\nonly.\n",
                            "properties": {
                              "text": {
                                "type": "string"
                              },
                              "contentType": {
                                "type": "string",
                                "enum": [
                                  "text",
                                  "voice",
                                  "poll"
                                ]
                              },
                              "senderId": {
                                "type": "string",
                                "format": "uuid",
                                "description": "User ID of the message sender. Present when the previewed message has a principal author; system previews (such as membership changes) and deleted messages omit it. When present, it resolves through `/user.info`."
                              },
                              "sender": {
                                "type": "object",
                                "description": "Per-message sender display override the previewed\nmessage was sent with. Present only when the\nmessage carries one; `senderId` remains the\nauthoring identity. Omitted for deleted messages.\nSee the\n[Sender Profiles guide](/docs/guides/sender-profiles).\n",
                                "properties": {
                                  "name": {
                                    "type": "string",
                                    "description": "Display name override for the previewed message."
                                  },
                                  "imageUrl": {
                                    "type": "string",
                                    "format": "uri",
                                    "description": "Avatar URL override for the previewed message."
                                  }
                                }
                              },
                              "mentioned": {
                                "type": "boolean",
                                "description": "`true` if the previewed message mentions the user."
                              }
                            }
                          },
                          "isMuted": {
                            "type": "boolean",
                            "description": "`true` if the user has muted the chat. Personal\naccess only.\n"
                          },
                          "isPinned": {
                            "type": "boolean",
                            "description": "`true` if the user has pinned the chat. Personal\naccess only.\n"
                          }
                        }
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "description": "Pagination cursor for fetching the next page of results."
                    },
                    "addresses": {
                      "type": "object",
                      "additionalProperties": {
                        "$ref": "#/components/schemas/Address"
                      },
                      "description": "Resolved addresses keyed by UUID. Included only with `expand=addresses`."
                    }
                  }
                },
                "example": {
                  "chats": [
                    {
                      "id": "295155ae-7df5-4ed5-9ebc-89a170559c81",
                      "type": "group",
                      "name": "Engineering Team",
                      "groupId": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                      "created": "2025-12-13T05:07:54-08:00",
                      "lastMessageTime": "2026-04-18T09:14:02-07:00",
                      "isUnread": true,
                      "preview": {
                        "text": "Let's review the Q2 roadmap on Friday.",
                        "contentType": "text",
                        "senderId": "ad1e9cc0-0ffd-47e5-895c-2630a73327b4",
                        "mentioned": true
                      }
                    },
                    {
                      "id": "53b8a72b-b442-4da2-94ea-41b6116c14ea",
                      "type": "dm",
                      "name": "Alex Chen",
                      "created": "2025-10-18T23:29:25-07:00",
                      "lastMessageTime": "2026-04-17T16:02:11-07:00",
                      "isPinned": true
                    }
                  ],
                  "nextCursor": "YzE6MTc2MDgzMDE2NTEwNTAwMA"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error",
            "description": "Bad request. Common causes:\n- `limit` is non-numeric, ≤ 0, or > 50\n- `cursor` is not a valid micro-second timestamp\n"
          },
          "401": {
            "$ref": "#/components/responses/Error",
            "description": "Presented invalid authentication credentials."
          },
          "405": {
            "$ref": "#/components/responses/Error",
            "description": "An unsupported method was requested."
          },
          "500": {
            "description": "An internal error occurred, including a preview sender that cannot resolve as a visible principal. The endpoint does not return a partial page."
          }
        }
      }
    },
    "/chat.post": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Send a message",
        "description": "Send a message to a chat. Messages can be plain markdown text, rich [Block Kit](/docs/guides/block-kit) layouts, or polls.\n\n**Destination (ONE of the following is required):**\n- `chatId` - Post to an existing chat by its ID\n- `groupId` - Post to a group chat\n- `userIds` - Post to a DM or Multi-DM with the specified users\n\nYou must specify exactly one destination. Specifying multiple destinations (e.g., both `chatId` and `groupId`) will return a 400 error.\n\nMentions use Slack's token syntax with Slack's semantics: `<@ID>` mentions a principal (a user or bot, e.g. `<@7861a4c6-765a-495d-898d-fae3d8fbba2d>` — resolvable via [`user.info`](/docs/api/user-info)), `<!subteam^ID>` mentions a group or channel, notifying its members (resolvable via [`group.info`](/docs/api/group-info)), and `<!channel>` notifies everyone in the chat.\nWhen rendered in the client, the tag will automatically be replaced with the human-readable display name (or \"everyone\" for `<!channel>`).\nOn write, either token form is accepted for any mentionable ID; the legacy `<@all>` broadcast alias is accepted; and a Slack-style `|label` suffix (e.g. `<@7861a4c6-…|Rob>`, `<!subteam^59c1a4d2-…|@eng>`) is accepted and ignored — the mentioned entity's live display name is always used. Write-side acceptance is identical on every [API version](/docs/guides/api-versioning). Messages read back always carry bare canonical tokens, and `<!channel>` for the broadcast — on API versions from `2026-08-07`; clients pinned to older versions read the older grammar (`<@ID>` for every mention, `<@all>`). Slack forms Roam does not implement are reserved and stay literal text: `<#ID>` channel links, `<!here>`, and `<!everyone>`.\n\n**Custom sender (optional):** see the [Sender Profiles guide](/docs/guides/sender-profiles).\n- `sender.name` / `sender.imageUrl` are per-message display overrides, stored on the message itself.\n- `sender.id` authors the message as a configured bot persona (Roam Administration > Developer > edit your app > Add Bot Persona). Ids that don't match a configured persona are accepted and ignored — the message is authored by the app's root identity. Sending never creates or renames personas.\n- **Personal access tokens**: Reject the `sender` field with 400. PATs always post as their personal bot.\n\n**Access:** Organization tokens can post to chats the bot is a member of,\nand to **public groups** in the workspace without joining. Personal tokens\ncan post only where the owner is a member (`403` `not_in_chat` for an\nunjoined public group).\n\n**Required scope:** `chat:send_message` or `chat:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "chat.post",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Exactly one content type is required: `text`/`items`/`assetIds`, `blocks`, or `poll`. They cannot be combined.\n",
                "properties": {
                  "chatId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Post to an existing chat by ID (mutually exclusive with groupId/userIds)"
                  },
                  "groupId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Post to a group channel (mutually exclusive with chatId/userIds)"
                  },
                  "userIds": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "Post to a DM or Multi-DM with these users (mutually exclusive with chatId/groupId)"
                  },
                  "threadTimestamp": {
                    "type": "integer",
                    "description": "Reply to a specific thread by providing the thread's timestamp.\nIf the timestamp doesn't correspond to an existing message, a 400 error is returned.\nMutually exclusive with `threadKey`.\n"
                  },
                  "threadKey": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "A stable external identifier used to group related messages into a thread.\nOn the first use of a given `threadKey`, a new message is posted and the resulting\nthread timestamp is stored. Subsequent messages with the same `threadKey` are\nautomatically threaded under the original message.\n\nThis is useful for external integrations (e.g. PagerDuty, Grafana, Sentry) that\nwant to thread related messages using their own identifiers (such as `dedup_key`,\n`fingerprint`, or `group_id`) without tracking Roam's internal thread timestamps.\n\nMutually exclusive with `threadTimestamp`. When `threadKey` is provided, the\nresponse is always synchronous (equivalent to `sync: true`).\n"
                  },
                  "replyTimestamp": {
                    "type": "integer",
                    "description": "Reply directly to a specific message by its timestamp. Unlike\n`threadTimestamp` (which threads a reply under a parent message in a\ngroup), `replyTimestamp` is a direct reply used in DMs — which have no\nthreads — and within an existing channel thread. Text messages only:\nnot supported together with `blocks` or `poll`.\n"
                  },
                  "text": {
                    "type": "string",
                    "format": "markdown",
                    "description": "Message text in GitHub-flavored markdown"
                  },
                  "markdown": {
                    "type": "boolean",
                    "description": "Text is markdown by default. If set to false, markdown interpretation will be disabled."
                  },
                  "items": {
                    "type": "array",
                    "description": "Array of Item IDs to attach to this message.",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    }
                  },
                  "assetIds": {
                    "type": "array",
                    "description": "Array of asset IDs from [`/asset.create`](/docs/api/asset-create)\nto attach to this message. Each asset must be owned by your app\nand fully uploaded (processed and ready). Combines with\n`text`/`items`; not with `blocks` or `poll`.\n",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    }
                  },
                  "blocks": {
                    "type": "array",
                    "description": "Array of [Block Kit](/docs/guides/block-kit) block objects for rich message formatting.\nCannot be combined with `text` or `items`. Maximum 10 blocks, 8,000 bytes total payload.\n",
                    "items": {
                      "type": "object",
                      "properties": {
                        "type": {
                          "type": "string",
                          "enum": [
                            "header",
                            "section",
                            "context",
                            "divider",
                            "actions"
                          ],
                          "description": "The block type."
                        }
                      }
                    }
                  },
                  "color": {
                    "type": "string",
                    "description": "Colored vertical strip on the side of the message. Only used with `blocks`.\nNamed values: `good` (green), `warning` (yellow), `danger` (red), or a hex color like `#5B3FD9`.\n"
                  },
                  "poll": {
                    "type": "object",
                    "description": "Create a poll message. Mutually exclusive with `text`, `items`, and `blocks`.\n",
                    "properties": {
                      "question": {
                        "type": "string",
                        "maxLength": 256,
                        "description": "The poll question (1–256 characters)."
                      },
                      "options": {
                        "type": "array",
                        "minItems": 2,
                        "description": "Poll answer options (at least 2, each 1–128 characters).",
                        "items": {
                          "type": "string",
                          "maxLength": 128
                        }
                      },
                      "allowMultipleAnswers": {
                        "type": "boolean",
                        "description": "Whether voters can select multiple options. Defaults to false."
                      },
                      "closesAt": {
                        "type": "string",
                        "format": "date-time",
                        "description": "Optional RFC-3339 datetime when the poll automatically closes."
                      }
                    },
                    "required": [
                      "question",
                      "options"
                    ]
                  },
                  "sender": {
                    "$ref": "#/components/schemas/Sender"
                  },
                  "sync": {
                    "type": "boolean",
                    "description": "If set, the post will be performed synchronously and its timestamp returned. Incompatible with `sendAt`."
                  },
                  "sendAt": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Schedule the message for later delivery (RFC 3339). Requirements:\n- Must be in the **future** and within **30 days**\n- Must fall on a **15-minute UTC boundary** (`:00`, `:15`, `:30`, or `:45`; seconds and sub-seconds zero)\n- Incompatible with `sync`, `poll`, `threadKey`, and `replyTimestamp`\n\nWhen `sendAt` is set, the response is `{chatId, scheduledMessageId, sendAt}`\ninstead of an immediate message `timestamp`.\n\nScheduled messages can be listed via\n[`/chat.scheduled.list`](/docs/api/chat-scheduled-list) and canceled via\n[`/chat.scheduled.cancel`](/docs/api/chat-scheduled-cancel) until they send.\n"
                  }
                }
              },
              "examples": {
                "postToChat": {
                  "summary": "Post to existing chat",
                  "value": {
                    "chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176",
                    "text": "Hello from the **API**"
                  }
                },
                "postToGroup": {
                  "summary": "Post to a group",
                  "value": {
                    "groupId": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                    "text": "Hello team!"
                  }
                },
                "postToDM": {
                  "summary": "Post to a DM",
                  "value": {
                    "userIds": [
                      "709b8a57-70bc-427a-b6f0-b16ba5297f8c"
                    ],
                    "text": "Hey, quick question..."
                  }
                },
                "postWithSenderDisplay": {
                  "summary": "Post with a per-message display override",
                  "value": {
                    "groupId": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                    "text": "Build completed successfully!",
                    "sender": {
                      "name": "CI · frontend",
                      "imageUrl": "https://example.com/build-bot.png"
                    }
                  }
                },
                "postAsPersona": {
                  "summary": "Post as a configured bot persona",
                  "value": {
                    "groupId": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                    "text": "A new ticket was assigned to you.",
                    "sender": {
                      "id": "support-bot"
                    }
                  }
                },
                "postWithThreadKey": {
                  "summary": "Post with thread key",
                  "value": {
                    "groupId": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                    "threadKey": "INCIDENT-12345",
                    "text": "Incident update: CPU usage normalized"
                  }
                },
                "postBlockKit": {
                  "summary": "Block Kit message",
                  "value": {
                    "groupId": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                    "blocks": [
                      {
                        "type": "header",
                        "text": {
                          "type": "plain_text",
                          "text": "Build Complete"
                        }
                      },
                      {
                        "type": "section",
                        "text": {
                          "type": "mrkdwn",
                          "text": "All checks passed for *main* branch."
                        }
                      }
                    ],
                    "color": "good"
                  }
                },
                "postPoll": {
                  "summary": "Poll message",
                  "value": {
                    "groupId": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                    "poll": {
                      "question": "When should we have the team standup?",
                      "options": [
                        "9:00 AM",
                        "10:00 AM",
                        "11:00 AM"
                      ],
                      "allowMultipleAnswers": false
                    }
                  }
                },
                "scheduledPost": {
                  "summary": "Schedule a message",
                  "value": {
                    "groupId": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                    "text": "Reminder: standup in 15 minutes",
                    "sendAt": "2026-07-20T15:00:00Z"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message posted or scheduled successfully. Immediate posts return\n`chatId` (and `timestamp` when `sync` is set). Scheduled posts\n(`sendAt`) return `chatId`, `scheduledMessageId`, and `sendAt`.\nAll success bodies include `\"ok\": true` — see\n[Responses and Errors](/docs/guides/responses-and-errors).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "chatId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "ID of the chat where the message was (or will be) posted"
                    },
                    "threadTimestamp": {
                      "type": "integer",
                      "description": "Thread timestamp if replying to a thread"
                    },
                    "timestamp": {
                      "type": "integer",
                      "description": "Message timestamp (present if sync is set; omitted for scheduled posts)"
                    },
                    "scheduledMessageId": {
                      "type": "string",
                      "description": "ID of the scheduled message (only when `sendAt` was provided). Pass to `/chat.scheduled.cancel` to cancel, or find it later via `/chat.scheduled.list`."
                    },
                    "sendAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Scheduled send time echoed from the request (only when `sendAt` was provided)"
                    }
                  }
                },
                "examples": {
                  "immediate": {
                    "summary": "Immediate post (sync)",
                    "value": {
                      "ok": true,
                      "chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176",
                      "timestamp": 1765602474760032
                    }
                  },
                  "scheduled": {
                    "summary": "Scheduled post",
                    "value": {
                      "ok": true,
                      "chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176",
                      "scheduledMessageId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                      "sendAt": "2026-07-20T15:00:00Z"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- No destination specified (chatId, groupId, or userIds required)\n- Multiple destinations specified (only one allowed)\n- `threadTimestamp` does not correspond to an existing message\n- Both `threadKey` and `threadTimestamp` were provided (they are mutually exclusive)\n- `threadKey` exceeds 64 characters\n- `replyTimestamp` provided on a non-text message (`blocks` or `poll`)\n- `replyTimestamp` used outside a DM or a channel thread\n- `sender.name` exceeds 128 characters\n- `sender.imageUrl` is not an absolute HTTP(S) URL\n- Personal access token provided a `sender` field (`access_mode_not_supported`)\n- Multiple content types provided (only one of `text`/`items`, `blocks`, or `poll` allowed)\n- An `assetId` was not found, not owned by your app, or not a file asset\n- An `assetId` is still processing — retry once its upload completes\n- `blocks` array exceeds 10 blocks or 8,000 bytes\n- Invalid block structure (see [Block Kit guide](/docs/guides/block-kit))\n- Interactive buttons sent without an Interactivity URL configured\n- Invalid `color` value\n- Poll question empty or exceeds 256 characters\n- Poll has fewer than 2 options, or an option exceeds 128 characters\n- `sendAt` not on a 15-minute UTC boundary, not in the future, more than 30 days ahead, or combined with `sync` / `poll` / `threadKey` / `replyTimestamp`\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "Forbidden (`not_in_chat` / `not_in_group`). Organization tokens may\nstill post to a public group in the workspace without joining. Personal\ntokens require the owner to be a member.\n",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "413": {
            "description": "Message content exceeds the maximum allowed size.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/chat.sendMessage": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Send a message (Legacy)",
        "deprecated": true,
        "description": "**Legacy:** Prefer [`/chat.post`](/docs/api/chat-post). This endpoint is maintained for backwards compatibility only.\n\nSend a markdown-formatted text message to a single group, addressed by `recipients` (a one-element array containing the group ID). New integrations should use [`/chat.post`](/docs/api/chat-post), which also supports DMs, threads, Block Kit, and polls.\n\nThe optional `sender` object follows the same semantics as `/chat.post` — see the [Sender Profiles guide](/docs/guides/sender-profiles).\n\n**Required scope:** `chat:send_message`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "chat.sendMessage",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "recipients": {
                    "type": "array",
                    "description": "Destination group, as a single-element array containing one group ID (UUID).\nExactly one recipient is required: zero recipients returns 400, and\nmore than one is not currently supported (also 400).\n",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "minItems": 1,
                    "maxItems": 1
                  },
                  "text": {
                    "type": "string",
                    "format": "markdown",
                    "description": "Message text in GitHub-flavored markdown. Required."
                  },
                  "markdown": {
                    "type": "boolean",
                    "description": "Text is markdown by default. If set to false, markdown interpretation will be disabled."
                  },
                  "items": {
                    "type": "array",
                    "description": "Array of Item IDs (UUIDs) to attach to this message.",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    }
                  },
                  "sender": {
                    "$ref": "#/components/schemas/Sender"
                  }
                },
                "required": [
                  "recipients",
                  "text"
                ]
              },
              "example": {
                "recipients": [
                  "757dfe66-37b4-4772-baa5-8c86ec68c176"
                ],
                "text": "Hello team!"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message sent successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "description": "Always `ok` on success."
                    },
                    "chatId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "ID of the chat where the message was posted."
                    }
                  },
                  "required": [
                    "status",
                    "chatId"
                  ]
                },
                "example": {
                  "status": "ok",
                  "chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176"
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- Missing `text` (`No text provided`)\n- No recipient specified (`No recipients specified`)\n- More than one recipient (`More than one recipient is not supported`)\n- Recipient is not a valid group UUID (`Invalid group ID`)\n- `sender.name` exceeds 128 characters, or `sender.imageUrl` is not an absolute HTTP(S) URL\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Invalid authentication credentials, or the target group belongs to a different account.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "Missing required scope (`chat:send_message`).",
            "$ref": "#/components/responses/Error"
          },
          "413": {
            "description": "Message text exceeds the maximum size (8000 bytes).",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/chat.postEphemeral": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Post an ephemeral message",
        "description": "Post an **ephemeral message** — visible to a single member of a chat, with an\n\"Only you can see this\" header — without posting anything the other members can\nsee. This is the standard way for a bot to respond privately in a shared\nchannel (the Roam equivalent of Slack's `chat.postEphemeral`).\n\nThe target `userId` must be a member of the chat (for channels: a member of the\nbacking group), otherwise the request fails with `user_not_in_chat`.\n\n`text` is always rendered as GitHub-flavored markdown. Mention markup\n(`<@USER_ID>`) is **not** supported in ephemeral messages. Block Kit `blocks`\nare not currently supported.\n\n**Delivery semantics — read before using:**\n- **Desktop and web only.** Mobile clients do not display ephemeral messages,\n  and no mobile push notification is sent. A recipient who only uses Roam on\n  mobile will never see the message.\n- **Best-effort, at-most-once.** The message is delivered in real time to the\n  recipient's connected clients, and to recently-active offline clients when\n  they reconnect. A recipient who has been offline for several days (or has\n  never signed in on that device) silently misses it. There are no retries\n  and no delivery receipt.\n- **Transient.** The message is never stored server-side. It disappears when\n  the recipient restarts their app, and it never appears in\n  [`/chat.history`](/docs/api/chat-history) or [`/chat.search`](/docs/api/chat-search).\n- **Not addressable.** It cannot be edited or deleted:\n  [`/chat.update`](/docs/api/chat-update) and [`/chat.delete`](/docs/api/chat-delete)\n  against its `(chatId, timestamp)` return `message_not_found`.\n- **No webhooks.** Posting an ephemeral message never triggers a\n  [`chat.message`](/docs/webhooks/chat-message) event, so it cannot leak to\n  org-wide webhook consumers.\n\nDo not use ephemeral messages for anything the recipient must durably receive —\nuse a DM ([`/chat.post`](/docs/api/chat-post) with `userIds`) for that.\n\n**Custom sender (optional):** same semantics as [`/chat.post`](/docs/api/chat-post) —\n`sender.name` / `sender.imageUrl` apply a per-message display override, and\n`sender.id` authors the message as a configured bot persona (unknown ids\nare accepted and ignored). Personal access tokens reject the `sender`\nfield. See the [Sender Profiles guide](/docs/guides/sender-profiles).\n\n**Required scope:** `chat:send_message` or `chat:write`\n\n**Access:** Organization and Personal. The organization bot or\npersonal-token **owner** must be a member of the chat (`403` `not_in_chat`\notherwise) — unlike [`/chat.post`](/docs/api/chat-post), there is no\npublic-group carveout. Personal tokens send as the user's personal bot\nand reject the `sender` field.\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "chat.postEphemeral",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "chatId",
                  "userId",
                  "text"
                ],
                "properties": {
                  "chatId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The chat to post into. Use [`/chat.list`](/docs/api/chat-list) or a `chat.message` webhook payload to obtain chat IDs."
                  },
                  "userId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The user who should see the message. Must be a member of the chat."
                  },
                  "threadTimestamp": {
                    "type": "integer",
                    "description": "Show the ephemeral message inside an existing thread instead of the\nmain channel view. Channels only — returns 400 in DMs and Multi-DMs.\nThe value is not validated against an existing thread: pass a real\nthread's timestamp, or the message is keyed under a thread view the\nrecipient can never open and is effectively never seen.\n"
                  },
                  "text": {
                    "type": "string",
                    "format": "markdown",
                    "description": "Message text in GitHub-flavored markdown (always rendered as\nmarkdown; there is no plain-text mode). Maximum 8,000 bytes.\nMention markup is not supported.\n"
                  },
                  "sender": {
                    "$ref": "#/components/schemas/Sender"
                  }
                }
              },
              "examples": {
                "respondPrivately": {
                  "summary": "Private reply in a channel",
                  "value": {
                    "chatId": "295155ae-7df5-4ed5-9ebc-89a170559c81",
                    "userId": "7861a4c6-765a-495d-898d-fae3d8fbba2d",
                    "text": "Only *you* can see this: your deploy token expires in 3 days."
                  }
                },
                "inThread": {
                  "summary": "Private reply inside a thread",
                  "value": {
                    "chatId": "295155ae-7df5-4ed5-9ebc-89a170559c81",
                    "userId": "7861a4c6-765a-495d-898d-fae3d8fbba2d",
                    "threadTimestamp": 1765602474760032,
                    "text": "Heads up — this thread mentions a repo you no longer have access to."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Ephemeral message accepted for delivery. The `(chatId, timestamp)` pair is\nthe identity the recipient's client renders the message under; it is not\naddressable by any other endpoint. All success bodies include `\"ok\": true` —\nsee [Responses and Errors](/docs/guides/responses-and-errors).\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "chatId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "ID of the chat the message was delivered into"
                    },
                    "timestamp": {
                      "type": "integer",
                      "description": "Message timestamp in microseconds"
                    },
                    "threadTimestamp": {
                      "type": "integer",
                      "description": "Echoed thread timestamp when the message was posted into a thread"
                    }
                  }
                },
                "examples": {
                  "posted": {
                    "summary": "Ephemeral message accepted",
                    "value": {
                      "ok": true,
                      "chatId": "295155ae-7df5-4ed5-9ebc-89a170559c81",
                      "timestamp": 1765602474760032
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- `text` missing (`missing_parameter`)\n- `chatId` or `userId` is not a UUID (`invalid_parameter`)\n- The target user is not a member of the chat (`user_not_in_chat`)\n- `threadTimestamp` used in a DM or Multi-DM (`invalid_parameter`)\n- The channel's group is archived (`is_archived`)\n- `sender.name` exceeds 128 characters, or `sender.imageUrl` is not an absolute HTTP(S) URL (`invalid_parameter`)\n- Personal access token provided a `sender` field (`access_mode_not_supported`)\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "Forbidden. The token lacks the required scope, or the bot lacks access to\nthe target chat (`not_in_chat` / `not_in_group`); a public channel in a\ndifferent Roam responds `group_not_found`, indistinguishable from a\nmissing group.\n",
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "Not found: the chat does not exist (`chat_not_found`), or `userId` does\nnot refer to a user or visitor address (`user_not_found`).\n",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "413": {
            "description": "Message text exceeds the maximum allowed size (`msg_too_long`).",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/chat.scheduled.list": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "List scheduled messages",
        "description": "Lists pending messages scheduled via [`/chat.post`](/docs/api/chat-post)'s `sendAt`\nthat have not been sent yet. Results are ordered ascending by `sendAt` (soonest\nfirst). Sent and canceled messages are not returned.\n\nOnly messages scheduled by the calling credential's bot identity are listed:\norganization tokens of the same app share the app's bot identity (and therefore\nsee each other's scheduled messages), while personal access tokens have a\nper-person bot identity and see only their own.\n\n**Access:** Organization and Personal.\n\n**Required scope:** `chat:send_message` or `chat:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "chat.scheduled.list",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "chatId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Only return messages scheduled for this chat."
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only return messages scheduled to send after this datetime\n(YYYY-MM-DD or RFC-3339). Exclusive.\n"
          },
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only return messages scheduled to send before this datetime\n(YYYY-MM-DD or RFC-3339). Exclusive.\n"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 10
            },
            "description": "The number of scheduled messages to return per response. Default is 10."
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "scheduledMessages": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "scheduledMessageId": {
                            "type": "string",
                            "format": "uuid",
                            "description": "The id returned by `/chat.post` when the message was scheduled; pass to `/chat.scheduled.cancel`."
                          },
                          "chatId": {
                            "type": "string",
                            "format": "uuid",
                            "description": "The chat the message will be posted to."
                          },
                          "threadTimestamp": {
                            "type": "integer",
                            "description": "Thread the message will post into (present only when scheduled with `threadTimestamp`; unix micros, matching `/chat.post` and `/chat.history` message keys)."
                          },
                          "sendAt": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When the message is scheduled to send (RFC 3339)."
                          },
                          "createdAt": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When the message was scheduled (RFC 3339)."
                          },
                          "text": {
                            "type": "string",
                            "description": "Preview snippet of the message text, truncated server-side. Empty for non-text content such as Block Kit messages."
                          }
                        }
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "description": "Returned if there is a subsequent page of scheduled messages."
                    }
                  }
                },
                "example": {
                  "scheduledMessages": [
                    {
                      "scheduledMessageId": "0197f9f0-5cc1-7d07-8a12-9e65a8a0c1b9",
                      "chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176",
                      "sendAt": "2026-08-01T14:30:00Z",
                      "createdAt": "2026-07-20T09:12:44Z",
                      "text": "Reminder: sprint review starts in 15 minutes"
                    }
                  ],
                  "nextCursor": "YzE6MTc1NDA2MzgwMDAwMDowMTk3ZjlmMC01Y2Mx"
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- Invalid `chatId`, `after`, `before`, or `limit`\n- Invalid or expired `cursor` (`invalid_cursor`) — restart without a cursor\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "Token lacks a required scope (`missing_scope`).",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occured."
          }
        }
      }
    },
    "/chat.scheduled.cancel": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Cancel a scheduled message",
        "description": "Cancels a pending message scheduled via [`/chat.post`](/docs/api/chat-post)'s\n`sendAt`, so it will never be delivered. Pending scheduled messages can be\ndiscovered with [`/chat.scheduled.list`](/docs/api/chat-scheduled-list).\n\nOnly the credential's bot identity that scheduled the message may cancel it. A\n`scheduledMessageId` scheduled by a different identity — or one that never\nexisted — returns `scheduled_message_not_found`; the endpoint does not reveal\nwhether such an id exists. Canceling a message that has already been sent\nreturns `scheduled_message_already_sent`.\n\nCancellation is best-effort once the scheduled send time arrives: delivery of a\ndue message begins in the seconds after its `sendAt` boundary, and a cancel\nissued inside that window may return success while the message is still\ndelivered. Cancel ahead of the scheduled time to be safe.\n\n**Access:** Organization and Personal.\n\n**Required scope:** `chat:send_message` or `chat:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "chat.scheduled.cancel",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "scheduledMessageId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The id returned by `/chat.post` when the message was scheduled."
                  }
                },
                "required": [
                  "scheduledMessageId"
                ]
              },
              "examples": {
                "cancel_scheduled_message": {
                  "summary": "Cancel a scheduled message",
                  "value": {
                    "scheduledMessageId": "0197f9f0-5cc1-7d07-8a12-9e65a8a0c1b9"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Scheduled message canceled; it will not be delivered.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "scheduledMessageId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The canceled scheduled message id."
                    }
                  }
                },
                "example": {
                  "scheduledMessageId": "0197f9f0-5cc1-7d07-8a12-9e65a8a0c1b9"
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- Missing `scheduledMessageId` (`missing_parameter`)\n- Malformed JSON body (`invalid_json`)\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "Token lacks a required scope (`missing_scope`).",
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "The scheduled message could not be found, or was scheduled by a different credential (`scheduled_message_not_found`).",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "description": "The message was already sent (or its delivery has begun) and can no longer be canceled (`scheduled_message_already_sent`).",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occured."
          }
        }
      }
    },
    "/chat.startStream": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Start a streaming message",
        "description": "Open a streaming message and post its first content. Streaming lets a bot\ndeliver a message incrementally — recipients see the text fill in live (with\na \"typing…\" indicator) instead of waiting for the full response. This is\nuseful for AI agents that produce text token-by-token.\n\nA stream has three steps, each its own request:\n\n1. **[`/chat.startStream`](/docs/api/chat-start-stream)** — open the stream and pick the destination. Returns a `streamId`.\n2. **[`/chat.appendStream`](/docs/api/chat-append-stream)** — append chunks of text (call as many times as needed).\n3. **[`/chat.stopStream`](/docs/api/chat-stop-stream)** — finalize the stream into a single persisted message.\n\nPass the `streamId` returned here to every subsequent `appendStream` and\n`stopStream`. The sender, destination, and thread are fixed for the lifetime\nof the stream.\n\n**Custom sender (optional):** same semantics as\n[`/chat.post`](/docs/api/chat-post) — `sender.name` / `sender.imageUrl`\napply a per-message display override to the finalized message, and\n`sender.id` authors the stream as a configured bot persona (unknown ids\nare accepted and ignored). The typing indicator shown while streaming uses\nthe override name when given, otherwise the persona's or app's configured\nname. See the [Sender Profiles guide](/docs/guides/sender-profiles).\n\n**Access:** Organization and Personal. Organization tokens follow the\nsame public-group carveout as [`/chat.post`](/docs/api/chat-post): the\nbot may stream into a public group in its roam without joining.\nPersonal tokens can stream only where the owner is a member\n(`403` `not_in_chat` for an unjoined public group) and reject the\n`sender` field.\n\n**Required scope:** `chat:send_message` or `chat:write`\n\n## Destination\n\nProvide exactly one of `chatId`, `groupId`, or `userIds`. If `text` is empty,\nthe destination is recorded but message creation is deferred until the first\nnon-empty `appendStream` or the `stopStream` call.\n\n## Thinking streams\n\nSet `kind` to `thinking` to finalize the message as a thought-bubble; clients\nshow a \"thinking…\" indicator instead of \"typing…\". The default `kind` is `text`.\n\n## Limits\n\n- Up to **10 concurrent streams per API client**.\n- Only **one active stream per chat** at a time.\n- Accumulated text may not exceed the regular message size limit.\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "chat.startStream",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "chatId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Stream into an existing chat by ID (mutually exclusive with groupId/userIds)."
                  },
                  "groupId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Stream into a group chat (mutually exclusive with chatId/userIds)."
                  },
                  "userIds": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "Stream into a DM or Multi-DM with these users (mutually exclusive with chatId/groupId)."
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "text",
                      "thinking"
                    ],
                    "default": "text",
                    "description": "Stream kind. `thinking` finalizes as a thought-bubble message."
                  },
                  "threadTimestamp": {
                    "type": "integer",
                    "description": "Optional thread to reply within."
                  },
                  "text": {
                    "type": "string",
                    "description": "Optional initial text. May be empty to defer destination resolution until the first append/stop."
                  },
                  "sender": {
                    "$ref": "#/components/schemas/Sender"
                  }
                }
              },
              "examples": {
                "start": {
                  "summary": "Start a text stream",
                  "value": {
                    "groupId": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                    "text": "Let me look into that..."
                  }
                },
                "deferred": {
                  "summary": "Deferred start (resolve on first append)",
                  "value": {
                    "groupId": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                    "text": ""
                  }
                },
                "thinking": {
                  "summary": "Start a thinking stream",
                  "value": {
                    "groupId": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                    "kind": "thinking",
                    "text": "Considering the trade-offs"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stream started.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "streamId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "Unique ID for this stream. Pass it to appendStream and stopStream."
                    },
                    "chatId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "ID of the destination chat."
                    },
                    "threadTimestamp": {
                      "type": "integer",
                      "description": "Thread timestamp if the stream is a thread reply."
                    }
                  }
                },
                "example": {
                  "streamId": "018f5c8e-7d2a-7c4e-8f9a-1a2b3c4d5e6f",
                  "chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176"
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- No destination, or multiple destinations (exactly one of chatId/groupId/userIds is required)\n- Unknown `kind` (must be `text` or `thinking`)\n- Invalid thread timestamp, or threading a destination that does not support it\n- Another stream is already active in the target chat\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "The token lacks the required scope (`chat:send_message` or `chat:write`).",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "413": {
            "description": "Initial text exceeds the maximum allowed message size.",
            "$ref": "#/components/responses/Error"
          },
          "429": {
            "description": "Too many concurrent streams (max 10 active per API client).",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/chat.appendStream": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Append to a streaming message",
        "description": "Append a chunk of text to an open stream (see\n[`/chat.startStream`](/docs/api/chat-start-stream)). Each chunk is broadcast\nto recipients as a delta, so the message appears to fill in live. Call as\nmany times as needed before [`/chat.stopStream`](/docs/api/chat-stop-stream).\n\n**Required scope:** `chat:send_message` or `chat:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "chat.appendStream",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "streamId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The stream ID returned by chat.startStream."
                  },
                  "text": {
                    "type": "string",
                    "description": "Text chunk to append. Required and non-empty."
                  },
                  "snapshot": {
                    "type": "boolean",
                    "description": "If `true`, **replace** the accumulated text with `text` (and broadcast it\nas a full snapshot) instead of appending. Useful when the client holds the\ncanonical current state — for example after rewriting prior output. The\nmessage size limit is applied to the new `text` alone.\n"
                  }
                },
                "required": [
                  "streamId",
                  "text"
                ]
              },
              "example": {
                "streamId": "018f5c8e-7d2a-7c4e-8f9a-1a2b3c4d5e6f",
                "text": " The answer is 42."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Chunk appended.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "streamId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "chatId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "threadTimestamp": {
                      "type": "integer",
                      "description": "Thread timestamp if the stream is a thread reply."
                    }
                  }
                },
                "example": {
                  "streamId": "018f5c8e-7d2a-7c4e-8f9a-1a2b3c4d5e6f",
                  "chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176"
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- `streamId` missing or not a valid UUID\n- `text` missing or empty\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "The token lacks the required scope (`chat:send_message` or `chat:write`).",
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "No active stream with that `streamId` for this app.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "413": {
            "description": "Accumulated stream text exceeds the maximum allowed size.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/chat.stopStream": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Stop a streaming message",
        "description": "Finalize an open stream (see [`/chat.startStream`](/docs/api/chat-start-stream))\ninto a single persisted chat message and return its timestamp. Optionally\ninclude trailing `text` to append before finalizing.\n\nIf the app never calls `stopStream` but has already streamed some text, the\nserver finalizes the buffered text into a message automatically.\n\n**Required scope:** `chat:send_message` or `chat:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "chat.stopStream",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "streamId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The stream ID returned by chat.startStream."
                  },
                  "text": {
                    "type": "string",
                    "description": "Optional trailing text appended before the message is finalized."
                  }
                },
                "required": [
                  "streamId"
                ]
              },
              "example": {
                "streamId": "018f5c8e-7d2a-7c4e-8f9a-1a2b3c4d5e6f",
                "text": " Hope that helps!"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stream finalized and message persisted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "streamId": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "chatId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "ID of the chat where the message was posted."
                    },
                    "timestamp": {
                      "type": "integer",
                      "description": "Timestamp of the finalized message (microseconds since epoch)."
                    },
                    "threadTimestamp": {
                      "type": "integer",
                      "description": "Thread timestamp if the stream was a thread reply."
                    }
                  }
                },
                "example": {
                  "streamId": "018f5c8e-7d2a-7c4e-8f9a-1a2b3c4d5e6f",
                  "chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176",
                  "timestamp": 1765602474760032
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- `streamId` missing or not a valid UUID\n- The finalized message would have no content\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "The token lacks the required scope (`chat:send_message` or `chat:write`).",
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "No active stream with that `streamId` for this app.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "413": {
            "description": "Accumulated stream text exceeds the maximum allowed size.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/chat.update": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Update a message",
        "description": "Edit a previously posted bot message. The updated message can contain plain markdown text or rich [Block Kit](/docs/guides/block-kit) layouts.\n\nThe bot must own the message being updated (matched by address ID). Personal access tokens always send as their bot persona and may only edit messages that personal bot posted.\n\n**Access:** Organization and Personal.\n\n**Required scope:** `chat:send_message` or `chat:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "chat.update",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Either `text` or `blocks` is required. They cannot be combined — a message is either plain text or Block Kit.\n",
                "properties": {
                  "chatId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "ID of the chat containing the message."
                  },
                  "timestamp": {
                    "type": "integer",
                    "description": "Timestamp of the message to update."
                  },
                  "threadTimestamp": {
                    "type": "integer",
                    "description": "Thread timestamp, if the message is in a thread."
                  },
                  "text": {
                    "type": "string",
                    "format": "markdown",
                    "description": "Updated markdown-formatted text content. Required unless `blocks` is provided.\nCannot be combined with `blocks`.\n"
                  },
                  "markdown": {
                    "type": "boolean",
                    "description": "Text is markdown by default. If this is set to false, markdown interpretation will be disabled."
                  },
                  "items": {
                    "type": "array",
                    "description": "Array of Item IDs to attach to this message. Cannot be combined with `blocks`.",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    }
                  },
                  "assetIds": {
                    "type": "array",
                    "description": "Array of asset IDs from [`/asset.create`](/docs/api/asset-create)\nto attach to this message. Each asset must be owned by your app\nand fully uploaded (processed and ready). Cannot be combined with `blocks`.\n",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    }
                  },
                  "blocks": {
                    "type": "array",
                    "description": "Array of [Block Kit](/docs/guides/block-kit) block objects for rich message formatting.\nCannot be combined with `text` or `items`. Maximum 10 blocks, 8,000 bytes total payload.\n",
                    "items": {
                      "type": "object",
                      "properties": {
                        "type": {
                          "type": "string",
                          "enum": [
                            "header",
                            "section",
                            "context",
                            "divider",
                            "actions"
                          ],
                          "description": "The block type."
                        }
                      }
                    }
                  },
                  "color": {
                    "type": "string",
                    "description": "Colored vertical strip on the side of the message. Only used with `blocks`.\nNamed values: `good` (green), `warning` (yellow), `danger` (red), or a hex color like `#5B3FD9`.\n"
                  }
                },
                "required": [
                  "chatId",
                  "timestamp"
                ]
              },
              "examples": {
                "update_text": {
                  "summary": "Update text content",
                  "value": {
                    "chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176",
                    "timestamp": 1765602474760032,
                    "text": "Updated message content with **bold text**"
                  }
                },
                "update_blocks": {
                  "summary": "Update with Block Kit",
                  "value": {
                    "chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176",
                    "timestamp": 1765602474760032,
                    "blocks": [
                      {
                        "type": "header",
                        "text": {
                          "type": "plain_text",
                          "text": "Build Status Updated"
                        }
                      },
                      {
                        "type": "section",
                        "text": {
                          "type": "mrkdwn",
                          "text": "Deployment *succeeded* for production."
                        }
                      }
                    ],
                    "color": "good"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message updated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "chatId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "ID of the chat."
                    },
                    "timestamp": {
                      "type": "integer",
                      "description": "Timestamp of the updated message."
                    },
                    "threadTimestamp": {
                      "type": "integer",
                      "description": "Thread timestamp, if the message is in a thread."
                    }
                  }
                },
                "example": {
                  "chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176",
                  "timestamp": 1765602474760032
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- Invalid chat ID\n- Invalid or missing `timestamp`\n- A `sender` field was provided — the sender is derived from the original message (see [Sender Profiles](/docs/guides/sender-profiles))\n- Both `text`/`items` and `blocks` provided (mutually exclusive)\n- An `assetId` was not found, not owned by your app, or not a file asset\n- An `assetId` is still processing — retry once its upload completes\n- `blocks` array exceeds 10 blocks or 8,000 bytes\n- Invalid block structure (see [Block Kit guide](/docs/guides/block-kit))\n- Interactive buttons sent without an Interactivity URL configured\n- Invalid `color` value\n- Bot does not own the message\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "Forbidden. The bot lacks access to the target chat.\n",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "413": {
            "description": "Message content exceeds the maximum allowed size.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/chat.delete": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Delete a message",
        "description": "Delete a previously posted bot message. The bot must own the message being deleted (matched by address ID). Personal access tokens always send as their bot persona and may only delete messages that personal bot posted.\n\nDeleting an already-deleted message is idempotent and returns success.\n\n**Access:** Organization and Personal.\n\n**Required scope:** `chat:send_message` or `chat:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "chat.delete",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "chatId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "ID of the chat containing the message."
                  },
                  "timestamp": {
                    "type": "integer",
                    "description": "Timestamp of the message to delete."
                  },
                  "threadTimestamp": {
                    "type": "integer",
                    "description": "Thread timestamp, if the message is in a thread."
                  }
                },
                "required": [
                  "chatId",
                  "timestamp"
                ]
              },
              "examples": {
                "delete_message": {
                  "summary": "Delete a message",
                  "value": {
                    "chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176",
                    "timestamp": 1765602474760032
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Message deleted successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "chatId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "ID of the chat."
                    },
                    "timestamp": {
                      "type": "integer",
                      "description": "Timestamp of the deleted message."
                    },
                    "threadTimestamp": {
                      "type": "integer",
                      "description": "Thread timestamp, if the message was in a thread."
                    }
                  }
                },
                "example": {
                  "chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176",
                  "timestamp": 1765602474760032
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- Invalid chat ID\n- Invalid or missing `timestamp`\n- A `sender` field was provided — the sender is derived from the original message (see [Sender Profiles](/docs/guides/sender-profiles))\n- Bot does not own the message\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "Forbidden. The bot lacks access to the target chat.\n",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/chat.typing": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Send a typing indicator",
        "description": "Notify other chat participants that you are working on a response.\nIf they have the chat open, they will see \"(Bot name) is typing...\".\n\nThe indicator lasts **6 seconds**. Re-send every **5 seconds** to keep\nit visible while you work. Longer gaps will let it expire between pings.\n\n**Destination options (mutually exclusive):**\n- `chatId` - Send to an existing chat by its ID\n- `groupId` - Send to a group channel\n- `userIds` - Send to a DM or Multi-DM with the specified users\n\n**Custom sender (optional):** pass `sender.id` to show the indicator as a\n[configured bot persona](/docs/guides/sender-profiles) — the persona's\nconfigured name and avatar are used. Only `id` is accepted; `name` and\n`imageUrl` are rejected on this endpoint. Selection is lookup-only: an id\nthat doesn't match a configured persona is accepted and ignored, and the\nindicator shows the app's own identity (same for an omitted, empty, or `_`\nid). Personal access tokens reject `sender` entirely.\n\n**Required scope:** `chat:send_message` or `chat:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "chat.typing",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "chatId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Send to an existing chat by ID (mutually exclusive with groupId/userIds)"
                  },
                  "groupId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Send to a group channel (mutually exclusive with chatId/userIds)"
                  },
                  "userIds": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "Send to a DM or Multi-DM with these users (mutually exclusive with chatId/groupId)"
                  },
                  "threadTimestamp": {
                    "type": "integer",
                    "description": "Timestamp of the message being replied to."
                  },
                  "sender": {
                    "type": "object",
                    "description": "Optional configured bot persona to show the indicator as. Only\n`id` is accepted — `name` and `imageUrl` are rejected on this\nendpoint. Personal access tokens reject this field entirely.\nSee the [Sender Profiles guide](/docs/guides/sender-profiles).\n",
                    "properties": {
                      "id": {
                        "type": "string",
                        "description": "Code of a configured bot persona (lookup-only; never\ncreates one). Ids that don't match a configured persona\nare accepted and ignored.\n"
                      }
                    }
                  }
                }
              },
              "examples": {
                "typingInChat": {
                  "summary": "Typing in existing chat",
                  "value": {
                    "chatId": "295155ae-7df5-4ed5-9ebc-89a170559c81"
                  }
                },
                "typingInGroup": {
                  "summary": "Typing in a group",
                  "value": {
                    "groupId": "88bebce7-6cbb-4666-96f9-5c02d73e6661"
                  }
                },
                "typingInThread": {
                  "summary": "Typing in a thread",
                  "value": {
                    "chatId": "295155ae-7df5-4ed5-9ebc-89a170559c81",
                    "threadTimestamp": 1765602474760032
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Typing indicator successfully sent"
          },
          "400": {
            "description": "Bad request. Common causes:\n- No destination specified (chatId, groupId, or userIds required)\n- Multiple destinations specified (only one allowed)\n- `sender.name` or `sender.imageUrl` provided (not supported on typing)\n- Personal access token provided a `sender` field\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/chat.history": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "Get chat messages",
        "description": "List messages in a chat, filtered by date range (after/before).\n\nMessages with `contentType` of `text`, `voice`, or `poll` are returned. System messages and other content types are excluded.\n\n**Specify ONE of the following:**\n- `chatId` - Fetch from an existing chat by its ID\n- `groupId` - Fetch from a group chat\n- `userIds` - Fetch from a DM or Multi-DM with the specified users\n\nYou must specify exactly one destination. Specifying multiple (e.g., both `chatId` and `groupId`) will return a 400 error.\n\nThe ordering of results depends on the filter specified:\n\n- When no parameters are provided, the most recent messages are returned,\n  sorted in reverse chronological order. This is equivalent to specifying `before`\n  as NOW and leaving `after` unspecified.\n\n- If `after` is specified, the results are sorted in forward chronological order.\n\nEither dates or datetimes may be specified. Date-only inputs (`YYYY-MM-DD`)\nare interpreted in the caller's timezone (see\n[Timezone handling](/docs/guides/migration-v0-to-v1#timezone-handling)).\n\n**Access:** Organization tokens need to be a **member** of the chat\n(`403` `not_in_chat` otherwise). Personal tokens can read any chat the\nowner can, including public groups in their roam they have not joined.\n\n**Required scope:** `chat:history`\n\nEvery returned sender includes `userId` plus `userType`. The ID resolves\nthrough [`user.info`](/docs/api/user-info) with the same credentials.\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "chat.history",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "chatId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The chat ID to fetch messages from. Either chatId, groupId, or userIds must be specified.",
            "required": false
          },
          {
            "name": "groupId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Group chat ID to fetch messages from. Either chatId, groupId, or userIds must be specified.",
            "required": false
          },
          {
            "name": "userIds",
            "in": "query",
            "schema": {
              "type": "array",
              "items": {
                "type": "string",
                "format": "uuid"
              }
            },
            "description": "User IDs to fetch DM/Multi-DM messages with. Either chatId, groupId, or userIds must be specified.",
            "required": false
          },
          {
            "name": "threadTimestamp",
            "in": "query",
            "schema": {
              "type": "number"
            },
            "description": "Read replies of the message with this timestamp. Specified in microseconds.",
            "required": false
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "datetime"
            },
            "description": "The datetime to begin listing messages (YYYY-MM-DD or RFC-3339).\nDate-only values are interpreted in the caller's timezone.\nSub-millisecond precision on datetimes is truncated. Defaults to\n\"no filter\".\n"
          },
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "datetime"
            },
            "description": "The datetime until which to list messages (YYYY-MM-DD or RFC-3339).\nDate-only values are interpreted in the caller's timezone.\nSub-millisecond precision on datetimes is truncated. Defaults to\n\"now\".\n"
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor from a previous response's `nextCursor`.",
            "required": false
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 10,
              "maximum": 200
            },
            "description": "Number of messages to return (default 10, max 200).",
            "required": false
          },
          {
            "name": "expand",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated fields to expand. Supported: `addresses` — include an\n`addresses` map resolving the sender (`userId`) and mentioned IDs on\neach message to their display info.\n",
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Messages retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "chatId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The chat ID"
                    },
                    "nextCursor": {
                      "type": "string",
                      "description": "A cursor to fetch the next page of results"
                    },
                    "threadTimestamp": {
                      "type": "integer",
                      "description": "The thread timestamp being read, echoed from the request (present only when reading a thread)."
                    },
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChatMessage"
                      }
                    },
                    "addresses": {
                      "type": "object",
                      "additionalProperties": {
                        "$ref": "#/components/schemas/Address"
                      },
                      "description": "Resolved address objects keyed by ID, for the senders and\nmentioned entities in this response. Included only when\n`expand=addresses` is requested.\n"
                    }
                  },
                  "required": [
                    "chatId",
                    "messages"
                  ]
                },
                "example": {
                  "chatId": "295155ae-7df5-4ed5-9ebc-89a170559c81",
                  "messages": [
                    {
                      "type": "message",
                      "contentType": "text",
                      "userId": "ad1e9cc0-0ffd-47e5-895c-2630a73327b4",
                      "userType": "user",
                      "timestamp": 1765602474760032,
                      "text": "Hey team, can we sync up on the roadmap this afternoon?"
                    },
                    {
                      "type": "message",
                      "contentType": "text",
                      "userId": "709b8a57-70bc-427a-b6f0-b16ba5297f8c",
                      "userType": "bot",
                      "timestamp": 1765602480123456,
                      "text": "Sure, I'm free at 3pm!"
                    }
                  ],
                  "nextCursor": "YzE6MTc2NTYwMjQ3NDc2MDAzMg"
                }
              }
            }
          },
          "400": {
            "description": "Bad request.",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "Organization token is not a member of this chat (`not_in_chat`).\nPersonal tokens receive this only when the owner cannot read the chat\n(private / DM they are not in).\n",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred, including a stored sender that cannot resolve as a visible principal. The endpoint does not return a partial page."
          }
        }
      }
    },
    "/chat.search": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Search chat messages",
        "description": "Full-text search over the caller's accessible messages. Returns\nfull-fidelity messages — text, items, voice, polls, blocks, and\nmentions — hydrated through the same pipeline as\n[`/chat.history`](/docs/api/chat-history).\n\nAll fields are optional. With no parameters, the most recent messages\nacross all chat types (DMs, multi-DMs, group chats) are returned in\nreverse chronological order.\n\n**Sort:** When omitted and `query` is empty, results are sorted\nchronologically (newest first), since relevance scoring is meaningless\nwithout search terms. Pass `sort: recent` to force chronological order\neven with a text query.\n\n**Date filters:** `before` and `after` accept `YYYY-MM-DD`. Dates are\ninterpreted in the caller's timezone (see\n[Timezone handling](/docs/guides/migration-v0-to-v1#timezone-handling)).\n\n**Access:** Organization and Personal.\n\n- **Personal tokens** search chats the owner can read, including public\n  groups in their roam they have not joined.\n- **Organization tokens** search chats the bot is a **member** of,\n  plus unjoined **public** groups in the bot's roam (Slack\n  `search:read.public`). Private groups the bot is not in are excluded.\n  [`/chat.history`](/docs/api/chat-history) stays membership-only.\n\n**Required scope:** `chat:history`\n\nEvery returned sender includes `userId` plus `userType`. The ID resolves\nthrough [`user.info`](/docs/api/user-info) with the same credentials.\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "chat.search",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "query": {
                    "type": "string",
                    "description": "Free-text search query. Empty matches all messages."
                  },
                  "in": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Group names to search within."
                  },
                  "from": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "email"
                    },
                    "description": "Filter to messages sent by these email addresses."
                  },
                  "with": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "email"
                    },
                    "description": "Filter to chats including these email addresses."
                  },
                  "before": {
                    "type": "string",
                    "description": "Only include messages before this date (`YYYY-MM-DD`, caller's timezone)."
                  },
                  "after": {
                    "type": "string",
                    "description": "Only include messages on or after this date (`YYYY-MM-DD`, caller's timezone)."
                  },
                  "has": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "mention",
                        "item"
                      ]
                    },
                    "description": "Restrict to messages that contain a mention or an item."
                  },
                  "chatTypes": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "channel",
                        "teamRoam",
                        "address"
                      ]
                    },
                    "description": "Restrict to specific chat types. Defaults to all types\n(channels, all-hands \"team Roam\" groups, and DMs).\n"
                  },
                  "excludeChatIds": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "Chat IDs to exclude from results."
                  },
                  "excludeUserIds": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "Sender user IDs to exclude from results."
                  },
                  "sort": {
                    "type": "string",
                    "enum": [
                      "relevant",
                      "recent"
                    ],
                    "description": "`relevant` (default) ranks by relevance to `query`; `recent`\nsorts newest first. With an empty `query`, results are\nsorted chronologically regardless.\n"
                  },
                  "expand": {
                    "type": "string",
                    "description": "Comma-separated fields to expand. Supported: `addresses` —\ninclude an `addresses` map resolving the sender (`userId`) and\nmentioned IDs on each message to their display info.\n"
                  },
                  "limit": {
                    "type": "integer",
                    "maximum": 200,
                    "description": "Number of messages per page (max 200)."
                  },
                  "cursor": {
                    "type": "string",
                    "description": "Opaque pagination cursor from a previous response's `nextCursor`."
                  }
                }
              },
              "examples": {
                "recent": {
                  "summary": "List recent messages across all chats",
                  "value": {
                    "after": "2026-04-13",
                    "limit": 20
                  }
                },
                "query": {
                  "summary": "Search for messages mentioning \"roadmap\"",
                  "value": {
                    "query": "roadmap",
                    "has": [
                      "mention"
                    ],
                    "limit": 50
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Search results.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ChatMessage"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "description": "Cursor to fetch the next page. Absent when there are no more results."
                    },
                    "addresses": {
                      "type": "object",
                      "additionalProperties": {
                        "$ref": "#/components/schemas/Address"
                      },
                      "description": "Resolved address objects keyed by ID, for the senders and\nmentioned entities in this response. Included only when\n`expand=addresses` is requested.\n"
                    }
                  },
                  "required": [
                    "messages"
                  ]
                },
                "example": {
                  "messages": [
                    {
                      "type": "message",
                      "contentType": "text",
                      "userId": "ad1e9cc0-0ffd-47e5-895c-2630a73327b4",
                      "userType": "user",
                      "chatId": "295155ae-7df5-4ed5-9ebc-89a170559c81",
                      "timestamp": 1765602474760032,
                      "text": "Let's review the Q2 roadmap on Friday."
                    }
                  ],
                  "nextCursor": "YzE6MTc2NTYwMjQ3NDc2MDAzMjoyOTUxNTVhZS03ZGY1"
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- Invalid `chatTypes` value\n- Invalid `excludeChatIds` or `excludeUserIds` (must be UUIDs)\n- Invalid `before`/`after` date format\n- Invalid `from`/`with` email addresses\n- Invalid `sort` value\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "Forbidden. The token lacks the required scope.\n",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred, including a stored sender that cannot resolve as a visible principal. The endpoint does not return a partial page."
          }
        }
      }
    },
    "/chat.link.resolve": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Resolve a chat link",
        "description": "Parse a Roam chat deep link (e.g. `https://ro.am/r/#/d/...`) and return the\nreferenced message.\n\nWhen the caller has access to the referenced chat, the full message is\nreturned and `readable` is `true`. The `message` object is the same\nshape as a `chat.history`/`chat.search` message — same fields, same\nmention rendering. When the caller lacks access, the response still\nincludes the message key (`chatId`, `timestamp`, and `threadTimestamp`\nif applicable) with `readable: false` and no message content — suitable\nfor rendering a reference without leaking content.\n\nUse [`/chat.link.create`](/docs/api/chat-link-create) for the reverse\noperation — minting a shareable Roam link from a message the caller can\nalready read.\n\n**Access:** Organization and Personal.\n\n**Required scope:** `chat:history`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "chat.link.resolve",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "link": {
                    "type": "string",
                    "description": "A Roam chat deep link URL that contains a message reference."
                  }
                },
                "required": [
                  "link"
                ]
              },
              "example": {
                "link": "https://ro.am/r/#/d/abc123xyz/c/757dfe66-37b4-4772-baa5-8c86ec68c176?ts=1765602474760032"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Link resolved. When `readable` is false the caller lacks access to the chat; only the message key is returned.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "chatId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "ID of the chat referenced by the link."
                    },
                    "timestamp": {
                      "type": "integer",
                      "description": "Timestamp of the referenced message (microseconds)."
                    },
                    "threadTimestamp": {
                      "type": "integer",
                      "description": "Thread timestamp if the referenced message is in a thread."
                    },
                    "readable": {
                      "type": "boolean",
                      "description": "`true` if the caller has access to the chat and `message` is populated.\n`false` if the caller lacks access; no message content is returned.\n"
                    },
                    "message": {
                      "$ref": "#/components/schemas/ChatMessage"
                    }
                  },
                  "required": [
                    "chatId",
                    "timestamp",
                    "readable"
                  ]
                },
                "examples": {
                  "readable": {
                    "summary": "Caller has access",
                    "value": {
                      "chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176",
                      "timestamp": 1765602474760032,
                      "readable": true,
                      "message": {
                        "userId": "709b8a57-70bc-427a-b6f0-b16ba5297f8c",
                        "chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176",
                        "timestamp": 1765602474760032,
                        "contentType": "text",
                        "text": "Hello from the API"
                      }
                    }
                  },
                  "not_readable": {
                    "summary": "Caller lacks access",
                    "value": {
                      "chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176",
                      "timestamp": 1765602474760032,
                      "readable": false
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- Missing `link`\n- Link cannot be parsed\n- Link does not contain a message reference\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "The referenced message does not exist.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/chat.link.create": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Create a chat message link",
        "description": "Create a shareable Roam link to a specific chat message. Opening the link\nin Roam navigates to that message in its chat.\n\nIdentify the chat with exactly one of `chatId`, `groupId`, or `userIds`,\nand the message by its `timestamp` (Unix microseconds), as returned by\n[`/chat.history`](/docs/api/chat-history), [`/chat.post`](/docs/api/chat-post),\nor webhook message events. For a thread reply, also pass the thread root's\ntimestamp as `threadTimestamp` — without it the reply will not be found.\n\nThe message must exist and be readable by the caller; otherwise no link is\nreturned (`404` if the message does not exist, `403` if the caller is not a\nmember of the chat). The link itself does not grant access: recipients can\nonly open it if they are members of the chat.\n\nUse [`/chat.link.resolve`](/docs/api/chat-link-resolve) for the reverse\noperation — turning a Roam chat link back into the referenced message.\n\n**Access:** Organization and Personal. In Personal mode, only chats the\nauthenticated user can access are allowed.\n\n**Required scope:** `chat:history`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "chat.link.create",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "chatId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "ID of the chat containing the message. Exactly one of `chatId`, `groupId`, or `userIds` is required."
                  },
                  "groupId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "ID of a group whose channel chat contains the message."
                  },
                  "userIds": {
                    "type": "array",
                    "maxItems": 16,
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "User ID(s) identifying the DM or group DM containing the message."
                  },
                  "timestamp": {
                    "type": "integer",
                    "format": "int64",
                    "description": "The message's timestamp in Unix microseconds."
                  },
                  "threadTimestamp": {
                    "type": "integer",
                    "format": "int64",
                    "description": "The thread root's timestamp in Unix microseconds. Required when the message is a thread reply."
                  }
                },
                "required": [
                  "timestamp"
                ]
              },
              "example": {
                "chatId": "295155ae-7df5-4ed5-9ebc-89a170559c81",
                "timestamp": 1765602474760032
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Link created successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "link": {
                      "type": "string",
                      "format": "uri",
                      "description": "Shareable Roam link that opens the message."
                    },
                    "chatId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "ID of the chat containing the message."
                    },
                    "timestamp": {
                      "type": "integer",
                      "format": "int64",
                      "description": "The message's timestamp in Unix microseconds."
                    },
                    "threadTimestamp": {
                      "type": "integer",
                      "format": "int64",
                      "description": "The thread root's timestamp. Omitted for top-level messages."
                    }
                  },
                  "required": [
                    "link",
                    "chatId",
                    "timestamp"
                  ]
                },
                "example": {
                  "link": "https://ro.am/r/#/c/KVFVrn31TtWevImhcFWcgQ/MTc2NTYwMjQ3NDc2MDAzMi9ub3Rocg",
                  "chatId": "295155ae-7df5-4ed5-9ebc-89a170559c81",
                  "timestamp": 1765602474760032
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- Missing `timestamp`\n- Timestamps that are not positive microseconds\n- Missing or multiple destinations (`chatId`, `groupId`, `userIds`)\n- Destination cannot be resolved to a chat\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "Caller is not a member of this chat.",
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "No message exists at the given timestamp. For thread replies, include `threadTimestamp`.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred.",
            "$ref": "#/components/responses/Error"
          }
        }
      }
    },
    "/chat.unfurl": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Unfurl a link",
        "description": "Attach app-provided preview cards to links in an existing text message.\nEvery map key must be an exact URL currently present in the message and\nmust match one of the app's registered unfurl domains. Validation is\natomic: if any entry is invalid, no previews are changed.\n\nApp previews replace Roam-generated previews for the same exact URL while\npreserving unrelated previews. The server does not fetch any URL supplied\nin this request.\n\n**Access:** Organization only (API Key or OAuth). Register unfurl domains on\nthe API client first — see [Unfurling links](/docs/guides/unfurling-links).\nPersonal Access Tokens cannot register domains or call this endpoint.\n\n**Required scope:** `links:write`\n",
        "operationId": "chat.unfurl",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "chatId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "messageTimestamp": {
                    "type": "integer",
                    "description": "Timestamp of a top-level or threaded message in Unix microseconds."
                  },
                  "unfurls": {
                    "type": "object",
                    "minProperties": 1,
                    "description": "Preview content keyed by the exact URL from the message.",
                    "additionalProperties": {
                      "$ref": "#/components/schemas/UnfurlContent"
                    }
                  }
                },
                "required": [
                  "chatId",
                  "messageTimestamp",
                  "unfurls"
                ]
              },
              "example": {
                "chatId": "8f3b9c2e-1a4d-4e7b-9c0a-2b6d1f5e3a7c",
                "messageTimestamp": 1748906400000000,
                "unfurls": {
                  "https://status.example.com/incidents/123": {
                    "title": "Incident 123",
                    "description": "Investigating elevated errors",
                    "siteName": "PagerDuty",
                    "favicon": "https://status.example.com/favicon.png",
                    "image": {
                      "url": "https://status.example.com/incident.png",
                      "type": "image/png",
                      "width": 1200,
                      "height": 630,
                      "alt": "Incident status"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Preview cards applied successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    }
                  },
                  "required": [
                    "ok"
                  ]
                },
                "example": {
                  "ok": true
                }
              }
            }
          },
          "400": {
            "description": "Invalid parameters or `cannot_unfurl_url` when a URL is absent from the message or outside the app's registered domains.",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "Missing `links:write` scope or no access to the chat.",
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "The chat or message was not found.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "409": {
            "description": "The message changed twice while previews were being applied; retry the request.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/reaction.add": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Add reaction to message",
        "description": "Add a reaction to a message in a chat.\n\nTo react to a thread reply, provide the `threadTimestamp` of the parent message\nand the `timestamp` of the specific reply.\n\n**Access:** The organization bot or personal-token **owner** must be a\nmember of the chat (`403` `not_in_chat` otherwise).\n\n**Required scope:** `chat:send_message` or `chat:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "reaction.add",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "chatId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The chat containing the message to react to."
                  },
                  "timestamp": {
                    "type": "integer",
                    "description": "Timestamp of the message to react to (Unix microseconds)."
                  },
                  "threadTimestamp": {
                    "type": "integer",
                    "description": "Timestamp of the parent thread message (Unix microseconds), if reacting to a thread reply."
                  },
                  "name": {
                    "type": "string",
                    "description": "Name of the reaction to add (e.g. \"thumbs_up\", \"heart\", \"100\")."
                  }
                },
                "required": [
                  "chatId",
                  "timestamp",
                  "name"
                ]
              },
              "examples": {
                "reactToMessage": {
                  "summary": "React to a message",
                  "value": {
                    "chatId": "7be17589-4b9a-4524-bddb-ce60abea08e6",
                    "timestamp": 1755723832718034,
                    "name": "thumbs_up"
                  }
                },
                "reactToThreadReply": {
                  "summary": "React to a thread reply",
                  "value": {
                    "chatId": "7be17589-4b9a-4524-bddb-ce60abea08e6",
                    "timestamp": 1755723900000000,
                    "threadTimestamp": 1755723832718034,
                    "name": "heart"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reaction successfully added"
          },
          "400": {
            "description": "Bad request. Common causes:\n- Message not found\n- Invalid reaction name\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "Not a member of this chat (`not_in_chat`). Organization and personal\ntokens both require membership to add a reaction.\n",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/reaction.remove": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Remove reaction from message",
        "description": "Remove a reaction from a message in a chat.\n\nOnly reactions added by the authenticated app can be removed.\n\nTo remove a reaction from a thread reply, provide the `threadTimestamp` of the parent message\nand the `timestamp` of the specific reply.\n\n**Required scope:** `chat:send_message` or `chat:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "reaction.remove",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "chatId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The chat containing the message."
                  },
                  "timestamp": {
                    "type": "integer",
                    "description": "Timestamp of the message (Unix microseconds)."
                  },
                  "threadTimestamp": {
                    "type": "integer",
                    "description": "Timestamp of the parent thread message (Unix microseconds), if removing from a thread reply."
                  },
                  "name": {
                    "type": "string",
                    "description": "Name of the reaction to remove (e.g. \"thumbs_up\", \"heart\")."
                  }
                },
                "required": [
                  "chatId",
                  "timestamp",
                  "name"
                ]
              },
              "example": {
                "chatId": "7be17589-4b9a-4524-bddb-ce60abea08e6",
                "timestamp": 1755723832718034,
                "name": "thumbs_up"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Reaction successfully removed"
          },
          "400": {
            "description": "Bad request. Common causes:\n- Message not found\n- Reaction not found\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "Forbidden. Common causes:\n- Bot does not have access to this chat\n- Reaction was not added by this app\n",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/reaction.list": {
      "get": {
        "tags": [
          "Chat"
        ],
        "summary": "List reactions on a message",
        "description": "List reactions on a specific message, grouped by emoji (Slack-style\n`{name, count, users}`). Poll votes are returned separately in `pollVotes`\nrather than folded into `reactions`.\n\n`users` contains visible principal IDs only. Unknown or unauthorized actors\nare omitted, and `count` is recomputed from the returned IDs. Hydrate them\nwith `user.list?ids`; these arrays do not carry inline type fields.\n\nTo list reactions on a thread reply, provide the `threadTimestamp` of the\nparent message and the `timestamp` of the specific reply.\n\n**Required scope:** `chat:history`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "reaction.list",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "chatId",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The chat containing the message.",
            "required": true
          },
          {
            "name": "timestamp",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Timestamp of the message (Unix microseconds).",
            "required": true
          },
          {
            "name": "threadTimestamp",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Timestamp of the parent thread message (Unix microseconds), if listing reactions on a thread reply.",
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Reactions retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "chatId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The chat containing the message."
                    },
                    "timestamp": {
                      "type": "integer",
                      "description": "Timestamp of the message (Unix microseconds)."
                    },
                    "threadTimestamp": {
                      "type": "integer",
                      "description": "Parent thread timestamp when the message is a thread reply."
                    },
                    "reactions": {
                      "type": "array",
                      "description": "Emoji reactions, one entry per distinct reaction name.",
                      "items": {
                        "$ref": "#/components/schemas/Reaction"
                      }
                    },
                    "pollVotes": {
                      "type": "array",
                      "description": "Poll option votes when the message is a poll. Not included among\n`reactions`.\n",
                      "items": {
                        "type": "object",
                        "properties": {
                          "optionId": {
                            "type": "string",
                            "description": "Poll option identifier."
                          },
                          "text": {
                            "type": "string",
                            "description": "Option display text (omitted if the option no longer resolves)."
                          },
                          "count": {
                            "type": "integer"
                          },
                          "users": {
                            "type": "array",
                            "items": {
                              "type": "string",
                              "format": "uuid"
                            }
                          }
                        },
                        "required": [
                          "optionId",
                          "count",
                          "users"
                        ]
                      }
                    }
                  },
                  "required": [
                    "chatId",
                    "timestamp",
                    "reactions",
                    "pollVotes"
                  ]
                },
                "example": {
                  "ok": true,
                  "chatId": "7be17589-4b9a-4524-bddb-ce60abea08e6",
                  "timestamp": 1755723832718034,
                  "reactions": [
                    {
                      "name": "thumbs_up",
                      "count": 2,
                      "users": [
                        "709b8a57-70bc-427a-b6f0-b16ba5297f8c",
                        "f589a8cb-78ac-493e-8719-0fa8a22f65e0"
                      ]
                    },
                    {
                      "name": "heart",
                      "count": 1,
                      "users": [
                        "af6663d5-0f37-4105-95df-4fea20ef7c7c"
                      ]
                    }
                  ],
                  "pollVotes": []
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- Message not found\n- Invalid cursor or parameters\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "Missing `chat:history` scope, or no access to this chat.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/asset.create": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Create a file upload",
        "description": "Create a file asset and get back a self-describing instruction for\nuploading its bytes — the JSON-friendly way to attach a file (image, PDF,\ndocument, …) to a message, or to supply media for a story. Unlike\n[`/item.upload`](/docs/api/item-upload), which takes raw bytes in the\nrequest body, every caller-visible step here is JSON in / JSON out (so it\ncan be driven from MCP and other tool-calling clients), and the file bytes\nnever pass through this API.\n\n**Flow:**\n1. `POST /asset.create` with the file `name` (include the extension, e.g.\n   `photo.png`) and, if known, its `size` in bytes. For stories, also pass\n   `purpose: \"story\"`. The response is an upload instruction: `assetId`,\n   `uploadUrl`, `uploadMethod`, and `uploadHeaders`.\n2. Upload the raw bytes in a **single request**: use `uploadMethod` (a\n   `POST`) against `uploadUrl`, send every header from `uploadHeaders`\n   verbatim, and put the file in the request body. Send the headers exactly\n   as given — they authorize the upload and select the single-request\n   upload protocol; omitting any will cause the upload to fail.\n3. Processing (thumbnails, previews, …) happens automatically once the\n   bytes land. There is no separate \"complete\" call.\n4. Once the asset is ready, use it:\n   - `purpose: \"file\"` (default) — attach via `assetIds` on\n     [`/chat.post`](/docs/api/chat-post) or\n     [`/chat.update`](/docs/api/chat-update)\n   - `purpose: \"story\"` — post via [`/story.post`](/docs/api/story-post)\n\nA freshly-uploaded asset may take a few seconds to process (videos take\nlonger). Endpoints that consume the asset return a 400 with a \"still\nprocessing\" message until processing completes.\n\nThe `uploadUrl` is short-lived; if it expires, call `asset.create` again for\na fresh instruction. Maximum file size is 5 GiB.\n\n## Purposes\n\n| Purpose | Use | Access |\n|---------|-----|--------|\n| `file` (default) | Chat message attachments | Organization and Personal |\n| `story` | Story media (photo or video) | Personal only |\n\nStory assets are owned by the authenticated user (stories are posted as you,\nnot as a bot) and expire about 48 hours after creation. Because the media\nmust outlive the story's 24-hour lifetime, call\n[`/story.post`](/docs/api/story-post) within about 23 hours of creating the\nasset; after that the asset is rejected and a new one must be created.\n\n**Access:** Organization and Personal. `purpose: \"story\"` is Personal only.\n\n**Required scope:** `item:write` for `purpose: \"file\"`; `chat:send_message`\nor `chat:write` for `purpose: \"story\"`.\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "asset.create",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "File name, including its extension (e.g. `report.pdf`). Processing determines the media type from the extension."
                  },
                  "size": {
                    "type": "integer",
                    "description": "File size in bytes, if known. The true size is enforced\nserver-side during the upload. Maximum 5 GiB.\n"
                  },
                  "purpose": {
                    "type": "string",
                    "enum": [
                      "file",
                      "story"
                    ],
                    "description": "What the asset will be used for. `file` (default) for chat\nmessage attachments; `story` for story media (Personal tokens\nonly).\n"
                  }
                },
                "required": [
                  "name"
                ]
              },
              "examples": {
                "file_attachment": {
                  "summary": "Chat attachment",
                  "value": {
                    "name": "quarterly-report.pdf",
                    "size": 248173
                  }
                },
                "story_media": {
                  "summary": "Story media",
                  "value": {
                    "name": "offsite.mp4",
                    "size": 20971520,
                    "purpose": "story"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Upload instruction created.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "assetId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "ID of the created asset. Pass it to chat.post / chat.update\nvia `assetIds`, or to story.post, once the upload completes.\n"
                    },
                    "uploadUrl": {
                      "type": "string",
                      "format": "uri",
                      "description": "URL to upload the file bytes to."
                    },
                    "uploadMethod": {
                      "type": "string",
                      "description": "HTTP method to use for the upload request (always `POST`)."
                    },
                    "uploadHeaders": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "string"
                      },
                      "description": "Headers to send verbatim on the upload request. They authorize\nthe upload and select the single-request upload protocol.\n"
                    }
                  },
                  "required": [
                    "assetId",
                    "uploadUrl",
                    "uploadMethod",
                    "uploadHeaders"
                  ]
                },
                "example": {
                  "assetId": "9b1c2d3e-4f50-6a7b-8c9d-0e1f2a3b4c5d",
                  "uploadUrl": "https://uploads.ro.am/",
                  "uploadMethod": "POST",
                  "uploadHeaders": {
                    "Authorization": "Bearer eyJhbGciOiJF...",
                    "Upload-Draft-Interop-Version": "6",
                    "Upload-Complete": "?1",
                    "Upload-Length": "248173"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- `name` missing or too long\n- `size` negative or larger than 5 GiB\n- `purpose: \"story\"` used with an organization token\n- Malformed JSON body\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "The token lacks the required scope (`item:write` for file assets;\n`chat:send_message` / `chat:write` for story assets).\n",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/item.upload": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Upload an item",
        "description": "Upload a file so that it can be sent as a chat message attachment.\nThe returned object contains an item ID which can be used with [chat.post](/docs/api/chat-post).\n\nUnlike other endpoints, this uses raw binary upload with metadata in HTTP headers\nrather than JSON. This is more efficient for file transfers.\n\n**Limits:**\n- Maximum file size: 10 MB\n\n**Supported Content Types:**\n\n| Content-Type | In-Product Behavior |\n|--------------|---------------------|\n| `image/png`, `image/jpeg`, `image/gif`, `image/webp` | Displayed inline with preview thumbnail |\n| `application/octet-stream` | Download link only (no preview) |\n\n**Important:** Use `application/octet-stream` for **any file type not listed above** (e.g., `.txt`, `.docx`, `.xlsx`, `.zip`, `.pdf`, etc.).\nThese files will be stored and downloadable, but won't have in-product preview functionality.\n\n**Validation:**\n- The `Content-Type` header must match the actual file content (server validates this for images)\n- For images, if the filename lacks the correct extension, it will be appended automatically\n\n**Required scope:** `item:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "item.upload",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "in": "header",
            "name": "Content-Type",
            "required": true,
            "description": "The MIME type of the file being uploaded.\n\nUse `application/octet-stream` for any file type not specifically listed (txt, docx, xlsx, zip, etc.).\n",
            "schema": {
              "type": "string",
              "enum": [
                "image/png",
                "image/jpeg",
                "image/gif",
                "image/webp",
                "application/octet-stream"
              ]
            }
          },
          {
            "in": "header",
            "name": "Content-Disposition",
            "required": true,
            "description": "Must be in the format: `attachment; filename=\"yourfile.png\"`\n\nThe filename is required and will be used as the item name.\n",
            "schema": {
              "type": "string"
            },
            "example": "attachment; filename=\"screenshot.png\""
          }
        ],
        "requestBody": {
          "description": "The raw binary file content (not base64 encoded, not multipart)",
          "required": true,
          "content": {
            "image/png": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/jpeg": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/gif": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "image/webp": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            },
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Item uploaded successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChatItem"
                },
                "example": {
                  "id": "019be84b-0fa8-788f-8850-96de4cc39130",
                  "type": "photo",
                  "created": "2026-01-21T10:30:00Z",
                  "name": "screenshot.png",
                  "url": "https://ro.am/card-images/019be84b-0fa8-7897-9b52-b064ddb3d185",
                  "thumbnail": "https://ro.am/card-images/019be84b-0fa8-7897-9b52-b064ddb3d185",
                  "size": 245678,
                  "width": 1920,
                  "height": 1080
                }
              }
            }
          },
          "400": {
            "description": "Bad request.",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "415": {
            "description": "An unsupported media type was provided.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occured."
          }
        }
      }
    },
    "/story.post": {
      "post": {
        "tags": [
          "Chat"
        ],
        "summary": "Post story",
        "description": "Posts a story to your Roam. Stories are short photo or video updates that appear\nabove your profile picture for your teammates, and expire 24 hours after posting.\n\n## Posting Flow\n\n1. Create the media asset with [asset.create](/docs/api/asset-create) using\n   `purpose: \"story\"`, and upload the file bytes using the returned upload instructions.\n2. Call this endpoint with the `assetId` (and an optional `caption`).\n\nThe media must be a photo or a video (videos up to 2.5 minutes; media is optimized\nto portrait 1080×1920). If the upload is still processing — typical for videos in\nthe first seconds after upload — this endpoint returns a 400 with a \"still\nprocessing\" message; retry after a short delay.\n\nThe media must outlive the story's 24-hour lifetime, so post within about 23 hours\nof creating the asset (story assets expire about 48 hours after creation); older\nassets are rejected and must be recreated.\n\n**Access:** Personal only. Stories are always posted as the authenticated user —\na story appears above *your* profile picture, and there is no bot persona surface\nfor stories — so organization tokens are rejected.\n\n**Required scope:** `chat:send_message` or `chat:write` (the same permission that\ngates sending a chat message)\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "story.post",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "assetId": {
                    "type": "string",
                    "format": "uuid",
                    "description": "ID of a processed asset created via [asset.create](/docs/api/asset-create)\nwith `purpose: \"story\"`. The asset must be owned by the authenticated user.\n"
                  },
                  "caption": {
                    "type": "string",
                    "maxLength": 2048,
                    "description": "Optional caption displayed with the story."
                  }
                },
                "required": [
                  "assetId"
                ]
              },
              "example": {
                "assetId": "019be84b-0fa8-788f-8850-96de4cc39130",
                "caption": "Greetings from the offsite 👋"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The story was posted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "itemId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "ID of the created story item."
                    },
                    "chatId": {
                      "type": "string",
                      "format": "uuid",
                      "description": "ID of the Roam's story chat the story was posted into."
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time",
                      "description": "When the story expires (24 hours after posting)."
                    }
                  }
                },
                "example": {
                  "itemId": "019be84b-1234-7890-8850-96de4cc39130",
                  "chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176",
                  "expiresAt": "2026-07-10T15:04:05Z"
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- Missing or malformed `assetId`\n- Asset not found, or not owned by the authenticated user\n- Asset was not created with `purpose: \"story\"`\n- Asset is still processing (retry shortly) or failed processing\n- Asset would expire before the story's 24-hour lifetime ends (post within about 23 hours of creating the asset)\n- Media is not a photo or video\n- `caption` exceeds 2,048 characters\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "Forbidden. An organization token was used (stories require a personal access\ntoken), or the token lacks the `chat:send_message`/`chat:write` scope.\n",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/user.list": {
      "get": {
        "summary": "List users",
        "description": "List workspace members, or hydrate an explicit ordered set of principal IDs.\n\nWithout `ids`, this is the active workspace member directory: guests,\nbots, and archived/deactivated members are never enumerated. Members are\nreturned in the order they were added to the account.\n\nWith `ids`, the endpoint becomes an unpaginated principal hydrator. Pass one\ncomma-separated value containing at most 100 bare or tagged IDs. Duplicate\ntokens are deduplicated in first-seen order; resolved entries are returned\nin that order. Unknown IDs, groups, and unauthorized principals are silently\nomitted. Explicit lookup may resolve archived/deactivated users and\nauthorized automated actors. The response keeps the existing `users` key\nbut its entries are principals, and `nextCursor` is omitted.\n\n`ids` cannot be combined with `q`, `limit`, or `cursor`. `expand=status`\nremains supported in either mode.\n\nSee [Identity & Principals](/docs/guides/identity-and-principals).\n\n**Required scope:** `user:read` (add `user:read.email` to include email addresses, `user:read.status` to expand presence status and `willReturn`)\n\n**Access:** Organization and Personal.\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "user.list",
        "security": [
          {
            "bearer": []
          }
        ],
        "tags": [
          "Users"
        ],
        "parameters": [
          {
            "name": "ids",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "One comma-separated list of up to 100 bare or tagged principal IDs.\nRepeating the `ids` query parameter, including empty tokens, or combining\nit with `q`, `limit`, or `cursor` returns `invalid_parameter`.\n"
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive member-directory filter by name. Also matches email\nwhen the token has `user:read.email`. Cannot be combined with `ids`.\n"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 10
            },
            "description": "The number of directory members to return per response. Default is 10. Cannot be combined with `ids`."
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque directory cursor from a previous response's `nextCursor`. Cannot be combined with `ids`."
          },
          {
            "name": "expand",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated list of additional fields. Supported: `status` (requires `user:read.status`). Expanding `status` also returns `willReturn` when set."
          }
        ],
        "responses": {
          "200": {
            "description": "Directory members or explicitly hydrated principals retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "users": {
                      "type": "array",
                      "description": "Principal entries. In directory mode every entry is an active workspace member.",
                      "items": {
                        "$ref": "#/components/schemas/User"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "description": "Pagination cursor for fetching the next page of results"
                    }
                  }
                },
                "examples": {
                  "directory": {
                    "summary": "Unfiltered active-member directory",
                    "value": {
                      "users": [
                        {
                          "id": "709b8a57-70bc-427a-b6f0-b16ba5297f8c",
                          "type": "user",
                          "name": "Alex Chen",
                          "imageUrl": "https://ro.am/card-images/7be550c0-6994-4b8f-9a41-48825c6fc62a",
                          "email": "alex.chen@example.com",
                          "isAdmin": false
                        },
                        {
                          "id": "af6663d5-0f37-4105-95df-4fea20ef7c7c",
                          "type": "user",
                          "name": "Jordan Smith",
                          "imageUrl": "https://ro.am/card-images/41b2a910-e37f-4ffb-9cdd-5be7d05e9f6f",
                          "email": "jordan.smith@example.com",
                          "isAdmin": true
                        }
                      ],
                      "nextCursor": "YzE6NTQ2"
                    }
                  },
                  "ids": {
                    "summary": "Explicit ordered principal hydration (no cursor)",
                    "value": {
                      "users": [
                        {
                          "id": "f589a8cb-78ac-493e-8719-0fa8a22f65e0",
                          "type": "user",
                          "name": "Taylor Guest",
                          "isGuest": true
                        },
                        {
                          "id": "b893c426-6d54-4d9a-8e71-6bd53b26124e",
                          "type": "bot",
                          "name": "Build Bot",
                          "botCode": "build-bot"
                        }
                      ]
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error",
            "description": "Bad request, including malformed/empty IDs, more than 100 raw tokens, repeated `ids` parameters, or `ids` combined with `q`, `limit`, or `cursor`."
          },
          "401": {
            "$ref": "#/components/responses/Error",
            "description": "Presented invalid authentication credentials."
          },
          "405": {
            "$ref": "#/components/responses/Error",
            "description": "An unsupported method was requested."
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/user.info": {
      "get": {
        "summary": "Get user info",
        "description": "Resolve a v1 principal by ID, or look up a workspace member by email.\n\nID lookup resolves active or archived members, guests, and authorized\nautomated actors (classic bots, agents, assistants, and coworkers). The\nresponse always includes `type: \"user\" | \"bot\"`; guests additionally have\n`isGuest: true`. Groups, unknown IDs, and automated actors outside the\ncaller's Roam/account/owner boundary return `user_not_found`.\n\nEmail lookup remains workspace-member-only. Personal access tokens and the\nMCP `user_info` tool may use ID lookup.\n\nProvide either `id` or `email`, not both.\n\nSee [Identity & Principals](/docs/guides/identity-and-principals) for the\ntaxonomy, visibility rules, and directory-versus-hydration guidance.\n\n**Required scope:** `user:read` (add `user:read.email` to look up by email or include email in response, `user:read.status` to expand presence status, availability, and `willReturn`)\n\n**Access:** Organization and Personal.\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "user.info",
        "security": [
          {
            "bearer": []
          }
        ],
        "tags": [
          "Users"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "A bare or tagged principal ID. Mutually exclusive with `email`."
          },
          {
            "name": "email",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "email"
            },
            "description": "The user's email address. Mutually exclusive with `id`. Requires `user:read.email` scope."
          },
          {
            "name": "expand",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated list of additional fields to include. Supported: `status`, `available` (each requires `user:read.status`). Expanding `status` also returns `willReturn` when the user has a future out-of-office entry."
          }
        ],
        "responses": {
          "200": {
            "description": "Principal info retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/User"
                },
                "examples": {
                  "member": {
                    "summary": "Workspace member (false-valued isAdmin is preserved)",
                    "value": {
                      "id": "709b8a57-70bc-427a-b6f0-b16ba5297f8c",
                      "type": "user",
                      "name": "Alex Chen",
                      "imageUrl": "https://ro.am/card-images/7be550c0-6994-4b8f-9a41-48825c6fc62a",
                      "email": "alex.chen@example.com",
                      "isAdmin": false,
                      "status": "checkedOut",
                      "willReturn": {
                        "returnTime": "2026-07-20T09:00:00-07:00",
                        "reason": "On Vacation",
                        "outOfRoam": true
                      },
                      "jobTitle": "Software Engineer",
                      "location": "San Francisco, CA"
                    }
                  },
                  "guest": {
                    "summary": "Guest user",
                    "value": {
                      "id": "f589a8cb-78ac-493e-8719-0fa8a22f65e0",
                      "type": "user",
                      "name": "Taylor Guest",
                      "imageUrl": "https://ro.am/card-images/f589a8cb-78ac-493e-8719-0fa8a22f65e0",
                      "isGuest": true
                    }
                  },
                  "bot": {
                    "summary": "Automated actor",
                    "value": {
                      "id": "b893c426-6d54-4d9a-8e71-6bd53b26124e",
                      "type": "bot",
                      "name": "Build Bot",
                      "imageUrl": "https://ro.am/card-images/b893c426-6d54-4d9a-8e71-6bd53b26124e",
                      "botCode": "build-bot",
                      "integrationId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- Neither `id` nor `email` provided\n- Both `id` and `email` provided\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error",
            "description": "Presented invalid authentication credentials."
          },
          "403": {
            "description": "Forbidden. Common causes:\n- Looking up by email requires `user:read.email` scope\n",
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "Principal not found, not authorized, or the ID belongs to a group (`user_not_found`).",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "$ref": "#/components/responses/Error",
            "description": "An unsupported method was requested."
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/userauditlog.list": {
      "get": {
        "tags": [
          "Users"
        ],
        "summary": "User Audit Log",
        "description": "Get a list of user audit log entries for the account.\n\n**Required scope:** `userauditlog:read`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "userauditlog.list",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "date",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "datetime"
            },
            "description": "The date to pull audit log entries from.  All activities from that date in UTC are returned.\n"
          }
        ],
        "responses": {
          "200": {
            "description": "Audit log entries retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "auditLogs": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/UserAuditLog"
                      }
                    }
                  }
                },
                "example": {
                  "auditLogs": [
                    {
                      "timestamp": "2026-01-21T07:59:59.199Z",
                      "eventType": "ENTER_ROOM",
                      "name": "Alex Chen",
                      "email": "alex.chen@example.com",
                      "data": {
                        "location": {
                          "kind": "RoomLocation",
                          "roomId": 266,
                          "positionNumber": 3
                        }
                      }
                    },
                    {
                      "timestamp": "2026-01-21T07:58:30.000Z",
                      "eventType": "LEAVE_ROOM",
                      "name": "Jordan Smith",
                      "email": "jordan.smith@example.com",
                      "data": {
                        "location": {
                          "kind": "RoomLocation",
                          "roomId": 215,
                          "positionNumber": 14
                        }
                      }
                    },
                    {
                      "timestamp": "2026-01-21T07:55:00.000Z",
                      "eventType": "ENTER_ROOM",
                      "name": "Taylor Williams",
                      "email": "taylor@example.com",
                      "data": {
                        "location": {
                          "kind": "AudienceLocation",
                          "roomId": 215,
                          "sectionNumber": 0,
                          "positionNumber": 46
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error",
            "description": "Invalid request, e.g. date not provided."
          },
          "401": {
            "$ref": "#/components/responses/Error",
            "description": "Unauthorized."
          },
          "500": {
            "description": "An internal error occured."
          }
        }
      }
    },
    "/messageevent.export": {
      "post": {
        "tags": [
          "Users"
        ],
        "summary": "Export message events",
        "description": "Obtain a daily message event export containing DMs and group\nchats within your account.\n\nFor customers with archival enabled (please reach out to a Roam\nArchiTech to get this process started), at the end of every day,\nwe export all message events for a particular day as a JSON Lines file.\nThis file contains all messages sent:\n- by a Roam user who is a member of your organization\n- into a chat containing (at the time of export) at least one Roam user who is a member of your organization\n- by a bot integration that is part of your organization\n\nThis file also contains message edit and deletion events that meet the above criteria.\nWe specifically exclude waves, room invitations, and other non-message content\n(that may appear as chats within the Roam application) from the export.\n\n**Access:** Organization only.\n\n**Required scope:** `admin:compliance:read`\n\n### Message Event Structure\n\nEach line within the file is a JSON object containing the following fields:\n- eventType: a string that is one of “sent”, “edited”, or “deleted”\n- chatId: a UUIDv4 identifier for a particular chat. All messages within the same chat shared the same chatId.\n- threadTimestamp (optional): if part of a thread, the Unix epoch timestamp of the thread’s parent message in numerical format. All messages part of a thread share the same threadTimestamp.\n- timestamp: the Unix epoch timestamp when the message was originally sent in numerical format.\n- messageId: an internal UUIDv4 identifier as a string\n- sender: a “Participant” object that identifiers the message sender\n- contentType: a string that is one of the contentTypes associated with the “MessageContent” object\n- content: a “MessageContent” object that contains the message’s content\n\n### Participant\n\nA Participant is a JSON object that contains three common fields: “participantType”, “id”, and “displayName”\n- participantType: one of “email”, “bot”, or “occupant”\n- id: a UUID identifier for the participant\n- displayName: the name associated with the account or an empty string if not provided\n\nDepending on the participant type, the object also contains additional fields:\n\nEmail Participant (a human user with a Roam user account)\n- email: the email of the participant\n\nBot Participant (an automated user maintained by the Roam team or created via the Roam API)\n- roamId: the roam ID associated with the integration\n- integrationId: a unique integration ID name provided by the bot creator\n- botCode: a unique identifier\n\n### Message Content\n\nA “MessageContent” object is a JSON object that contains the field “contentType” and,\ndepending on the content type, contains additional fields:\n\n*Text Content* (contentType = “text”)\n- text: the text in plaintext\n- markdownText: the text in Markdown format\n- attachments: A list of attachment objects\n\n*Emoji Content* (contentType = “emoji”)\n- text: text representation of the emoji\n- colons: emoji in :emoji: format\n- fileUrl: an optional field containing the URL to a custom emoji image\n\n*Item Content* (contentType = “item”)\n- itemUrl: the URL where the file can be downloaded from\n- itemType: the type of item (e.g. \"photo\", \"pdf\", \"blob\", \"video\", \"audio\", etc.)\n\n*Text Snippet Content* (contentType = \"textSnippet\")\n- text: the content of the snippet\n- language: the language of the snippet\n\n*Members Changed Content* (contentType = “membersChanged”)\n- added: a list of Participant objects corresponding to all participants added in this event\n- removed: a list of Participant objects corresponding to all participants removed in this event\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "messageevent.export",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "description": "Information on the archive to export",
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "date": {
                    "type": "string",
                    "description": "The UTC date to fetch the export for in YYYY-MM-DD format."
                  }
                },
                "required": [
                  "date"
                ]
              },
              "example": {
                "date": "2026-01-21"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Export file returned successfully",
            "content": {
              "application/x-ndjson": {
                "schema": {
                  "type": "string",
                  "description": "JSON Lines (NDJSON) format with one message event per line"
                },
                "example": "{\"eventType\":\"sent\",\"chatId\":\"757dfe66-37b4-4772-baa5-8c86ec68c176\",\"timestamp\":1737454800000000,\"messageId\":\"a1b2c3d4-e5f6-7890-abcd-ef1234567890\",\"sender\":{\"participantType\":\"email\",\"id\":\"U-709b8a57-70bc-427a-b6f0-b16ba5297f8c\",\"displayName\":\"Alex Chen\",\"email\":\"alex.chen@example.com\"},\"contentType\":\"text\",\"content\":{\"text\":\"Good morning team!\",\"markdownText\":\"Good morning team!\",\"attachments\":[]}}\n{\"eventType\":\"sent\",\"chatId\":\"757dfe66-37b4-4772-baa5-8c86ec68c176\",\"timestamp\":1737454860000000,\"messageId\":\"b2c3d4e5-f6a7-8901-bcde-f23456789012\",\"sender\":{\"participantType\":\"email\",\"id\":\"U-af6663d5-0f37-4105-95df-4fea20ef7c7c\",\"displayName\":\"Jordan Smith\",\"email\":\"jordan.smith@example.com\"},\"contentType\":\"text\",\"content\":{\"text\":\"Good morning! Ready for the standup?\",\"markdownText\":\"Good morning! Ready for the standup?\",\"attachments\":[]}}\n"
              }
            }
          },
          "400": {
            "description": "Bad request.",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/conversation.list": {
      "get": {
        "summary": "List conversations",
        "description": "Lists conversations (meetings) that occurred in your Roam, with participant details.\n\n**Access:**\n- **Organization with [`admin:meetings:read`](/docs/guides/scopes#meeting-width-adminmeetingsread)**\n  (or a grandfathered roam-wide API key): all conversations in the workspace.\n- **Personal access tokens:** supported — returns only conversations the\n  token owner participated in (matched by confirmed email).\n- **Organization without roam-wide meeting access** must use\n  [`/meeting.list`](/docs/api/meeting-list) instead (`403`).\n\n**Required scope:** `meetings:read` (add `admin:meetings:read` for roam-wide org access)\n\nParticipant details require `user:read` scope. Email addresses require `user:read.email` scope.\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "conversation.list",
        "security": [
          {
            "bearer": []
          }
        ],
        "tags": [
          "Meetings"
        ],
        "parameters": [
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only return conversations that started before this ISO-8601 timestamp."
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only return conversations that started after this ISO-8601 timestamp."
          },
          {
            "name": "ascending",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "Sort results in ascending order by start time. Default is descending (newest first)."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 10
            },
            "description": "The number of conversations to return per response."
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually."
          }
        ],
        "responses": {
          "200": {
            "description": "Conversations retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Unique identifier for the conversation (meeting GUID)"
                          },
                          "place": {
                            "type": "string",
                            "description": "The place where the conversation occurred"
                          },
                          "room": {
                            "type": "string",
                            "description": "The room name"
                          },
                          "roomType": {
                            "type": "string",
                            "description": "The type of room"
                          },
                          "start": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When the conversation started (ISO-8601)"
                          },
                          "end": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When the conversation ended (ISO-8601)"
                          },
                          "participants": {
                            "type": "array",
                            "description": "List of participants (requires `user:read` scope)",
                            "items": {
                              "type": "object",
                              "properties": {
                                "name": {
                                  "type": "string",
                                  "description": "Display name of the participant"
                                },
                                "email": {
                                  "type": "string",
                                  "format": "email",
                                  "description": "Email address (requires `user:read.email` scope)"
                                },
                                "seconds": {
                                  "type": "number",
                                  "description": "Duration the participant was in the conversation, in seconds"
                                }
                              }
                            }
                          },
                          "meetingLinkIds": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "description": "IDs of meeting links associated with this conversation"
                          }
                        }
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "description": "Pagination cursor for fetching the next page of results"
                    }
                  }
                },
                "example": {
                  "conversations": [
                    {
                      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                      "place": "Headquarters",
                      "room": "Conference Room A",
                      "roomType": "meeting",
                      "start": "2025-03-15T14:00:00Z",
                      "end": "2025-03-15T14:45:00Z",
                      "participants": [
                        {
                          "name": "Alex Chen",
                          "email": "alex.chen@example.com",
                          "seconds": 2700
                        },
                        {
                          "name": "Jordan Smith",
                          "email": "jordan.smith@example.com",
                          "seconds": 2400
                        }
                      ],
                      "meetingLinkIds": []
                    }
                  ],
                  "nextCursor": "YzE6MjAyNS0wMy0xNVQxNDowMDowMFo"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error",
            "description": "Bad request."
          },
          "401": {
            "$ref": "#/components/responses/Error",
            "description": "Presented invalid authentication credentials."
          },
          "403": {
            "$ref": "#/components/responses/Error",
            "description": "Forbidden. Either a personal access token was used (an account-level\ntoken is required), or the org client does not have roam-wide meeting\naccess (`admin:meetings:read`). Use [`/meeting.list`](/docs/api/meeting-list)\ninstead.\n"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/meeting.list": {
      "get": {
        "tags": [
          "Meetings"
        ],
        "summary": "List meetings",
        "description": "List meetings, ordered newest-first.\n\n**Access:** Organization and Personal. Personal tokens return meetings the\nauthenticated user participated in. Organization tokens return every meeting\nin the Roam only with [`admin:meetings:read`](/docs/guides/scopes#meeting-width-adminmeetingsread);\nwithout it, results are limited to meetings the install's bot has access to.\n\n**Required scope:** `meetings:read` (add `admin:meetings:read` for roam-wide org access)\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "meeting.list",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only return meetings that started before this time (RFC-3339). Sub-millisecond precision is truncated."
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only return meetings that started after this time (RFC-3339). Sub-millisecond precision is truncated."
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 10
            },
            "description": "Number of meetings to return per page. Capped to **10** when\n`expand` includes `summary`, `actionItems`, or `chapters`, since\nexpanded payloads are substantially larger.\n"
          },
          {
            "name": "expand",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated list of fields to inline on each meeting. Allowed\nvalues are `summary`, `actionItems`, and `chapters` — same shape\nas on [`/meeting.info`](/docs/api/meeting-info). Use this to\navoid N+1 follow-up calls when scanning many recent meetings.\n"
          }
        ],
        "responses": {
          "200": {
            "description": "Meetings retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "meetings": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid",
                            "description": "Meeting ID"
                          },
                          "title": {
                            "type": "string",
                            "description": "Meeting title"
                          },
                          "subtitle": {
                            "type": "string",
                            "description": "Meeting subtitle"
                          },
                          "start": {
                            "type": "string",
                            "format": "date-time",
                            "description": "Meeting start time (RFC-3339)"
                          },
                          "participantCount": {
                            "type": "integer",
                            "description": "Total number of participants"
                          },
                          "hasVideo": {
                            "type": "boolean",
                            "description": "Whether the meeting was video recorded — a video track\nexists. `true` from the moment recording starts and it\nnever flips back. It does **not** mean the recording is\nready to fetch or play; read `videoStatus` for that.\nMatches [`/meeting.info`](/docs/api/meeting-info) and\nthe `meeting.ended` webhook `data.hasVideo`.\n"
                          },
                          "videoStatus": {
                            "type": "string",
                            "enum": [
                              "none",
                              "processing",
                              "available"
                            ],
                            "description": "Where this meeting's video recording is, which — unlike\n`hasVideo` — changes over time:\n\n- `none` — no video track; the meeting was not recorded.\n  Always paired with `hasVideo: false`.\n- `processing` — a recording exists but its upload has\n  not finished, so there is nothing to play yet. List\n  again shortly, or call\n  [`/meeting.info`](/docs/api/meeting-info) for that one\n  meeting.\n- `available` — the recording is uploaded and has an\n  asset. Use\n  [`/meeting.shareLink`](/docs/api/meeting-share-link)\n  to get a shareable link to it.\n"
                          },
                          "host": {
                            "description": "Meeting host as a participant object. Requires\n`user:read` scope; emails are only included with\n`user:read.email`. Omitted when the host cannot be\nresolved.\n",
                            "$ref": "#/components/schemas/MeetingParticipant"
                          },
                          "summary": {
                            "type": "string",
                            "description": "AI-generated meeting summary. Only present when `expand=summary`."
                          },
                          "actionItems": {
                            "type": "array",
                            "description": "AI-extracted action items. Only present when `expand=actionItems`.",
                            "items": {
                              "$ref": "#/components/schemas/ActionItem"
                            }
                          },
                          "chapters": {
                            "type": "array",
                            "description": "Meeting chapters/segments. Only present when `expand=chapters`.",
                            "items": {
                              "type": "object",
                              "properties": {
                                "name": {
                                  "type": "string"
                                },
                                "start": {
                                  "type": "integer",
                                  "description": "Offset in milliseconds since the meeting's `start`."
                                },
                                "synopsis": {
                                  "type": "string"
                                }
                              }
                            }
                          }
                        },
                        "required": [
                          "id",
                          "title",
                          "start",
                          "participantCount",
                          "hasVideo",
                          "videoStatus"
                        ]
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "description": "Pagination cursor for the next page"
                    }
                  }
                },
                "example": {
                  "meetings": [
                    {
                      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                      "title": "Weekly Team Standup",
                      "start": "2025-04-07T10:00:00Z",
                      "participantCount": 8,
                      "hasVideo": true,
                      "videoStatus": "available",
                      "host": {
                        "type": "member",
                        "id": "ad1e9cc0-0ffd-47e5-895c-2630a73327b4",
                        "name": "Alex Chen",
                        "email": "alex.chen@example.com"
                      }
                    },
                    {
                      "id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
                      "title": "Product Review",
                      "subtitle": "Q2 Roadmap",
                      "start": "2025-04-06T14:00:00Z",
                      "participantCount": 12,
                      "hasVideo": true,
                      "videoStatus": "processing",
                      "host": {
                        "type": "member",
                        "id": "709b8a57-70bc-427a-b6f0-b16ba5297f8c",
                        "name": "Jamie Park",
                        "email": "jamie.park@example.com"
                      }
                    }
                  ],
                  "nextCursor": "YzE6MjAyNS0wNC0wNlQxNDowMDowMFo"
                }
              }
            }
          },
          "400": {
            "description": "Bad request.",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/recording.list": {
      "get": {
        "tags": [
          "Meetings"
        ],
        "summary": "List meeting recordings (Legacy)",
        "deprecated": true,
        "description": "**Legacy:** Prefer [`/meeting.list`](/docs/api/meeting-list) /\n[`/meeting.info`](/docs/api/meeting-info) for new integrations.\n\nLists recordings in your home Roam, filtered by date range (after/before).\nOrganization clients without roam-wide meeting access\n([`admin:meetings:read`](/docs/guides/scopes#meeting-width-adminmeetingsread))\nreceive `403`; use [`/meeting.list`](/docs/api/meeting-list) instead.\nThis route remains registered for existing callers. It returns v0-style\nidentifiers and is not a v1 media-download path.\n\nThe plural alias `/recordings.list` is also registered for existing callers;\nuse this singular form in new documentation and tooling.\n\nThe ordering of results depends on the filter specified:\n\n- When no parameters are provided, the most recent recordings are returned,\n  sorted in reverse chronological order. This is equivalent to specifying `before`\n  as NOW and leaving `after` unspecified.\n\n- If `after` is specified, the results are sorted in forward chronological order.\n\nEither dates or datetimes may be specified. Dates are interpreted in UTC.\n\n**Access:** Organization only. Requires roam-wide meeting access.\n\n**Required scope:** `recordings:read` and `admin:meetings:read` (or a grandfathered roam-wide API key)\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "recording.list",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "datetime"
            },
            "description": "The datetime to begin listing recordings (YYYY-MM-DD or RFC-3339).\nDefaults to \"no filter\".\n"
          },
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "datetime"
            },
            "description": "The datetime until which to list recordings (YYYY-MM-DD or RFC-3339).\nDefaults to \"now\".\n"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 10
            },
            "description": "The number of recordings to return per response. Default is 10."
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "recordings": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "recordingId": {
                            "type": "string",
                            "format": "uuid",
                            "description": "A unique identifier for the recording"
                          },
                          "location": {
                            "type": "string",
                            "description": "Name of the Roam room where the recording took place"
                          },
                          "startTime": {
                            "type": "string",
                            "format": "date-time",
                            "description": "Exact time when the recording began"
                          },
                          "endTime": {
                            "type": "string",
                            "format": "date-time",
                            "description": "Exact time when the recording stopped"
                          },
                          "videoUrl": {
                            "type": "string",
                            "format": "uri",
                            "description": "URL where the video file may be downloaded"
                          }
                        }
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "description": "Returned if there is a subsequent page of recordings."
                    }
                  }
                },
                "example": {
                  "recordings": [
                    {
                      "recordingId": "9003ec0e-ea7d-41b4-93cf-ef42d730f771",
                      "location": "Conference Room A",
                      "startTime": "2026-01-21T14:10:54Z",
                      "endTime": "2026-01-21T15:12:27Z",
                      "videoUrl": "https://ro.am/recordings/video/9003ec0e-ea7d-41b4-93cf-ef42d730f771.mp4?pwd=abc123"
                    },
                    {
                      "recordingId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                      "location": "Huddle Space",
                      "startTime": "2026-01-21T10:00:00Z",
                      "endTime": "2026-01-21T10:45:00Z",
                      "videoUrl": "https://ro.am/recordings/video/a1b2c3d4-e5f6-7890-abcd-ef1234567890.mp4?pwd=def456"
                    }
                  ],
                  "nextCursor": "YzE6MjAyNi0wMS0yMVQxMDowMDowMC4wMDBa"
                }
              }
            }
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/meeting.info": {
      "get": {
        "tags": [
          "Meetings"
        ],
        "summary": "Get meeting info",
        "description": "Get detailed information about a specific meeting, including AI-generated summary, action items, and chapters.\n\nParticipants are included inline up to the `maxParticipants` limit. For meetings with more participants, use [`/meeting.participants`](/docs/api/meeting-participants) to paginate through the full list.\n\n**Access:** Organization and Personal. Personal tokens are limited to meetings\nthe authenticated user participated in. Organization tokens without\n[`admin:meetings:read`](/docs/guides/scopes#meeting-width-adminmeetingsread)\nare limited to meetings the install's bot has access to.\n\n**Required scope:** `meetings:read` (add `admin:meetings:read` for roam-wide org access; add `user:read` to include participants, `user:read.email` for participant emails)\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "meeting.info",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The meeting ID."
          },
          {
            "name": "maxParticipants",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 10
            },
            "description": "Maximum number of participants to resolve and include inline. Use `/meeting.participants` for full pagination."
          }
        ],
        "responses": {
          "200": {
            "description": "Meeting info retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "Meeting ID"
                    },
                    "title": {
                      "type": "string",
                      "description": "Meeting title"
                    },
                    "subtitle": {
                      "type": "string",
                      "description": "Meeting subtitle"
                    },
                    "start": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Meeting start time (RFC-3339)"
                    },
                    "end": {
                      "type": "string",
                      "format": "date-time",
                      "description": "Meeting end time (RFC-3339). Omitted while the meeting is still in progress."
                    },
                    "participantCount": {
                      "type": "integer",
                      "description": "Total number of participants"
                    },
                    "hasVideo": {
                      "type": "boolean",
                      "description": "Whether the meeting was video recorded — a video track exists.\n`true` from the moment recording starts, `true` at meeting end, and\nit never flips back. It does **not** mean the recording is ready to\nfetch or play; read `videoStatus` for that. Matches `meeting.list`,\nthe `meeting.ended` webhook `data.hasVideo`, and the\n`{\"hasVideo\": true}` subscription filter.\n"
                    },
                    "videoStatus": {
                      "type": "string",
                      "enum": [
                        "none",
                        "processing",
                        "available"
                      ],
                      "description": "Where the meeting's video recording is, which — unlike `hasVideo` —\nchanges over time:\n\n- `none` — no video track. The meeting was not recorded and no\n  recording will appear later. Always paired with `hasVideo: false`.\n- `processing` — a recording exists but its upload has not finished,\n  so there is nothing to play yet. Call `/meeting.info` again\n  shortly.\n- `available` — the recording is uploaded and has an asset. Use\n  [`/meeting.shareLink`](/docs/api/meeting-share-link) to get a\n  shareable link to it.\n\nNot present on the `meeting.ended` webhook payload — see that\nevent's page.\n"
                    },
                    "host": {
                      "description": "Meeting host as a participant object. Requires `user:read`\nscope; emails are only included with `user:read.email`.\nOmitted when the host cannot be resolved.\n",
                      "$ref": "#/components/schemas/MeetingParticipant"
                    },
                    "participants": {
                      "type": "array",
                      "description": "Resolved participants (up to `maxParticipants`). Requires `user:read` scope.",
                      "items": {
                        "$ref": "#/components/schemas/MeetingParticipant"
                      }
                    },
                    "participantsOmitted": {
                      "type": "boolean",
                      "description": "True if total participants exceeds the resolved count"
                    },
                    "summary": {
                      "type": "string",
                      "description": "AI-generated meeting summary"
                    },
                    "actionItems": {
                      "type": "array",
                      "description": "AI-extracted action items",
                      "items": {
                        "$ref": "#/components/schemas/ActionItem"
                      }
                    },
                    "chapters": {
                      "type": "array",
                      "description": "Meeting chapters/segments",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string",
                            "description": "Chapter name"
                          },
                          "start": {
                            "type": "integer",
                            "description": "Chapter start offset in milliseconds since the meeting's `start`."
                          },
                          "synopsis": {
                            "type": "string",
                            "description": "Brief synopsis of the chapter"
                          }
                        }
                      }
                    }
                  },
                  "required": [
                    "id",
                    "title",
                    "start",
                    "participantCount",
                    "hasVideo",
                    "videoStatus"
                  ]
                },
                "example": {
                  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                  "title": "Weekly Team Standup",
                  "start": "2025-04-07T10:00:00Z",
                  "end": "2025-04-07T10:42:00Z",
                  "participantCount": 8,
                  "hasVideo": true,
                  "videoStatus": "available",
                  "host": {
                    "type": "member",
                    "id": "709b8a57-70bc-427a-b6f0-b16ba5297f8c",
                    "name": "Alex Chen",
                    "email": "alex.chen@example.com"
                  },
                  "participants": [
                    {
                      "type": "member",
                      "id": "709b8a57-70bc-427a-b6f0-b16ba5297f8c",
                      "name": "Alex Chen",
                      "email": "alex.chen@example.com"
                    },
                    {
                      "type": "guest",
                      "id": "af6663d5-0f37-4105-95df-4fea20ef7c7c",
                      "name": "Jordan Smith"
                    }
                  ],
                  "participantsOmitted": false,
                  "summary": "The team discussed Q2 roadmap priorities and assigned action items for the upcoming sprint.",
                  "actionItems": [
                    {
                      "id": "f1e2d3c4-b5a6-7890-abcd-ef1234567890",
                      "title": "Update API documentation",
                      "description": "Add v1 meeting endpoints to the developer docs",
                      "complete": false,
                      "assigneeId": "709b8a57-70bc-427a-b6f0-b16ba5297f8c",
                      "suggestedAssigneeName": "Alex Chen"
                    }
                  ],
                  "chapters": [
                    {
                      "name": "Sprint Review",
                      "start": 0,
                      "synopsis": "Reviewed completed tasks from the previous sprint"
                    },
                    {
                      "name": "Q2 Planning",
                      "start": 1140000,
                      "synopsis": "Discussed priorities and resource allocation for Q2"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Missing or invalid `id`.",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "Meeting not found.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/meeting.participants": {
      "get": {
        "tags": [
          "Meetings"
        ],
        "summary": "List meeting participants",
        "description": "Paginate through all participants of a meeting. This is the dedicated endpoint for retrieving the full participant list, complementing the capped inline participants in [`/meeting.info`](/docs/api/meeting-info).\n\nPagination uses an **opaque cursor** (not a row offset). Pass `nextCursor`\nfrom a previous response as `cursor` to fetch the next page. Invalid cursors\nreturn `error: \"invalid_cursor\"` — see [Responses and Errors](/docs/guides/responses-and-errors).\n\n**Access:** Organization and Personal. Personal access tokens restrict to meetings the authenticated user participated in.\n\n**Required scope:** `meetings:read` and `user:read` (add `user:read.email` for participant emails)\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "meeting.participants",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The meeting ID."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            },
            "description": "Number of participants to return per page (default 50, max 200)."
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor from a previous response's `nextCursor`. Do not parse or construct cursors yourself.\n"
          }
        ],
        "responses": {
          "200": {
            "description": "Participants retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "participants": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/MeetingParticipant"
                      }
                    },
                    "total": {
                      "type": "integer",
                      "description": "Total number of participants in the meeting"
                    },
                    "nextCursor": {
                      "type": "string",
                      "description": "Present when more participants remain. Pass as `cursor` on the next request. Omitted on the last page.\n"
                    }
                  },
                  "required": [
                    "participants",
                    "total"
                  ]
                },
                "example": {
                  "ok": true,
                  "participants": [
                    {
                      "type": "member",
                      "id": "709b8a57-70bc-427a-b6f0-b16ba5297f8c",
                      "name": "Alex Chen",
                      "email": "alex.chen@example.com"
                    },
                    {
                      "type": "guest",
                      "id": "af6663d5-0f37-4105-95df-4fea20ef7c7c",
                      "name": "Jordan Smith"
                    }
                  ],
                  "total": 8,
                  "nextCursor": "YzE6YWY2NjYzZDUtMGYzNy00MTA1LTk1ZGYtNGZlYTIwZWY3Yzdj"
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Missing or invalid `id`, or invalid `cursor` (`invalid_cursor`).",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "Meeting not found.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/meeting.transcript": {
      "get": {
        "tags": [
          "Meetings"
        ],
        "summary": "Get meeting transcript",
        "description": "Retrieve the transcript for a meeting.\n\nSupports content negotiation:\n- **JSON** (default): Returns structured transcript with cues containing speaker IDs, text, and timing\n- **WebVTT**: Set `Accept: text/vtt` header to receive standard WebVTT format with speaker voice tags\n\n**Access:** Organization and Personal. Personal access tokens restrict to meetings the authenticated user participated in.\n\n**Required scope:** `meetings:read`\n\n**Errors** (see [Responses and Errors](/docs/guides/responses-and-errors)):\n\n| `error` code | Meaning |\n|--------------|---------|\n| `meeting_not_found` | Unknown or inaccessible meeting |\n| `transcript_pending` | Not ready yet — retry later (may include `Retry-After`) |\n| `transcript_unavailable` | Meeting was not transcribed — stop retrying |\n| `upstream_timeout` | Timed out waiting on an upstream service — retry |\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "meeting.transcript",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The meeting ID."
          }
        ],
        "responses": {
          "200": {
            "description": "Transcript retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "Meeting ID"
                    },
                    "cues": {
                      "type": "array",
                      "description": "Transcript cues in chronological order",
                      "items": {
                        "type": "object",
                        "properties": {
                          "speakerId": {
                            "type": "string",
                            "format": "uuid",
                            "description": "Address ID of the speaker"
                          },
                          "text": {
                            "type": "string",
                            "description": "Spoken text"
                          },
                          "start": {
                            "type": "integer",
                            "description": "Start time in milliseconds from meeting start"
                          },
                          "end": {
                            "type": "integer",
                            "description": "End time in milliseconds from meeting start"
                          }
                        },
                        "required": [
                          "text",
                          "start",
                          "end"
                        ]
                      }
                    }
                  },
                  "required": [
                    "id",
                    "cues"
                  ]
                },
                "example": {
                  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                  "cues": [
                    {
                      "speakerId": "709b8a57-70bc-427a-b6f0-b16ba5297f8c",
                      "text": "Let's start with the sprint review.",
                      "start": 0,
                      "end": 3200
                    },
                    {
                      "speakerId": "af6663d5-0f37-4105-95df-4fea20ef7c7c",
                      "text": "Sure, I'll go first with the API updates.",
                      "start": 3500,
                      "end": 6100
                    }
                  ]
                }
              },
              "text/vtt": {
                "schema": {
                  "type": "string"
                },
                "example": "WEBVTT\n\n00:00:00.000 --> 00:00:03.200\n<v Alex Chen>Let's start with the sprint review.\n\n00:00:03.500 --> 00:00:06.100\n<v Jordan Smith>Sure, I'll go first with the API updates.\n"
              }
            }
          },
          "400": {
            "description": "Bad request. Missing or invalid `id`.",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "Meeting or transcript not found.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/meeting.search": {
      "get": {
        "tags": [
          "Meetings"
        ],
        "summary": "Search meetings",
        "description": "AI-powered search across meeting transcripts and summaries.\n\n**Access:** Personal access only. Organization (account-level) tokens are not supported.\n\n**Required scope:** `meetings:read`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "meeting.search",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "query",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Search query string."
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Only return results from meetings after this date (YYYY-MM-DD)."
          },
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date"
            },
            "description": "Only return results from meetings before this date (YYYY-MM-DD)."
          },
          {
            "name": "timezone",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Timezone for date interpretation (e.g. \"America/New_York\")."
          }
        ],
        "responses": {
          "200": {
            "description": "Search results retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "meetingId": {
                            "type": "string",
                            "format": "uuid",
                            "description": "Meeting ID"
                          },
                          "meetingName": {
                            "type": "string",
                            "description": "Meeting title"
                          },
                          "meetingDate": {
                            "type": "string",
                            "format": "date-time",
                            "description": "Meeting date (RFC-3339)"
                          },
                          "participants": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            },
                            "description": "Participant names"
                          },
                          "highlightedSummary": {
                            "type": "string",
                            "description": "Relevant summary excerpt"
                          },
                          "highlightedTranscript": {
                            "type": "string",
                            "description": "Relevant transcript excerpt"
                          }
                        }
                      }
                    },
                    "inferredFilter": {
                      "type": "object",
                      "description": "AI-inferred search filters",
                      "properties": {
                        "after": {
                          "type": "string",
                          "description": "Inferred start date (YYYY-MM-DD)"
                        },
                        "before": {
                          "type": "string",
                          "description": "Inferred end date (YYYY-MM-DD)"
                        },
                        "attendees": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Inferred attendee filter"
                        }
                      }
                    },
                    "inferredQuery": {
                      "type": "string",
                      "description": "AI-refined search query"
                    }
                  }
                },
                "example": {
                  "results": [
                    {
                      "meetingId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                      "meetingName": "Weekly Team Standup",
                      "meetingDate": "2025-04-07T10:00:00Z",
                      "participants": [
                        "Alex Chen",
                        "Jordan Smith"
                      ],
                      "highlightedSummary": "Discussed the **API documentation** updates for v1 endpoints",
                      "highlightedTranscript": "...we need to add the meeting endpoints to the **API docs**..."
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Missing `query` parameter.",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "Organization (account-level) tokens are not supported. Use a personal access token.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/meeting.prompt": {
      "post": {
        "tags": [
          "Meetings"
        ],
        "summary": "Prompt about a meeting",
        "description": "Ask an AI question about a meeting's transcript content. Returns a natural language response based on the meeting transcript.\n\n**Access:** Organization and Personal. Personal access tokens restrict to meetings the authenticated user participated in.\n\n**Required scope:** `meetings:read`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "meeting.prompt",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The meeting ID."
                  },
                  "prompt": {
                    "type": "string",
                    "description": "The question to ask about the meeting."
                  }
                },
                "required": [
                  "id",
                  "prompt"
                ]
              },
              "examples": {
                "ask_question": {
                  "summary": "Ask about action items",
                  "value": {
                    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                    "prompt": "What action items were assigned to Alex?"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Response generated successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "response": {
                      "type": "string",
                      "description": "AI-generated response to the prompt"
                    }
                  },
                  "required": [
                    "response"
                  ]
                },
                "example": {
                  "response": "Alex was assigned two action items: 1) Update the API documentation with v1 meeting endpoints, and 2) Review the PR for the new webhook events."
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Missing `id` or `prompt`.",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "Meeting not found or meeting has no transcript.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/meeting.shareLink": {
      "post": {
        "tags": [
          "Meetings"
        ],
        "summary": "Get a shareable meeting link",
        "description": "Returns a shareable URL for a meeting that you can distribute to others. Pass the `id` of a meeting obtained from [`/meeting.list`](/docs/api/meeting-list) or [`/meeting.info`](/docs/api/meeting-info).\n\nThis endpoint is **get-or-create**: it returns the meeting's existing share link, or mints one the first time it is called for that meeting. Repeat calls for the same meeting return the same URL.\n\nCreating a share link is a deliberate action, which is why it has its own endpoint rather than being returned as a field on `meeting.list` / `meeting.info` — fetching a meeting never mints a shareable link as a side effect. You can only create a share link for a meeting you can access; the same access check as `meeting.info` applies.\n\n**Access:** Organization and Personal. Personal access tokens restrict to meetings the authenticated user participated in.\n\n**Required scope:** `meetings:read`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "meeting.shareLink",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The meeting ID."
                  }
                },
                "required": [
                  "id"
                ]
              },
              "examples": {
                "share_meeting": {
                  "summary": "Get a shareable link for a meeting",
                  "value": {
                    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The meeting's shareable link.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The meeting ID."
                    },
                    "url": {
                      "type": "string",
                      "format": "uri",
                      "description": "The shareable meeting URL."
                    }
                  },
                  "required": [
                    "id",
                    "url"
                  ]
                },
                "example": {
                  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                  "url": "https://ro.am/share/abc123xyz"
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Missing `id`.",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "Meeting not found or not accessible.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/meeting.link.create": {
      "post": {
        "tags": [
          "Meetings"
        ],
        "summary": "Create a meeting link",
        "description": "Create a meeting link.\n\n**Access:** Organization and Personal. In Organization mode, specify the host by email. In Personal mode, the host defaults to the authenticated user.\n\n**Required scope:** `meeting:write` or `meetinglink:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "meeting.link.create",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Meeting Name"
                  },
                  "host": {
                    "type": "string",
                    "format": "email",
                    "description": "Meeting Host Email, matching a member of your Roam.\n\nRequired for Organization tokens. For Personal tokens, this is optional and defaults to the authenticated user. If provided with a Personal token, it must match the authenticated user's email.\n"
                  },
                  "start": {
                    "type": "string",
                    "format": "date-time",
                    "description": "(Optional) Meeting start time in RFC3339."
                  },
                  "end": {
                    "type": "string",
                    "format": "date-time",
                    "description": "(Optional) Meeting end time in RFC3339."
                  },
                  "requireUnconfirmedEmail": {
                    "type": "boolean",
                    "description": "(Optional) If true, guests must verify ownership of their email address before joining.\n"
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "Q1 Planning Session",
                "host": "alex.chen@example.com",
                "start": "2026-02-15T14:00:00Z",
                "end": "2026-02-15T15:00:00Z"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Meeting link successfully created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "Meeting link ID"
                    },
                    "url": {
                      "type": "string",
                      "format": "uri",
                      "description": "Meeting link URL"
                    }
                  }
                },
                "example": {
                  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                  "url": "https://ro.am/r/#/d/abc123xyz/def456uvw"
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- Required `name` missing\n- Personal token provided a `host` that does not match the authenticated user\n- Floor could not be determined for the meeting link (host has no home floor and the Roam has no default home floor configured)\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/meeting.link.info": {
      "post": {
        "tags": [
          "Meetings"
        ],
        "summary": "Get a meeting link",
        "description": "Get a meeting link.\n\n**Access:** Organization and Personal. Personal tokens may only read meeting links where the authenticated user is the host.\n\n**Required scope:** `meetinglink:read`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "meeting.link.info",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Meeting Link ID"
                  }
                },
                "required": [
                  "id"
                ]
              },
              "example": {
                "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Meeting link info",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "Meeting Link ID"
                    },
                    "name": {
                      "type": "string",
                      "description": "Meeting Name"
                    },
                    "host": {
                      "type": "string",
                      "format": "email",
                      "description": "Meeting Host Email, matching a member of your Roam."
                    },
                    "start": {
                      "type": "string",
                      "format": "date-time",
                      "description": "(Optional) Meeting start time in RFC3339."
                    },
                    "end": {
                      "type": "string",
                      "format": "date-time",
                      "description": "(Optional) Meeting end time in RFC3339."
                    },
                    "url": {
                      "type": "string",
                      "format": "uri",
                      "description": "Meeting link URL"
                    },
                    "requireUnconfirmedEmail": {
                      "type": "boolean",
                      "description": "Whether attendees joining with an unconfirmed email are required to verify it before joining."
                    }
                  },
                  "required": [
                    "id",
                    "name",
                    "host",
                    "url"
                  ]
                },
                "example": {
                  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                  "name": "Q1 Planning Session",
                  "host": "alex.chen@example.com",
                  "start": "2026-02-15T14:00:00Z",
                  "end": "2026-02-15T15:00:00Z",
                  "url": "https://ro.am/r/#/d/abc123xyz/def456uvw",
                  "requireUnconfirmedEmail": false
                }
              }
            }
          },
          "400": {
            "description": "Bad request.",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/meeting.link.update": {
      "post": {
        "tags": [
          "Meetings"
        ],
        "summary": "Update a meeting link",
        "description": "Update a meeting link.\n\n**Access:** Organization and Personal. Personal tokens may only update meeting links where the authenticated user is the host.\n\n**Required scope:** `meetinglink:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "meeting.link.update",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Meeting Link ID"
                  },
                  "name": {
                    "type": "string",
                    "description": "Meeting Name"
                  },
                  "host": {
                    "type": "string",
                    "format": "email",
                    "description": "(Optional) Meeting Host Email.\n\nThe Host may NOT be updated.\nAs a result, this property may be omitted or empty.\nIf it is provided, it MUST match the existing value.\n"
                  },
                  "start": {
                    "type": "string",
                    "format": "date-time",
                    "description": "(Optional) Meeting start time in RFC3339."
                  },
                  "end": {
                    "type": "string",
                    "format": "date-time",
                    "description": "(Optional) Meeting end time in RFC3339."
                  },
                  "requireUnconfirmedEmail": {
                    "type": "boolean",
                    "description": "(Optional) If true, guests must verify ownership of their email address before joining.\n"
                  }
                },
                "required": [
                  "id",
                  "name"
                ]
              },
              "example": {
                "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                "name": "Q1 Planning Session - Updated",
                "start": "2026-02-15T15:00:00Z",
                "end": "2026-02-15T16:30:00Z"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Meeting link successfully updated"
          },
          "400": {
            "description": "Bad request.",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/calendar.event.create": {
      "post": {
        "tags": [
          "Meetings"
        ],
        "summary": "Create a calendar event",
        "description": "Create a calendar event on the host's connected calendar. A Roam meeting link\nis automatically attached and email notifications are sent to attendees.\n\nThe event is written to the first active, writable calendar associated with the\nhost. The host must have a connected calendar provider (e.g. Google, Microsoft).\n\n**Recurring events:** Provide `rrule` to create a recurring series. A\n`timeZone` is required for recurring events.\n\n**All-day events:** Set `allDay: true`; `start` and `end` are interpreted as\ndates and normalized to UTC midnight.\n\n**Access:** Organization and Personal. For Organization tokens, the `host` email\nis required and identifies the calendar owner. For Personal tokens, `host`\ndefaults to the authenticated user; if provided it must match the\nauthenticated user's email.\n\n**Required scope:** `calendar:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "calendar.event.create",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "Event title."
                  },
                  "description": {
                    "type": "string",
                    "description": "(Optional) Event description."
                  },
                  "start": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Event start time (RFC3339). For all-day events, the date portion is used."
                  },
                  "end": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Event end time (RFC3339). For all-day events, the date portion is used."
                  },
                  "allDay": {
                    "type": "boolean",
                    "description": "Whether this is an all-day event. Defaults to false."
                  },
                  "rrule": {
                    "type": "string",
                    "description": "(Optional) iCalendar RFC 5545 recurrence rule, e.g. `FREQ=WEEKLY;COUNT=10`.\nWhen provided, `timeZone` is required.\n"
                  },
                  "timeZone": {
                    "type": "string",
                    "description": "IANA timezone name, e.g. `America/New_York`. Required for recurring\nevents; recommended for all events. Defaults to `UTC` when omitted.\n"
                  },
                  "attendees": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Attendee email addresses. Each entry may be a plain email\n(`user@example.com`) or an address string (`Name <user@example.com>`).\n"
                  },
                  "host": {
                    "type": "string",
                    "format": "email",
                    "description": "Calendar host email. Required for Organization tokens. For Personal\ntokens, defaults to the authenticated user and, if provided, must\nmatch the authenticated user's email.\n"
                  }
                },
                "required": [
                  "title",
                  "start",
                  "end"
                ]
              },
              "example": {
                "title": "Q1 Planning",
                "description": "Plan Q1 roadmap",
                "start": "2026-02-15T14:00:00Z",
                "end": "2026-02-15T15:00:00Z",
                "timeZone": "America/Los_Angeles",
                "attendees": [
                  "sam@example.com",
                  "Alex Doe <alex@example.com>"
                ],
                "host": "host@example.com"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Calendar event created successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Calendar event ID (provider-specific)."
                    },
                    "title": {
                      "type": "string"
                    },
                    "description": {
                      "type": "string"
                    },
                    "start": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "end": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "allDay": {
                      "type": "boolean"
                    },
                    "attendees": {
                      "type": "array",
                      "description": "Attendees as stored on the calendar event.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "name": {
                            "type": "string"
                          },
                          "email": {
                            "type": "string",
                            "format": "email"
                          },
                          "status": {
                            "type": "string",
                            "description": "RSVP status, if provided by the calendar provider."
                          }
                        }
                      }
                    },
                    "meetingLink": {
                      "type": "object",
                      "description": "The Roam meeting link attached to the event.",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid"
                        },
                        "url": {
                          "type": "string",
                          "format": "uri"
                        }
                      }
                    }
                  },
                  "required": [
                    "id",
                    "title",
                    "start",
                    "end",
                    "attendees"
                  ]
                },
                "example": {
                  "id": "evt_01HX3YABCDEF",
                  "title": "Q1 Planning",
                  "description": "Plan Q1 roadmap",
                  "start": "2026-02-15T14:00:00Z",
                  "end": "2026-02-15T15:00:00Z",
                  "attendees": [
                    {
                      "name": "Sam Smith",
                      "email": "sam@example.com",
                      "status": "needsAction"
                    }
                  ],
                  "meetingLink": {
                    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                    "url": "https://ro.am/r/#/d/abc123xyz/def456uvw"
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- Missing `title`, `start`, or `end`\n- `start` or `end` is not valid RFC3339\n- `rrule` provided without `timeZone`\n- Invalid `timeZone` (must be an IANA zone name)\n- No writable calendar found for the host\n- Organization token missing `host` email\n- Host email not found in the Roam\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "403": {
            "description": "Forbidden. Personal tokens may only create events for the authenticated user.\n",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/calendar.list": {
      "get": {
        "tags": [
          "Meetings"
        ],
        "summary": "List calendar events",
        "description": "List events from the authenticated user's connected calendars within\na date range.\n\nPulls events from every active personal calendar attached to the user\n(e.g. Google, Microsoft) and merges them into a single chronological\nlist. Canceled events are omitted.\n\n**Date range:** Defaults to a 7-day window starting today (caller's\ntimezone). Pass `startDate` to shift the window's start; pass\n`endDate` to set its end (inclusive). Both are interpreted as\n`YYYY-MM-DD` in the caller's timezone.\n\n**Access:** Personal access only. Organization tokens do not have\naccess to individual calendars and receive a `400`.\n\n**Required scope:** `calendar:read`\n\n`meetings:read` also grants this endpoint, but only for API clients\nregistered **before 2026-07-29T00:00Z**. Clients registered on or after that\ndate must hold `calendar:read`, or the call fails with `403` /\n`missing_scope`. See [Scopes](/docs/guides/scopes).\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "calendar.list",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "startDate",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "First day to include (`YYYY-MM-DD`, caller's timezone). Defaults to today.",
            "required": false
          },
          {
            "name": "endDate",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Last day to include (`YYYY-MM-DD`, caller's timezone, inclusive).\nDefaults to seven days after the resolved `startDate`.\n",
            "required": false
          }
        ],
        "responses": {
          "200": {
            "description": "Calendar events retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "description": "Provider-specific event ID."
                          },
                          "title": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          },
                          "startTime": {
                            "type": "string",
                            "format": "date-time",
                            "description": "Event start time (RFC3339, caller's timezone)."
                          },
                          "endTime": {
                            "type": "string",
                            "format": "date-time",
                            "description": "Event end time (RFC3339, caller's timezone)."
                          },
                          "weekday": {
                            "type": "string",
                            "description": "Weekday name (`Monday`, `Tuesday`, …) of `startTime` in the caller's timezone."
                          },
                          "allDay": {
                            "type": "boolean",
                            "description": "`true` for all-day events. All-day events are\nemitted at UTC midnight without timezone\nconversion.\n"
                          },
                          "location": {
                            "type": "string",
                            "description": "Conference URL if the event has video conferencing\nattached (preferring video entry points).\n"
                          },
                          "invites": {
                            "type": "array",
                            "items": {
                              "type": "object",
                              "properties": {
                                "name": {
                                  "type": "string"
                                },
                                "email": {
                                  "type": "string",
                                  "format": "email"
                                },
                                "responseStatus": {
                                  "type": "string",
                                  "description": "RSVP status from the calendar provider."
                                }
                              }
                            }
                          },
                          "organizer": {
                            "type": "string",
                            "format": "email",
                            "description": "Organizer email address."
                          },
                          "rrule": {
                            "type": "string",
                            "description": "iCalendar RFC 5545 recurrence rule for the master\nevent in a recurring series. Mutually exclusive\nwith `recurringEventId`.\n"
                          },
                          "recurringEventId": {
                            "type": "string",
                            "description": "Master event ID when this event is one instance of\na recurring series.\n"
                          },
                          "meetingLinkId": {
                            "type": "string",
                            "format": "uuid",
                            "description": "Roam meeting link attached to the event, if any.\n"
                          }
                        },
                        "required": [
                          "id",
                          "title",
                          "startTime",
                          "endTime",
                          "weekday",
                          "invites"
                        ]
                      }
                    }
                  },
                  "required": [
                    "events"
                  ]
                },
                "example": {
                  "events": [
                    {
                      "id": "evt_01HX3YABCDEF",
                      "title": "Q2 Planning",
                      "description": "Plan Q2 roadmap",
                      "startTime": "2026-04-21T10:00:00-07:00",
                      "endTime": "2026-04-21T11:00:00-07:00",
                      "weekday": "Tuesday",
                      "location": "https://ro.am/r/#/d/abc123xyz/def456uvw",
                      "invites": [
                        {
                          "name": "Sam Smith",
                          "email": "sam@example.com",
                          "responseStatus": "accepted"
                        }
                      ],
                      "organizer": "host@example.com",
                      "meetingLinkId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- Organization token (calendar.list is Personal access only)\n- Invalid `startDate` or `endDate` format\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/lobby.list": {
      "get": {
        "tags": [
          "Meetings"
        ],
        "summary": "List lobbies",
        "description": "Lists active lobbies in your account.\n\nA lobby URL has the form `ro.am/{handle}` or `ro.am/{handle}/{slug}`.\n- The \"handle\" is the first path segment\n- The \"slug\" is the optional second path segment. It may be empty for the default lobby under a handle\n\nOptionally filter by a specific lobby handle. If provided, only lobbies\nassociated with that handle are returned.\n\nThis endpoint is **not paginated**. The 200 body is `{ \"lobbies\": [...] }`\nwith every matching lobby; there is no `cursor` / `nextCursor` and no\n`data` array. The TypeScript SDK returns that object directly, not a\npage helper.\n\n**Access:** Organization and Personal.\n\n**Required scope:** `lobby:read`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "lobby.list",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "handle",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Filter by lobby handle (first path segment), e.g., `robfig` for\n`ro.am/robfig` or `ro.am/robfig/tour`.\n"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "lobbies": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid",
                            "description": "Unique identifier of the lobby configuration (UUID)"
                          },
                          "slug": {
                            "type": "string",
                            "description": "Optional second path segment for the lobby. May be empty.\n"
                          },
                          "displayName": {
                            "type": "string",
                            "description": "Human-readable name of the lobby configuration"
                          },
                          "active": {
                            "type": "boolean",
                            "description": "Whether the lobby configuration is active"
                          },
                          "url": {
                            "type": "string",
                            "format": "uri",
                            "description": "Public URL of the lobby (e.g., `https://ro.am/handle` or `https://ro.am/handle/slug`)"
                          },
                          "handle": {
                            "type": "string",
                            "description": "First path segment of the lobby URL\n"
                          }
                        }
                      }
                    }
                  }
                },
                "example": {
                  "lobbies": [
                    {
                      "id": "6a2e0a6c-2a63-4a7d-9f2e-9b63f2a6c4b1",
                      "slug": "",
                      "displayName": "Default Lobby",
                      "active": true,
                      "url": "https://ro.am/person",
                      "handle": "person"
                    },
                    {
                      "id": "7b3c1d92-6d6f-4f23-9c2a-2a5f8e1d4c77",
                      "slug": "tour",
                      "displayName": "Tour Lobby",
                      "active": true,
                      "url": "https://ro.am/person/tour",
                      "handle": "person"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error",
            "description": "Bad request."
          },
          "401": {
            "$ref": "#/components/responses/Error",
            "description": "Presented invalid authentication credentials."
          },
          "404": {
            "$ref": "#/components/responses/Error",
            "description": "Handle not found."
          },
          "405": {
            "$ref": "#/components/responses/Error",
            "description": "An unsupported method was requested."
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/lobby.booking.list": {
      "get": {
        "tags": [
          "Meetings"
        ],
        "summary": "List lobby bookings",
        "description": "Lists bookings for a specific lobby configuration, filtered by date range (after/before).\n\nThe ordering of results depends on the filter specified:\n\n- When no parameters are provided, the most recent bookings are returned,\n  sorted in reverse chronological order. This is equivalent to specifying `before`\n  as NOW and leaving `after` unspecified.\n\n- If `after` is specified, the results are sorted in forward chronological order.\n\nEither dates or datetimes may be specified. Dates are interpreted in UTC.\n\n**Access:** Organization and Personal.\n\n**Required scope:** `lobby:read`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "lobby.booking.list",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "lobbyId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The lobby configuration ID to list bookings for."
          },
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "The datetime to begin listing bookings (YYYY-MM-DD or RFC-3339).\nDefaults to \"no filter\".\n"
          },
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "The datetime until which to list bookings (YYYY-MM-DD or RFC-3339).\nDefaults to \"now\".\n"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 10
            },
            "description": "The number of bookings to return per response. Default is 10."
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually."
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "bookings": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/LobbyBooking"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "description": "Returned if there is a subsequent page of bookings."
                    }
                  }
                },
                "example": {
                  "bookings": [
                    {
                      "id": "018f3f37-eca3-7d07-8a12-9e65a8a0c1b9",
                      "start": "2025-08-03T14:10:54Z",
                      "end": "2025-08-03T14:40:54Z",
                      "status": "active",
                      "timeZone": "America/Los_Angeles",
                      "notes": "Intro call",
                      "created": "2025-08-01T10:00:00Z",
                      "hosts": [
                        {
                          "name": "Alex Doe",
                          "email": "alex@example.com",
                          "isOrganizer": true
                        }
                      ],
                      "invitees": [
                        {
                          "name": "Sam Smith",
                          "email": "sam@example.com",
                          "status": "invited",
                          "isBooker": true
                        }
                      ]
                    }
                  ],
                  "nextCursor": "YzE6MDE4ZjNmMzktMmI1Yy03YjNkLTlmOTYtOGIyZjFjMGUxMjM0"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error",
            "description": "Bad request."
          },
          "401": {
            "$ref": "#/components/responses/Error",
            "description": "Presented invalid authentication credentials."
          },
          "404": {
            "$ref": "#/components/responses/Error",
            "description": "Lobby not found."
          },
          "405": {
            "$ref": "#/components/responses/Error",
            "description": "An unsupported method was requested."
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/magicast.list": {
      "get": {
        "tags": [
          "Magicasts"
        ],
        "summary": "List magicasts",
        "description": "List Magicasts in your account, most recent first.\n\nReturns metadata only (`id`, `name`, `createdAt`, `ownerId`,\n`coverImageUrl`). Use [`/magicast.info`](/docs/api/magicast-info) for\ntranscript cues, chapters, video status, and a signed download URL.\n\n**Access:** Organization and Personal. Organization tokens list every\nMagicast in the account, including ones the creator never shared. Personal\ntokens are restricted to Magicasts owned by the authenticated user.\n\n**Required scope:** `magicast:read`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "magicast.list",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "after",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only return magicasts created after this time (RFC-3339)."
          },
          {
            "name": "before",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "date-time"
            },
            "description": "Only return magicasts created before this time (RFC-3339)."
          },
          {
            "name": "ascending",
            "in": "query",
            "schema": {
              "type": "boolean",
              "default": false
            },
            "description": "Sort oldest-first instead of newest-first."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 10
            },
            "description": "Number of magicasts to return per response. Default 10."
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually."
          }
        ],
        "responses": {
          "200": {
            "description": "Magicasts retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "magicasts": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Magicast"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "description": "Cursor for the next page; omitted on the last page."
                    }
                  }
                },
                "example": {
                  "magicasts": [
                    {
                      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                      "name": "Q1 All-Hands Recap",
                      "createdAt": "2026-01-21T18:30:00Z",
                      "ownerId": "709b8a57-70bc-427a-b6f0-b16ba5297f8c",
                      "coverImageUrl": "https://ro.am/card-images/a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                    }
                  ],
                  "nextCursor": "YzE6MjAyNi0wMS0yMVQxODozMDowMFo6YTFiMmMzZDQtZTVmNg"
                }
              }
            }
          },
          "400": {
            "description": "Bad request.",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/magicast.info": {
      "get": {
        "tags": [
          "Magicasts"
        ],
        "summary": "Get magicast info",
        "description": "Get details for a single Magicast by ID, including transcript cues,\nchapters, video status, a signed video download URL when ready, and a\nplayer URL if a share link already exists.\n\nThis is the content endpoint. [`/magicast.list`](/docs/api/magicast-list)\nreturns metadata only. Magicasts are not meetings — they do not appear on\n[`/recording.list`](/docs/api/recording-list) or meeting transcript\nsurfaces, and they have no Magic Minutes summary or action items.\n\nAsset, transcript, and share-link lookups are best-effort. If the video or\ntranscript is still processing, those fields are omitted and the request\nstill succeeds. Fetching this endpoint **never** mints a shareable link;\nuse [`/magicast.shareLink`](/docs/api/magicast-share-link) for that.\n\nThere is no `https://ro.am/magicast/{id}` browser URL. The player URL is\nalways `https://ro.am/share/{key}`.\n\n**Access:** Organization and Personal. Organization tokens can read every\nMagicast in the account, including ones the creator never shared. Personal\ntokens are restricted to Magicasts owned by the authenticated user. Filter\non whether `shareUrl` is present if you only want shared recordings.\n\n**Required scope:** `magicast:read`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "magicast.info",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The magicast ID."
          }
        ],
        "responses": {
          "200": {
            "description": "Magicast retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MagicastInfo"
                },
                "example": {
                  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                  "name": "Q1 All-Hands Recap",
                  "createdAt": "2026-01-21T18:30:00Z",
                  "ownerId": "709b8a57-70bc-427a-b6f0-b16ba5297f8c",
                  "coverImageUrl": "https://ro.am/card-images/a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                  "durationMs": 184000,
                  "videoStatus": "available",
                  "videoUrl": "https://cdn.example/signed/magicast.mp4",
                  "shareUrl": "https://ro.am/share/abcd1234-efgh5678-ijkl9012-mnop3456",
                  "chapters": [
                    {
                      "title": "Intro",
                      "startTime": 0
                    },
                    {
                      "title": "Roadmap",
                      "startTime": 32000
                    }
                  ],
                  "cues": [
                    {
                      "text": "Welcome to the recap.",
                      "startOffset": 0,
                      "endOffset": 2400
                    },
                    {
                      "text": "Let's look at the roadmap.",
                      "startOffset": 2400,
                      "endOffset": 6100
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Missing or invalid `id`.",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "Magicast not found.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/magicast.shareLink": {
      "post": {
        "tags": [
          "Magicasts"
        ],
        "summary": "Get a shareable Magicast link",
        "description": "Returns a shareable player URL for a Magicast. Pass the `id` obtained from\n[`/magicast.list`](/docs/api/magicast-list) or\n[`/magicast.info`](/docs/api/magicast-info).\n\nThis endpoint is **get-or-create**: it returns the Magicast's existing\nshare link, or mints one the first time it is called. Repeat calls for the\nsame Magicast return the same URL.\n\nCreating a share link is a deliberate action, which is why it has its own\nendpoint rather than being returned as a field that is always present on\n`magicast.list` / `magicast.info`. Fetching a Magicast never mints a\nshareable link as a side effect. `magicast.info` includes `shareUrl` only\nwhen a link already exists.\n\nThe URL is `https://ro.am/share/{key}`. There is no\n`https://ro.am/magicast/{id}` route.\n\nYou can only create a share link for a Magicast you can access; the same\naccess check as `magicast.info` applies.\n\n**Access:** Organization and Personal. Personal access tokens restrict to\nMagicasts owned by the authenticated user.\n\n**Required scope:** `magicast:read`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "magicast.shareLink",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The Magicast ID."
                  }
                },
                "required": [
                  "id"
                ]
              },
              "examples": {
                "share_magicast": {
                  "summary": "Get a shareable link for a Magicast",
                  "value": {
                    "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The Magicast's shareable link.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "format": "uuid",
                      "description": "The Magicast ID."
                    },
                    "url": {
                      "type": "string",
                      "format": "uri",
                      "description": "The shareable player URL (`https://ro.am/share/{key}`)."
                    }
                  },
                  "required": [
                    "id",
                    "url"
                  ]
                },
                "example": {
                  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                  "url": "https://ro.am/share/abcd1234-efgh5678-ijkl9012-mnop3456"
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Missing or invalid `id`.",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "Magicast not found or not accessible.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/group.list": {
      "get": {
        "summary": "List groups",
        "description": "Lists non-archived groups accessible to the caller.\n\nFilter by name with `query` (ranked text match), restrict by group\ntype with `type`, and paginate with `limit` / `cursor`.\n\n**Access:** Organization and Personal.\n\n**Required scope:** `group:read`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "group.list",
        "security": [
          {
            "bearer": []
          }
        ],
        "tags": [
          "Groups"
        ],
        "parameters": [
          {
            "name": "query",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Text filter. Groups are ranked by how well their name matches the query."
          },
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated list of group types to include. Must be one or\nmore of `standard`, `magicast`, `meeting`, `roam`, `onair`.\nDefaults to all types.\n"
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 50
            },
            "description": "Number of groups to return per page (default 50, max 100)."
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually."
          }
        ],
        "responses": {
          "200": {
            "description": "Groups retrieved successfully.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "groups": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "required": [
                          "id",
                          "name",
                          "type"
                        ],
                        "properties": {
                          "id": {
                            "type": "string",
                            "format": "uuid",
                            "description": "The group ID."
                          },
                          "name": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string",
                            "description": "Group description, if set."
                          },
                          "imageUrl": {
                            "type": "string",
                            "format": "uri",
                            "description": "Group image URL, if set."
                          },
                          "type": {
                            "type": "string",
                            "enum": [
                              "standard",
                              "magicast",
                              "meeting",
                              "roam",
                              "onair",
                              "community"
                            ],
                            "description": "Group type."
                          },
                          "accessMode": {
                            "type": "string",
                            "enum": [
                              "public",
                              "private"
                            ],
                            "description": "Whether the group is public or private."
                          },
                          "dateCreated": {
                            "type": "string",
                            "format": "date-time",
                            "description": "When the group was created (RFC3339, caller's timezone)."
                          }
                        }
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "description": "Pagination cursor for the next page. Absent when there are no more results."
                    }
                  },
                  "required": [
                    "groups"
                  ]
                },
                "example": {
                  "groups": [
                    {
                      "id": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                      "name": "All Hands",
                      "description": "Company-wide announcements",
                      "type": "roam",
                      "accessMode": "public",
                      "dateCreated": "2025-01-15T10:00:00-08:00",
                      "imageUrl": "https://ro.am/card-images/88bebce7-6cbb-4666-96f9-5c02d73e6661"
                    },
                    {
                      "id": "c6040d77-a61c-4834-a939-fe3e687ffd72",
                      "name": "Engineering Team",
                      "type": "standard",
                      "accessMode": "public",
                      "dateCreated": "2025-02-20T14:30:00-08:00",
                      "imageUrl": "https://ro.am/card-images/c6040d77-a61c-4834-a939-fe3e687ffd72"
                    }
                  ],
                  "nextCursor": "YzE6MjAyNS0wMi0yMFQxNDozMDowMFo6YzYwNDBkNzctYTYxYw"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error",
            "description": "Bad request. Common causes:\n- Invalid `limit` (non-numeric or ≤ 0)\n- Invalid `cursor`\n- Invalid `type` value\n"
          },
          "401": {
            "$ref": "#/components/responses/Error",
            "description": "Presented invalid authentication credentials."
          },
          "405": {
            "$ref": "#/components/responses/Error",
            "description": "An unsupported method was requested."
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/groups.list": {
      "get": {
        "tags": [
          "Groups"
        ],
        "summary": "List groups (Legacy)",
        "deprecated": true,
        "description": "**Legacy:** Prefer [`/group.list`](/docs/api/group-list) for new integrations.\n\nLists all public, non-archived groups in your home Roam.\n\nUnlike `/group.list`, this endpoint returns a **raw JSON array** (not the\n`{\"ok\": true, …}` envelope). It is the sole ok-envelope exception on `/v1`\nand remains only for existing callers.\n\n**Access:** Organization only.\n\n**Required scope:** `group:read`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "groups.list",
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "OK. **Note:** response is a raw array, not the v1 `ok` envelope.\n",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "type": "object",
                    "description": "Legacy group row from `groups.list` (not the v1 Group object).",
                    "properties": {
                      "addressId": {
                        "type": "string"
                      },
                      "roamId": {
                        "type": "integer"
                      },
                      "accountId": {
                        "type": "integer"
                      },
                      "groupType": {
                        "type": "string"
                      },
                      "name": {
                        "type": "string"
                      },
                      "accessMode": {
                        "type": "string"
                      },
                      "groupManagement": {
                        "type": "string"
                      },
                      "enforceThreadedMode": {
                        "type": "boolean",
                        "default": false
                      },
                      "dateCreated": {
                        "type": "string"
                      },
                      "imageUrl": {
                        "type": "string"
                      }
                    }
                  }
                },
                "example": [
                  {
                    "addressId": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                    "roamId": 12345,
                    "accountId": 67890,
                    "groupType": "roam",
                    "name": "All Hands",
                    "accessMode": "public",
                    "groupManagement": "groupAdminsOnly",
                    "enforceThreadedMode": false,
                    "dateCreated": "2025-01-15T10:00:00Z",
                    "imageUrl": "https://ro.am/card-images/88bebce7-6cbb-4666-96f9-5c02d73e6661"
                  },
                  {
                    "addressId": "c6040d77-a61c-4834-a939-fe3e687ffd72",
                    "roamId": 12345,
                    "accountId": 67890,
                    "groupType": "group",
                    "name": "Engineering Team",
                    "accessMode": "public",
                    "groupManagement": "allMembers",
                    "enforceThreadedMode": true,
                    "dateCreated": "2025-02-20T14:30:00Z",
                    "imageUrl": "https://ro.am/card-images/c6040d77-a61c-4834-a939-fe3e687ffd72"
                  }
                ]
              }
            }
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/group.info": {
      "get": {
        "tags": [
          "Groups"
        ],
        "summary": "Get group info",
        "description": "Get information about a specific group by its ID or name.\n\nProvide either `id` or `name`, not both.\n\n**Required scope:** `group:read`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "group.info",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "The group's ID. Mutually exclusive with `name`."
          },
          {
            "name": "name",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "The group's name. Mutually exclusive with `id`. Returns first match if multiple groups have the same name."
          }
        ],
        "responses": {
          "200": {
            "description": "Group info retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                },
                "example": {
                  "id": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                  "chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176",
                  "name": "Engineering Team",
                  "type": "standard",
                  "accessMode": "public",
                  "groupManagement": "allMembers",
                  "enforceThreadedMode": true,
                  "dateCreated": "2025-02-20T14:30:00Z",
                  "imageUrl": "https://ro.am/card-images/88bebce7-6cbb-4666-96f9-5c02d73e6661"
                }
              }
            }
          },
          "400": {
            "description": "Bad request. Common causes:\n- Neither `id` nor `name` provided\n- Both `id` and `name` provided\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "$ref": "#/components/responses/Error",
            "description": "Presented invalid authentication credentials."
          },
          "403": {
            "description": "App does not have access to this group.",
            "$ref": "#/components/responses/Error"
          },
          "404": {
            "description": "Group not found.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "$ref": "#/components/responses/Error",
            "description": "An unsupported method was requested."
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/group.create": {
      "post": {
        "tags": [
          "Groups"
        ],
        "summary": "Create a group",
        "description": "Create a group chat.\n\nGroups which specify at least one admin will operate in an \"Admin only\" management\nmode, where only admins may change settings. Otherwise, all members have\nthat capability.\n\nGroups require at least one member. Users can be specified by user ID or email address.\n\n**Required scope:** `group:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "group.create",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "Name of the group"
                  },
                  "description": {
                    "type": "string",
                    "maxLength": 1024,
                    "description": "Description of the group"
                  },
                  "private": {
                    "type": "boolean",
                    "description": "Whether the group is private (default false)"
                  },
                  "enforceThreads": {
                    "type": "boolean",
                    "description": "Whether to enforce threaded conversations"
                  },
                  "members": {
                    "type": "array",
                    "minItems": 1,
                    "description": "Group members with their roles",
                    "items": {
                      "type": "object",
                      "properties": {
                        "userId": {
                          "type": "string",
                          "description": "User ID or email address"
                        },
                        "role": {
                          "type": "string",
                          "enum": [
                            "member",
                            "admin"
                          ],
                          "description": "Role for this member"
                        }
                      },
                      "required": [
                        "userId",
                        "role"
                      ]
                    }
                  }
                },
                "required": [
                  "name",
                  "members"
                ]
              },
              "examples": {
                "withEmails": {
                  "summary": "Create group with email addresses",
                  "value": {
                    "name": "Engineering Team",
                    "description": "Group chat for engineering discussions and updates",
                    "private": false,
                    "enforceThreads": true,
                    "members": [
                      {
                        "userId": "alex.chen@example.com",
                        "role": "member"
                      },
                      {
                        "userId": "taylor@example.com",
                        "role": "member"
                      },
                      {
                        "userId": "jordan.smith@example.com",
                        "role": "admin"
                      }
                    ]
                  }
                },
                "withIds": {
                  "summary": "Create group with user IDs",
                  "value": {
                    "name": "Product Team",
                    "private": true,
                    "members": [
                      {
                        "userId": "709b8a57-70bc-427a-b6f0-b16ba5297f8c",
                        "role": "member"
                      },
                      {
                        "userId": "f589a8cb-78ac-493e-8719-0fa8a22f65e0",
                        "role": "member"
                      },
                      {
                        "userId": "af6663d5-0f37-4105-95df-4fea20ef7c7c",
                        "role": "admin"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Group created successfully",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                },
                "example": {
                  "id": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                  "chatId": "",
                  "name": "Engineering Team",
                  "type": "standard",
                  "accessMode": "public",
                  "groupManagement": "groupAdminsOnly",
                  "enforceThreadedMode": true,
                  "dateCreated": "2026-01-21T10:30:00Z"
                }
              }
            }
          },
          "400": {
            "description": "Bad request.",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "405": {
            "description": "An unsupported method was requested.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/group.rename": {
      "post": {
        "tags": [
          "Groups"
        ],
        "summary": "Rename a group",
        "description": "Rename a group by ID.\n\nApps may only rename groups for which they are an admin.\n\n**Required scope:** `group:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "group.rename",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The group ID"
                  },
                  "name": {
                    "type": "string",
                    "maxLength": 64,
                    "description": "The new name for the group"
                  }
                },
                "required": [
                  "id",
                  "name"
                ]
              },
              "example": {
                "id": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                "name": "Product Engineering"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Group renamed successfully"
          },
          "400": {
            "$ref": "#/components/responses/Error",
            "description": "Bad request."
          },
          "401": {
            "$ref": "#/components/responses/Error",
            "description": "Presented invalid authentication credentials."
          },
          "403": {
            "$ref": "#/components/responses/Error",
            "description": "App does not have admin access to this group."
          },
          "404": {
            "$ref": "#/components/responses/Error",
            "description": "Group not found."
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/group.archive": {
      "post": {
        "tags": [
          "Groups"
        ],
        "summary": "Archive a group",
        "description": "Archive a group by ID.\n\nApps may only archive groups for which they are an admin.\n\n**Required scope:** `group:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "group.archive",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The group ID to archive."
                  }
                },
                "required": [
                  "id"
                ]
              },
              "example": {
                "id": "88bebce7-6cbb-4666-96f9-5c02d73e6661"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Group archived successfully"
          },
          "400": {
            "$ref": "#/components/responses/Error",
            "description": "Bad request."
          },
          "401": {
            "$ref": "#/components/responses/Error",
            "description": "Presented invalid authentication credentials."
          },
          "403": {
            "$ref": "#/components/responses/Error",
            "description": "App does not have admin access to this group."
          },
          "404": {
            "$ref": "#/components/responses/Error",
            "description": "Group not found."
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/group.members": {
      "get": {
        "tags": [
          "Groups"
        ],
        "summary": "List group members",
        "description": "List members in a group with their roles.\n\nApps may list members if one of the following conditions is true:\n1. It is a public group in their Roam.\n2. They are a member of the group.\n\n**Required scope:** `group:read`\n\nEvery returned `userId` is a visible principal ID that resolves through\n[`user.info`](/docs/api/user-info) with the same credentials. Use\n`user.list?ids` for ordered bulk hydration.\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "group.members",
        "security": [
          {
            "bearer": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "schema": {
              "type": "string",
              "format": "uuid"
            },
            "description": "Group ID.",
            "required": true
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 100,
              "default": 10
            },
            "description": "The number of members to return per response. Default is 10."
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Opaque pagination cursor from a previous response's `nextCursor`. Do not construct cursors manually."
          }
        ],
        "responses": {
          "200": {
            "description": "Members retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "members": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/GroupMember"
                      }
                    },
                    "nextCursor": {
                      "type": "string",
                      "description": "Pagination cursor for fetching the next page of results"
                    }
                  }
                },
                "example": {
                  "members": [
                    {
                      "userId": "709b8a57-70bc-427a-b6f0-b16ba5297f8c",
                      "role": "member"
                    },
                    {
                      "userId": "af6663d5-0f37-4105-95df-4fea20ef7c7c",
                      "role": "admin"
                    },
                    {
                      "userId": "f589a8cb-78ac-493e-8719-0fa8a22f65e0",
                      "role": "member"
                    }
                  ],
                  "nextCursor": "YzE6ZjU4OWE4Y2ItNzhhYy00OTNlLTg3MTktMGZhOGEyMmY2NWUw"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error",
            "description": "Invalid request, e.g. group does not exist."
          },
          "401": {
            "$ref": "#/components/responses/Error",
            "description": "Unauthorized."
          },
          "403": {
            "$ref": "#/components/responses/Error",
            "description": "App does not have access to this group."
          },
          "500": {
            "description": "An internal error occurred, including a group-member ID that cannot resolve as a visible principal. The endpoint does not return a partial page."
          }
        }
      }
    },
    "/group.add": {
      "post": {
        "tags": [
          "Groups"
        ],
        "summary": "Add group members",
        "description": "Add one or more group members with specified roles.\n\nMembers can be specified by user ID or email address. Each member must be assigned a role (member or admin).\n\nApps may add members to a group if one of the following conditions is true:\n1. It is a public group in their Roam.\n2. They are a member of the group.\n\nIf attempting to add an admin, the app must be an admin of the group.\n\n**Required scope:** `group:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "group.add",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Group ID"
                  },
                  "members": {
                    "type": "array",
                    "description": "List of members to add with their roles",
                    "items": {
                      "type": "object",
                      "properties": {
                        "userId": {
                          "type": "string",
                          "description": "User ID or email address"
                        },
                        "role": {
                          "type": "string",
                          "enum": [
                            "member",
                            "admin"
                          ],
                          "description": "Role for this member"
                        }
                      },
                      "required": [
                        "userId",
                        "role"
                      ]
                    }
                  }
                },
                "required": [
                  "id"
                ]
              },
              "examples": {
                "withIds": {
                  "summary": "Add members by user ID",
                  "value": {
                    "id": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                    "members": [
                      {
                        "userId": "709b8a57-70bc-427a-b6f0-b16ba5297f8c",
                        "role": "member"
                      },
                      {
                        "userId": "f589a8cb-78ac-493e-8719-0fa8a22f65e0",
                        "role": "member"
                      },
                      {
                        "userId": "af6663d5-0f37-4105-95df-4fea20ef7c7c",
                        "role": "admin"
                      }
                    ]
                  }
                },
                "withEmails": {
                  "summary": "Add members by email",
                  "value": {
                    "id": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                    "members": [
                      {
                        "userId": "alex.chen@example.com",
                        "role": "member"
                      },
                      {
                        "userId": "taylor@example.com",
                        "role": "admin"
                      }
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Members added successfully"
          },
          "400": {
            "$ref": "#/components/responses/Error",
            "description": "Invalid request, e.g. group does not exist or incorrect user IDs."
          },
          "401": {
            "$ref": "#/components/responses/Error",
            "description": "Unauthorized."
          },
          "403": {
            "$ref": "#/components/responses/Error",
            "description": "App does not have permission to add members to this group."
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/group.join": {
      "post": {
        "tags": [
          "Groups"
        ],
        "summary": "Join a group",
        "description": "Join a public group as the calling identity (Slack `conversations.join`).\n\n- Org tokens add the bot address as a member.\n- Personal tokens add the **owner person**, never the PAT bot address.\n- Private groups cannot be self-joined (`403`).\n- Idempotent if the calling identity is already a member.\n- Non-members of a group in another roam receive an opaque `403`\n  (`group_not_found`) — archived / type / privacy are not distinguished.\n\n**Access:** Organization and Personal.\n\n**Required scope:** `group:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "group.join",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Group ID"
                  }
                },
                "required": [
                  "id"
                ]
              },
              "example": {
                "id": "88bebce7-6cbb-4666-96f9-5c02d73e6661"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Joined the group (or already a member)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Group"
                },
                "example": {
                  "id": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                  "chatId": "757dfe66-37b4-4772-baa5-8c86ec68c176",
                  "name": "Engineering Team",
                  "type": "standard",
                  "accessMode": "public",
                  "groupManagement": "allMembers",
                  "enforceThreadedMode": true,
                  "dateCreated": "2026-01-21T10:30:00Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/Error",
            "description": "Invalid request, e.g. missing id, archived group, or unsupported group type."
          },
          "401": {
            "$ref": "#/components/responses/Error",
            "description": "Unauthorized."
          },
          "403": {
            "$ref": "#/components/responses/Error",
            "description": "Private group, or public group outside the caller's Roam."
          },
          "404": {
            "$ref": "#/components/responses/Error",
            "description": "Group not found."
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/group.remove": {
      "post": {
        "tags": [
          "Groups"
        ],
        "summary": "Remove group members",
        "description": "Remove one or more group members.\n\nMembers can be specified by user ID or email address.\n\nApps may remove members from a group if one of the following conditions is true:\n1. It is a public group in their Roam.\n2. They are a member of the group.\n\nRemoving members with the Admin role is not yet supported.\n\n**Required scope:** `group:write`\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "group.remove",
        "security": [
          {
            "bearer": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Group ID"
                  },
                  "members": {
                    "type": "array",
                    "description": "List of member IDs or email addresses to remove",
                    "items": {
                      "type": "string"
                    }
                  }
                },
                "required": [
                  "id",
                  "members"
                ]
              },
              "examples": {
                "withIds": {
                  "summary": "Remove members by user ID",
                  "value": {
                    "id": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                    "members": [
                      "709b8a57-70bc-427a-b6f0-b16ba5297f8c"
                    ]
                  }
                },
                "withEmails": {
                  "summary": "Remove members by email",
                  "value": {
                    "id": "88bebce7-6cbb-4666-96f9-5c02d73e6661",
                    "members": [
                      "alex.chen@example.com"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Members removed successfully"
          },
          "400": {
            "$ref": "#/components/responses/Error",
            "description": "Invalid request, e.g. group does not exist or incorrect user IDs."
          },
          "401": {
            "$ref": "#/components/responses/Error",
            "description": "Unauthorized."
          },
          "403": {
            "$ref": "#/components/responses/Error",
            "description": "App does not have permission to remove members from this group."
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/token.info": {
      "get": {
        "tags": [
          "App"
        ],
        "summary": "Access token info",
        "description": "Get information about the access token, including the authenticated user/bot\nand granted scopes.\n\n**No specific scope required.**\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "token.info",
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Token info retrieved successfully",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "user": {
                      "type": "object",
                      "description": "The authenticated user or bot",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid",
                          "description": "User/bot ID"
                        },
                        "name": {
                          "type": "string",
                          "description": "Display name"
                        },
                        "imageUrl": {
                          "type": "string",
                          "format": "uri",
                          "description": "Profile image URL"
                        },
                        "email": {
                          "type": "string",
                          "format": "email",
                          "description": "Email address. Included for personal access tokens with the `user:read.email` scope."
                        }
                      },
                      "required": [
                        "id",
                        "name"
                      ]
                    },
                    "bot": {
                      "type": "object",
                      "description": "The bot persona associated with the token. Present only for personal\naccess tokens, where `user` is the authenticated person and `bot` is\nthe persona that messages are posted as. Omitted for organization\ntokens, where `user` is the app's own identity.\n",
                      "properties": {
                        "id": {
                          "type": "string",
                          "format": "uuid",
                          "description": "Bot ID"
                        },
                        "name": {
                          "type": "string",
                          "description": "Bot display name"
                        },
                        "imageUrl": {
                          "type": "string",
                          "format": "uri",
                          "description": "Bot profile image URL"
                        }
                      },
                      "required": [
                        "id",
                        "name"
                      ]
                    },
                    "clientId": {
                      "type": "string",
                      "description": "The API client (app) ID this token belongs to."
                    },
                    "scopes": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "List of OAuth scopes granted to this token"
                    },
                    "roam": {
                      "type": "object",
                      "description": "Information about the Roam workspace",
                      "properties": {
                        "id": {
                          "type": "string",
                          "description": "The Roam's external ID"
                        },
                        "name": {
                          "type": "string",
                          "description": "The Roam's display name"
                        },
                        "imageUrl": {
                          "type": "string",
                          "format": "uri",
                          "description": "URL of the Roam's profile image"
                        },
                        "iconUrl": {
                          "type": "string",
                          "format": "uri",
                          "description": "URL of the Roam's icon"
                        }
                      }
                    }
                  },
                  "required": [
                    "user",
                    "clientId",
                    "scopes",
                    "roam"
                  ]
                },
                "example": {
                  "user": {
                    "id": "b893c426-6d54-4d9a-8e71-6bd53b26124e",
                    "name": "My Bot",
                    "imageUrl": "https://ro.am/card-images/b893c426-6d54-4d9a-8e71-6bd53b26124e"
                  },
                  "clientId": "8f3a1c2e-9b4d-4f6a-bc11-2d3e4f5a6b7c",
                  "scopes": [
                    "chat:read",
                    "chat:write",
                    "user:read",
                    "user:read.email"
                  ],
                  "roam": {
                    "id": "12QJUIKR29",
                    "name": "Acme Corp",
                    "imageUrl": "https://ro.am/card-images/b5313458-1363-4832-a408-deaf740ad014",
                    "iconUrl": "https://ro.am/card-images/08705e4f-7e32-4a5f-8f57-39e24eaadcbd"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    },
    "/token.revoke": {
      "post": {
        "tags": [
          "App"
        ],
        "summary": "Revoke access token",
        "description": "Permanently revoke the presented OAuth access token **and its refresh\ntoken**. After a successful response the grant is dead — refresh will not\nresurrect it; the client must re-authorize.\n\nThis does **not** uninstall your app from the workspace, delete webhook\nsubscriptions, or affect other users' tokens. For install removal see the\n[`app.uninstalled`](/docs/webhooks/app-uninstalled) event (fired from admin\n/ Dev Settings uninstall paths, not from this endpoint).\n\nOn success Roam also delivers a [`token.revoked`](/docs/webhooks/token-revoked)\nwebhook to your subscriptions (`reason: \"api_revoked\"`), including to the\nsame app that called this endpoint. Treat that delivery as idempotent.\n\nSubsequent API calls with the revoked access token return HTTP `401` with\n`invalid_token` (the token row is gone). Distinct from `token_revoked`,\nwhich signals an archived person or archived client while a credential may\nstill exist.\n\nThis operation is only valid for OAuth access tokens, not for API keys.\n\n**Access:** Organization and Personal (OAuth access tokens only). Personal\ntokens may revoke their own grant. API keys cannot use this endpoint.\n\n**No specific scope required.**\n\n---\n\n**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)\n",
        "operationId": "token.revoke",
        "security": [
          {
            "bearer": []
          }
        ],
        "responses": {
          "200": {
            "description": "Token successfully revoked"
          },
          "400": {
            "description": "Bad request. Common causes:\n- Token is an API key (not revocable via this endpoint)\n",
            "$ref": "#/components/responses/Error"
          },
          "401": {
            "description": "Presented invalid authentication credentials.",
            "$ref": "#/components/responses/Error"
          },
          "500": {
            "description": "An internal error occurred."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearer": {
        "type": "http",
        "scheme": "bearer",
        "description": "Pass your API Key or OAuth access token as a Bearer token.\nExample: `Authorization: Bearer <token>`\n"
      }
    },
    "schemas": {
      "User": {
        "type": "object",
        "description": "A v1 acting principal. Workspace members and guests have `type: user`;\nclassic bots, agents, assistants, and coworkers have `type: bot`.\nSee [Identity & Principals](/docs/guides/identity-and-principals).\n",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The principal's unique address identifier."
          },
          "type": {
            "type": "string",
            "enum": [
              "user",
              "bot"
            ],
            "description": "Stable public principal type. All automated actors are `bot`."
          },
          "name": {
            "type": "string",
            "description": "Display name of the principal."
          },
          "imageUrl": {
            "type": "string",
            "format": "uri",
            "description": "URL of the principal's profile image."
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Email address for a member or guest (requires `user:read.email`). Omitted for bots."
          },
          "isGuest": {
            "type": "boolean",
            "enum": [
              true
            ],
            "description": "Present and true only for users without membership in the caller's account."
          },
          "isAdmin": {
            "type": "boolean",
            "description": "Whether a workspace member is an admin. Present for members even when false; omitted for guests and bots."
          },
          "jobTitle": {
            "type": "string",
            "description": "Workspace member's job title. Omitted for guests and bots."
          },
          "location": {
            "type": "string",
            "description": "Workspace member's location. Omitted for guests and bots."
          },
          "status": {
            "type": "string",
            "enum": [
              "checkedIn",
              "checkedOut"
            ],
            "description": "User's current presence status. Only included when `expand=status` is requested and the `user:read.status` scope is granted."
          },
          "willReturn": {
            "type": "object",
            "description": "Out-of-office / \"Will Return\" status. Present only when `expand=status` is requested, the `user:read.status` scope is granted, and the user has a future return time. A user can be `checkedIn` and still have `willReturn` (multi-day Out of Roam) — key off the presence of this object rather than `status` alone.",
            "properties": {
              "returnTime": {
                "type": "string",
                "format": "date-time",
                "description": "When the user is expected to return (RFC 3339)."
              },
              "reason": {
                "type": "string",
                "description": "Optional absence message (e.g. \"On Vacation\")."
              },
              "outOfRoam": {
                "type": "boolean",
                "description": "When true, multi-day Out of Roam that persists across check-ins. When false or omitted, same-day Will Return Today."
              }
            },
            "required": [
              "returnTime"
            ]
          },
          "available": {
            "type": "boolean",
            "description": "Whether the user is currently available for visitors. Only included when `expand=available` is requested and the `user:read.status` scope is granted."
          },
          "botCode": {
            "type": "string",
            "description": "Classic bot persona identifier, when available."
          },
          "integrationId": {
            "type": "string",
            "description": "Integration/client identifier for an automated actor, when available."
          }
        },
        "required": [
          "id",
          "type",
          "name"
        ]
      },
      "Group": {
        "type": "object",
        "description": "A group (channel) in the Roam workspace",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The group's unique identifier"
          },
          "chatId": {
            "type": "string",
            "format": "uuid",
            "description": "The group's channel chat ID. Populated by `group.create` and\n`group.info` (the channel chat is created together with the group), and\nused to post to or read the channel via the chat endpoints.\n"
          },
          "name": {
            "type": "string",
            "description": "Name of the group"
          },
          "type": {
            "type": "string",
            "enum": [
              "standard",
              "magicast",
              "meeting",
              "roam",
              "onair",
              "community"
            ],
            "description": "The type of group:\n- `standard` - A regular channel created by users\n- `magicast` - A Magicast channel\n- `meeting` - A meeting channel\n- `roam` - The main Roam channel (one per workspace)\n- `onair` - An On-Air channel\n- `community` - A community channel\n"
          },
          "accessMode": {
            "type": "string",
            "enum": [
              "public",
              "private"
            ],
            "description": "Whether the group is public or private"
          },
          "groupManagement": {
            "type": "string",
            "enum": [
              "allMembers",
              "groupAdminsOnly"
            ],
            "description": "Who can manage group settings and membership"
          },
          "enforceThreadedMode": {
            "type": "boolean",
            "default": false,
            "description": "Whether the group enforces threaded conversations"
          },
          "dateCreated": {
            "type": "string",
            "format": "date-time",
            "description": "When the group was created"
          },
          "imageUrl": {
            "type": "string",
            "format": "uri",
            "description": "URL of the group's image"
          }
        },
        "required": [
          "id",
          "name",
          "type"
        ]
      },
      "GroupMember": {
        "type": "object",
        "description": "A member of a group with their role",
        "properties": {
          "userId": {
            "type": "string",
            "format": "uuid",
            "description": "The user's unique identifier"
          },
          "role": {
            "type": "string",
            "enum": [
              "member",
              "admin"
            ],
            "description": "The member's role in the group"
          }
        },
        "required": [
          "userId",
          "role"
        ]
      },
      "ChatMessage": {
        "type": "object",
        "description": "A chat message in the Roam workspace",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "message"
            ],
            "description": "Message type identifier"
          },
          "userId": {
            "type": "string",
            "format": "uuid",
            "description": "Principal ID of the message sender. Resolve with `user.info`."
          },
          "userType": {
            "type": "string",
            "enum": [
              "user",
              "bot"
            ],
            "description": "Principal type of `userId`; always equals `user.info.type` for the same credentials. Use it to prevent bot loops without another lookup."
          },
          "chatId": {
            "type": "string",
            "format": "uuid",
            "description": "ID of the chat the message belongs to"
          },
          "timestamp": {
            "type": "integer",
            "description": "Message key as Unix microseconds"
          },
          "threadTimestamp": {
            "type": "integer",
            "description": "Unix microseconds timestamp of the parent message (for thread replies)"
          },
          "replyTimestamp": {
            "type": "integer",
            "description": "Timestamp of the message this one quotes — a DM or channel-thread quoted reply, set via the `replyTimestamp` request field on chat.post. Omitted otherwise."
          },
          "ephemeral": {
            "type": "boolean",
            "description": "Whether the message is ephemeral. Always omitted (false) in chat.history, chat.search, and chat.link.resolve responses — ephemeral messages (posted via chat.postEphemeral) are never persisted, so they never appear in these APIs."
          },
          "text": {
            "type": "string",
            "description": "Text of the message, formatted as github-flavored markdown. Mention tokens use Slack's syntax: `<@ID>` is a principal (user or bot — resolve with `user.info`), `<!subteam^ID>` is a group or channel (resolve with `group.info`), and `<!channel>` is the broadcast keyword."
          },
          "contentType": {
            "type": "string",
            "enum": [
              "text",
              "voice",
              "block",
              "poll"
            ],
            "description": "Type of message content: `text` (markdown body, optionally with items), `voice` (voice note), `block` (rich block layout), or `poll`."
          },
          "items": {
            "type": "array",
            "description": "Items attached to this message",
            "items": {
              "$ref": "#/components/schemas/ChatItem"
            }
          },
          "poll": {
            "type": "object",
            "description": "Poll content, present when contentType is `poll`.",
            "properties": {
              "question": {
                "type": "string",
                "description": "The poll question."
              },
              "options": {
                "type": "array",
                "description": "The poll answer options.",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Unique option identifier."
                    },
                    "text": {
                      "type": "string",
                      "description": "Option display text."
                    }
                  }
                }
              },
              "allowMultipleAnswers": {
                "type": "boolean",
                "description": "Whether voters can select multiple options."
              },
              "closesAt": {
                "type": "string",
                "format": "date-time",
                "description": "When the poll closes (RFC-3339). Omitted if no close time is set."
              }
            }
          },
          "voice": {
            "type": "object",
            "description": "Voice-note content, present when contentType is `voice`.",
            "properties": {
              "audioUrl": {
                "type": "string",
                "format": "uri",
                "description": "URL of the voice-note audio (m4a)."
              },
              "duration": {
                "type": "integer",
                "description": "Duration of the voice note in milliseconds."
              },
              "transcript": {
                "type": "string",
                "description": "Text transcript of the voice note. Omitted if not yet available."
              }
            },
            "required": [
              "audioUrl",
              "duration"
            ]
          },
          "blocks": {
            "type": "array",
            "description": "Rich block layout, present when contentType is `block`.",
            "items": {
              "type": "object",
              "additionalProperties": true,
              "description": "An opaque block object."
            }
          },
          "color": {
            "type": "string",
            "description": "Accent color for a block message, present when contentType is `block`. Omitted otherwise."
          },
          "replyCount": {
            "type": "integer",
            "description": "Number of replies in this message's thread. Omitted when zero."
          },
          "sender": {
            "type": "object",
            "description": "Per-message sender display override supplied at send time via the\nrequest's `sender` field. Present only when the stored message carries\none. Additive: `userId` remains the authoring identity — render the\noverride, attribute with `userId`. See the\n[Sender Profiles guide](/docs/guides/sender-profiles).\n",
            "properties": {
              "name": {
                "type": "string",
                "description": "Display name override for this message."
              },
              "imageUrl": {
                "type": "string",
                "format": "uri",
                "description": "Avatar URL override for this message."
              }
            }
          },
          "mentions": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Flat, order-preserving, de-duplicated list of everything referenced by\nmention tokens in `text`: bare address UUIDs (from both `<@ID>` principal\nand `<!subteam^ID>` group tokens), plus the literal `all` for the `<!channel>`\nbroadcast keyword. Present when the text contains mentions. Resolve UUIDs\nto display info via the response's `addresses` map (request\n`expand=addresses`).\n"
          }
        },
        "required": [
          "contentType",
          "userId",
          "userType",
          "timestamp"
        ]
      },
      "Reaction": {
        "type": "object",
        "description": "One emoji reaction on a message, grouped across reactors\n(Slack-style `{name, count, users}`).\n",
        "properties": {
          "name": {
            "type": "string",
            "description": "Reaction shortcode without surrounding colons (e.g. `thumbs_up`, `wave`,\n`heart`). Matches the `name` accepted by `reaction.add` / `reaction.remove`\nand delivered on the `chat.reaction` webhook.\n"
          },
          "count": {
            "type": "integer",
            "description": "Number of users who added this reaction."
          },
          "users": {
            "type": "array",
            "description": "Visible principal IDs of reactors. Hydrate with `user.list?ids`; unauthorized IDs are omitted and `count` reflects this array.",
            "items": {
              "type": "string",
              "format": "uuid"
            }
          }
        },
        "required": [
          "name",
          "count",
          "users"
        ]
      },
      "Sender": {
        "type": "object",
        "description": "Optional sender customization — see the\n[Sender Profiles guide](/docs/guides/sender-profiles).\n\n`name` / `imageUrl` are **per-message display overrides**: they are stored on\nthe message itself and never modify your app's (or a persona's) profile.\n`id` selects a **configured bot persona** (Roam Administration > Developer >\nedit your app > Add Bot Persona) as the message author; an id that doesn't\nmatch a configured persona is accepted and ignored, and the message is\nauthored by the app's root identity.\n\nPersonal access tokens reject this field (400).\n",
        "properties": {
          "id": {
            "type": "string",
            "description": "Code of a configured bot persona to author the message as (trimmed,\ncase-insensitive). Omitted, empty, or `_` posts as the app's root\nidentity. Unconfigured ids are accepted and ignored — supplying an id\nnever creates a persona.\n"
          },
          "name": {
            "type": "string",
            "maxLength": 128,
            "description": "Display name override for this message only (max 128 characters).\nDoes not rename the app or persona.\n"
          },
          "imageUrl": {
            "type": "string",
            "format": "uri",
            "description": "Avatar URL override for this message only. Must be an absolute\nHTTP(S) URL.\n"
          }
        }
      },
      "MeetingParticipant": {
        "type": "object",
        "description": "A participant in a meeting",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "member",
              "guest"
            ],
            "description": "Whether the participant is a workspace member or an external guest"
          },
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The participant's address ID"
          },
          "name": {
            "type": "string",
            "description": "Display name of the participant"
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Email address of the participant (requires `user:read.email` scope)"
          }
        },
        "required": [
          "type",
          "id",
          "name"
        ]
      },
      "Address": {
        "type": "object",
        "description": "A resolved address. Which fields are populated depends on `type`: `user`\naddresses include `email`/`isGuest`; `bot` addresses include\n`botCode`/`integrationId` when available; group addresses include only the\ncommon fields. Classic bots, agents, assistants, and coworkers all project\nas `type: bot`.\n",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "The address ID."
          },
          "type": {
            "type": "string",
            "enum": [
              "user",
              "bot",
              "standardGroup",
              "meetingGroup",
              "userGroup",
              "teamRoam"
            ],
            "description": "The kind of address."
          },
          "displayName": {
            "type": "string",
            "description": "Display name of the address."
          },
          "displayImageUrl": {
            "type": "string",
            "format": "uri",
            "description": "Display image URL of the address."
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Email address. Present on `user` addresses, and only when the token has the `user:read.email` scope."
          },
          "isGuest": {
            "type": "boolean",
            "enum": [
              true
            ],
            "description": "Present and true only when the user has no membership in the caller's account."
          },
          "botCode": {
            "type": "string",
            "description": "Classic bot persona identifier, when available. Other bot-like actors may omit it."
          },
          "integrationId": {
            "type": "string",
            "description": "Integration/client ID, when available. Some bot-like actors omit it."
          }
        },
        "required": [
          "id",
          "type"
        ]
      },
      "Magicast": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Unique identifier for the magicast"
          },
          "name": {
            "type": "string",
            "description": "Display name of the magicast"
          },
          "createdAt": {
            "type": "string",
            "format": "date-time",
            "description": "ISO-8601 timestamp when the magicast was created (UTC)"
          },
          "ownerId": {
            "type": "string",
            "format": "uuid",
            "description": "Address ID of the magicast owner"
          },
          "coverImageUrl": {
            "type": "string",
            "format": "uri",
            "description": "URL for the magicast cover image thumbnail"
          }
        },
        "required": [
          "id",
          "name",
          "createdAt"
        ]
      },
      "ActionItem": {
        "type": "object",
        "description": "An AI-extracted action item from a meeting. `complete` reflects whether the\ntask has been marked done. Assignment has two forms: an explicit `assigneeId`\n(a user the item was assigned to) and a `suggestedAssigneeId` (an AI-suggested\nowner); `suggestedAssigneeName` is the display name for whichever applies.\n",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Action item ID."
          },
          "title": {
            "type": "string",
            "description": "Action item title."
          },
          "description": {
            "type": "string",
            "description": "Longer description of the action item."
          },
          "complete": {
            "type": "boolean",
            "description": "Whether the action item has been marked complete. Omitted when false."
          },
          "assigneeId": {
            "type": "string",
            "format": "uuid",
            "description": "User ID this item was explicitly assigned to, if any."
          },
          "suggestedAssigneeId": {
            "type": "string",
            "format": "uuid",
            "description": "AI-suggested assignee user ID, if any."
          },
          "suggestedAssigneeName": {
            "type": "string",
            "description": "Display name for the assignee — resolved from `assigneeId` when set, otherwise the AI-suggested name."
          },
          "assignedToMe": {
            "type": "boolean",
            "description": "Whether this item is assigned to the authenticated user. Personal access tokens only."
          },
          "suggestedForMe": {
            "type": "boolean",
            "description": "Whether this item is AI-suggested for the authenticated user. Personal access tokens only."
          }
        },
        "required": [
          "title"
        ]
      },
      "Error": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean",
            "enum": [
              false
            ],
            "description": "Always `false` on error responses."
          },
          "error": {
            "type": "string",
            "description": "Machine-readable error code from the catalog\n(e.g. `invalid_token`, `missing_scope`, `ratelimited`, `invalid_cursor`).\nBranch on this field. See [Responses and Errors](/docs/guides/responses-and-errors).\n"
          },
          "needed": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Present on `missing_scope`. Scopes that would satisfy the check.\n**Any-of (OR)** semantics: holding any one element is enough.\nDistinct from Slack's comma-separated `needed` string.\n"
          },
          "provided": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Present on `missing_scope`. The token's granted scopes after alias\nnormalization (e.g. legacy `groups:read` reports as `group:read`).\nFor personal tokens this is the expanded OAuth set, not `pat:*`\ngroup names.\n"
          }
        },
        "required": [
          "ok",
          "error"
        ]
      },
      "UnfurlContent": {
        "type": "object",
        "description": "Rich preview content supplied by an app for one exact URL.",
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 300,
            "description": "Title displayed on the preview card."
          },
          "description": {
            "type": "string",
            "maxLength": 3000,
            "description": "Optional preview summary."
          },
          "siteName": {
            "type": "string",
            "maxLength": 100,
            "description": "Optional service or website name."
          },
          "favicon": {
            "type": "string",
            "format": "uri",
            "maxLength": 2048,
            "description": "Optional HTTPS favicon URL. Roam's server does not fetch it."
          },
          "image": {
            "type": "object",
            "properties": {
              "url": {
                "type": "string",
                "format": "uri",
                "maxLength": 2048,
                "description": "HTTPS image URL. Roam's server does not fetch it."
              },
              "type": {
                "type": "string",
                "maxLength": 100,
                "description": "Image media type, such as `image/png`."
              },
              "width": {
                "type": "integer",
                "minimum": 1
              },
              "height": {
                "type": "integer",
                "minimum": 1
              },
              "alt": {
                "type": "string",
                "maxLength": 500,
                "description": "Accessible alternative text for the image."
              }
            },
            "required": [
              "url"
            ]
          }
        },
        "required": [
          "title"
        ]
      },
      "ChatItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "UUID identifying this item"
          },
          "type": {
            "type": "string",
            "enum": [
              "photo",
              "blob"
            ],
            "description": "Type of item:\n\n- **photo**: Images with inline preview and thumbnail\n  - image/jpeg, image/png, image/gif, image/webp\n\n- **blob**: Any other file type (download only, no preview)\n  - application/octet-stream\n"
          },
          "mime": {
            "type": "string",
            "description": "MIME type of the file (e.g., \"application/octet-stream\").\nMay be omitted for photo items where the type is inferred from the image format.\n"
          },
          "created": {
            "type": "string",
            "format": "date-time",
            "description": "Timestamp when the item was created"
          },
          "name": {
            "type": "string",
            "description": "Name of the item (typically the filename)."
          },
          "url": {
            "type": "string",
            "format": "uri",
            "description": "URL for the uploaded item."
          },
          "thumbnail": {
            "type": "string",
            "format": "uri",
            "description": "URL for a thumbnail of the uploaded item (photo type only).\nThis may be equal to the item's main URL if it is suitable to use as a thumbnail.\n"
          },
          "size": {
            "type": "integer",
            "description": "Size of the item in bytes"
          },
          "width": {
            "type": "integer",
            "description": "Width in pixels (images only)"
          },
          "height": {
            "type": "integer",
            "description": "Height in pixels (images only)"
          }
        },
        "required": [
          "id",
          "type",
          "created",
          "name",
          "url"
        ]
      },
      "UserAuditLog": {
        "type": "object",
        "properties": {
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "description": "Time at which the audit log entry occurred (UTC)"
          },
          "eventType": {
            "type": "string",
            "description": "Type of the audit event"
          },
          "name": {
            "type": "string",
            "description": "Name of the user associated with this event"
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Email address of the user associated with this event"
          },
          "data": {
            "type": "object",
            "description": "JSON payload containing event type specific data, such as chat recipients or which room was knocked on",
            "additionalProperties": true
          },
          "platform": {
            "type": "string",
            "enum": [
              "web",
              "electron",
              "ios",
              "android",
              "sip",
              "bot"
            ],
            "description": "Best-effort client platform the user was on for this event. Reliably present for ENTER_ROAM events; may be absent for some events. Maps to usage categories as: mobile = ios/android, desktop = electron, web = web."
          }
        }
      },
      "LobbyBookingHost": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Display name of the host"
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Email address of the host"
          },
          "isOrganizer": {
            "type": "boolean",
            "description": "Whether this host is the organizer"
          }
        },
        "required": [
          "email",
          "isOrganizer"
        ]
      },
      "LobbyBookingInvitee": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "description": "Display name of the invitee"
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Email address of the invitee"
          },
          "status": {
            "type": "string",
            "description": "Invitee's RSVP or booking status"
          },
          "isBooker": {
            "type": "boolean",
            "description": "Whether this invitee created the booking"
          }
        },
        "required": [
          "email",
          "status"
        ]
      },
      "LobbyBookingResponse": {
        "type": "object",
        "description": "A guest's answer to one of the lobby's custom questions, captured when the booking\nwas made. Includes answers to hidden fields, which are populated from URL query\nparameters on the lobby link (e.g. `?utm_source=partner`).\n",
        "properties": {
          "fieldId": {
            "type": "string",
            "description": "ID of the custom field (question) this answer belongs to."
          },
          "key": {
            "type": "string",
            "description": "The field's stable key, if the lobby owner assigned one, as captured when the\nbooking was made. For hidden fields this is the URL query parameter name used to\npopulate the value.\n"
          },
          "question": {
            "type": "string",
            "description": "The question's display name. Omitted if the field definition can no longer be\nfound on the lobby configuration.\n"
          },
          "type": {
            "type": "string",
            "enum": [
              "short_text",
              "text",
              "email",
              "phone_number",
              "radio",
              "checkbox",
              "dropdown",
              "hidden"
            ],
            "description": "The custom field type. Omitted when `question` is omitted."
          },
          "value": {
            "description": "The human-readable answer. For option fields (radio, checkbox, dropdown) this is\nthe selected option label(s), not internal option IDs. Checkbox answers are\narrays of strings; all other answers are strings.\n",
            "oneOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          }
        },
        "required": [
          "fieldId"
        ]
      },
      "LobbyBooking": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Unique booking identifier"
          },
          "start": {
            "type": "string",
            "format": "date-time",
            "description": "Start time in RFC3339"
          },
          "end": {
            "type": "string",
            "format": "date-time",
            "description": "End time in RFC3339"
          },
          "status": {
            "type": "string",
            "description": "Current status of the booking"
          },
          "timeZone": {
            "type": "string",
            "description": "IANA time zone of the booking times"
          },
          "notes": {
            "type": "string",
            "description": "Optional notes provided by the booker"
          },
          "created": {
            "type": "string",
            "format": "date-time",
            "description": "Creation time"
          },
          "hosts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LobbyBookingHost"
            }
          },
          "invitees": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/LobbyBookingInvitee"
            }
          },
          "responses": {
            "type": "array",
            "description": "The guest's answers to the lobby's custom questions, including hidden fields\npopulated from URL query parameters on the lobby link. One entry per answered\nquestion; empty or absent when the guest answered no custom questions.\n",
            "items": {
              "$ref": "#/components/schemas/LobbyBookingResponse"
            }
          },
          "meetingLink": {
            "type": "string",
            "format": "uri",
            "description": "Meeting link URL, used to join the meeting"
          }
        },
        "required": [
          "id",
          "start",
          "end",
          "status"
        ]
      },
      "MagicastChapter": {
        "type": "object",
        "description": "A navigation chapter generated from the Magicast transcript.",
        "properties": {
          "title": {
            "type": "string",
            "description": "Chapter title."
          },
          "startTime": {
            "type": "integer",
            "description": "Milliseconds from the start of the recording."
          }
        },
        "required": [
          "title",
          "startTime"
        ]
      },
      "MagicastCue": {
        "type": "object",
        "description": "One transcribed sentence. Magicast transcripts are not speaker-diarized,\nso there is no `speaker` field (unlike meeting `transcript.info` cues).\n",
        "properties": {
          "text": {
            "type": "string",
            "description": "The transcribed text."
          },
          "startOffset": {
            "type": "integer",
            "description": "Milliseconds from the start of the recording when the sentence began."
          },
          "endOffset": {
            "type": "integer",
            "description": "Milliseconds from the start of the recording when the sentence ended."
          }
        },
        "required": [
          "text",
          "startOffset",
          "endOffset"
        ]
      },
      "MagicastInfo": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Magicast"
          },
          {
            "type": "object",
            "properties": {
              "durationMs": {
                "type": "integer",
                "description": "Duration of the playable video in milliseconds. Omitted when unknown."
              },
              "videoStatus": {
                "type": "string",
                "enum": [
                  "none",
                  "processing",
                  "available"
                ],
                "description": "Where the Magicast video is:\n\n- `none` — no content file. There is nothing to play.\n- `processing` — a content file exists but is not ready yet. Call\n  `/magicast.info` again shortly. You can still mint a share link\n  with [`/magicast.shareLink`](/docs/api/magicast-share-link).\n- `available` — the video is ready. `videoUrl` is a short-lived\n  signed download URL; fetch it at download time and do not persist\n  it as the canonical link.\n"
              },
              "videoUrl": {
                "type": "string",
                "format": "uri",
                "description": "Short-lived signed URL for the video file. Present only when\n`videoStatus` is `available` and the signed URL could be minted.\nDo not persist this URL.\n"
              },
              "shareUrl": {
                "type": "string",
                "format": "uri",
                "description": "Player URL (`https://ro.am/share/{key}`) if a share link already\nexists. Omitted when nobody has minted one. Fetching this endpoint\nnever creates a share link — use\n[`/magicast.shareLink`](/docs/api/magicast-share-link) for that.\nThere is no `https://ro.am/magicast/{id}` URL.\n"
              },
              "chapters": {
                "type": "array",
                "description": "Navigation chapters. Omitted when none have been generated.",
                "items": {
                  "$ref": "#/components/schemas/MagicastChapter"
                }
              },
              "cues": {
                "type": "array",
                "description": "Flattened transcript sentences. Omitted while the transcript is still\nprocessing or unavailable. Magicasts are not meetings — there is no\nsummary or action-items field.\n",
                "items": {
                  "$ref": "#/components/schemas/MagicastCue"
                }
              }
            }
          }
        ]
      }
    },
    "responses": {
      "Error": {
        "description": "Error response. `ok` is always false; branch on the machine-readable `error` code. See the Responses and Errors guide.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "examples": {
              "missing_scope": {
                "summary": "Scope gate failure with re-auth details",
                "value": {
                  "ok": false,
                  "error": "missing_scope",
                  "needed": [
                    "chat:send_message",
                    "chat:write"
                  ],
                  "provided": [
                    "chat:read",
                    "chat:history",
                    "group:read"
                  ]
                }
              },
              "invalid_token": {
                "summary": "Unknown, malformed, or expired token",
                "value": {
                  "ok": false,
                  "error": "invalid_token"
                }
              }
            }
          }
        }
      }
    }
  }
}