# API

The Roam API provides chat and user management capabilities for building powerful integrations with your Roam Virtual Office.

**OpenAPI Spec:** [chat-v1.json](https://developer.ro.am/chat-v1.json)

See the [Migration Guide](/docs/guides/migration-v0-to-v1) for details on upgrading from v0.

Acting people and automated actors share one principal contract. See
[Identity & Principals](/docs/guides/identity-and-principals) for `userId`
taxonomy, directory versus hydration behavior, and bot-loop guidance.

## Base URL

```
https://api.ro.am/v1
```

## API version

Within the `/v1` generation, response and webhook shapes are pinned by a **dated
version string** (`YYYY-MM-DD`). The current default for new integrations is:

```
2026-08-20
```

Every REST response includes a `Roam-Version` header with the version that shaped
the body. To pin a request explicitly:

```bash
curl -H "Authorization: Bearer $ROAM_TOKEN" \
  -H "Roam-Version: 2026-08-20" \
  https://api.ro.am/v1/token.info
```

Each API client is created on the latest version available at that time and
**never advances automatically**. Webhook delivery shapes are pinned at
subscribe time. Full rules, resolution order, and support window:
[API Versioning](/docs/guides/api-versioning).

## Authentication

Every request carries a bearer token — an organization API key, an OAuth access
token, or a Personal Access Token:

```
Authorization: Bearer YOUR_TOKEN
```

Create API clients in **Roam Administration > Developer**. See
[OAuth & Authentication](/docs/guides/oauth) for the authorization flows and
[Scopes](/docs/guides/scopes) for the permission catalog.

### Authentication failures

Failed authentication returns HTTP `401 Unauthorized` with a machine-readable
`error` catalog code. Distinguish these cases:

| Code | Meaning | Client action |
|------|---------|---------------|
| `not_authed` | No `Authorization: Bearer` token was sent | Attach a token and retry |
| `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) |
| `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 |

Responses with `invalid_token` or `token_revoked` also include:

```http
WWW-Authenticate: Bearer error="invalid_token"
```

per [RFC 6750](https://datatracker.ietf.org/doc/html/rfc6750). Use that header
(or the body `error` field) to stop retry loops on dead credentials.

## Access Models

Every integration is backed by an **API client**, created in **Roam Administration →
Developer** by a workspace admin. A client has one of two authorization types:

- **API Key** — A long-lived secret for server-to-server integrations that act as the app
  itself. No user authorization step.
- **OAuth** — A client ID and secret for apps that are authorized per install, by an admin
  (organization access) or by an individual user (personal access), and that can be revoked
  or uninstalled.

Personal Access Tokens are a third credential, created by users under **User Settings →
Developer** when workspace policy allows, without registering an OAuth app.

Those credentials map onto the two access models Roam APIs support:

- **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).
- **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).

A single OAuth app can support both models. The access model is selected when the user authorizes the app.

See the [Access Models guide](/docs/guides/access-models) for a full comparison, endpoint compatibility matrix, and guidance on choosing the right model.

## Response shape

Successful JSON responses include `"ok": true`. Errors return
`"ok": false` with a machine-readable `error` code. List endpoints use
**opaque** `cursor` / `nextCursor` values — do not parse or construct them.
Full details: [Responses and Errors](/docs/guides/responses-and-errors).

```json
{ "ok": true, "…": "…" }
{ "ok": false, "error": "invalid_token" }
```

## Pagination

List endpoints return paginated results. The pagination style varies by endpoint:

**Cursor-based pagination** (most endpoints):

- `cursor`: Opaque string from a previous response's `nextCursor` field. Do not parse or construct cursors yourself.
- `limit`: Number of results per page. Default is typically 10, maximum is typically 100.

An invalid or expired cursor returns `400` with `error: "invalid_cursor"` — restart
pagination without a cursor.

**Date-range pagination** (some endpoints like `meeting.list`):

- `after`: Return items after this date (RFC3339 or YYYY-MM-DD format)
- `before`: Return items before this date
- When `after` is specified, results are returned in ascending order; otherwise descending.

Check individual endpoint documentation for specific pagination parameters and limits.

## Rate Limiting

API requests are rate limited to prevent abuse.

**Global (all credential types)**

- **Burst**: 10 requests
- **Sustained rate**: 1 request per second

**Personal Access Tokens (additional)**

PATs also have a **daily quota of 1,000 requests per day**.

Every response — success and `429` — carries quota headers for the burst
bucket. `Retry-After` is sent only on `429`.

A successful request looks like:

```http
HTTP/1.1 200 OK
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 7
X-RateLimit-Reset: 1776556801
RateLimit-Policy: "burst";q=10;w=1
RateLimit: "burst";r=7
```

A burst rejection looks like:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 10
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1776556810
RateLimit-Policy: "burst";q=10;w=1
RateLimit: "burst";r=0;t=10

{"ok":false,"error":"ratelimited"}
```

| Header | Meaning | Example |
| --- | --- | --- |
| `X-RateLimit-Limit` | Burst size | `10` |
| `X-RateLimit-Remaining` | Tokens left in the burst bucket (`0`–`10`) | `7` |
| `X-RateLimit-Reset` | Unix epoch seconds (UTC) when another burst token is available | `1776556801` |
| `Retry-After` | Seconds to wait after a `429`. Honor this over `Reset`. Typical burst wait is `10`. | `10` |
| `RateLimit-Policy` | IETF draft policy list (`q` = quota, `w` = window seconds) | `"burst";q=10;w=1` |
| `RateLimit` | IETF draft remaining (`r`). `t` (seconds until more quota) is present only when `r` is 0 | `"burst";r=7` |

PATs add a `"day"` policy on the IETF headers for the daily quota.
There is no custom `X-RateLimit-*-Day` trio — `X-RateLimit-*` is always
the burst bucket.

```http
RateLimit-Policy: "burst";q=10;w=1, "day";q=1000;w=86400
RateLimit: "burst";r=7, "day";r=847
```

`Remaining` is a hint from the appserver process that handled the request.
Replicas do not share counters, so a follow-up request may see a different
count. `Retry-After` on `429` is the authoritative wait.

Repeated abuse may enter a short **tarpit** (delayed responses) before
further requests are dropped.

**Best practices:**

- Honor `Retry-After` on `429`; do not retry sooner
- Use `X-RateLimit-Remaining` (or `RateLimit` `r=`) to slow down before you
  hit the ceiling
- Batch operations where possible to reduce request count
- For PATs, stay well under the daily 1000 budget for long-running sync jobs

## Common Error Responses

Errors return `"ok": false` and a machine-readable `error` catalog code — not a
free-text sentence. Branch on `error` rather than parsing prose.

```json
{ "ok": false, "error": "invalid_parameter" }
```

Status codes follow these conventions:

| Status | Meaning |
|--------|---------|
| `400 Bad Request` | Invalid request (malformed or missing parameters) |
| `401 Unauthorized` | Missing or invalid credentials (see [Authentication failures](#authentication-failures)) |
| `403 Forbidden` | Valid auth, but the token lacks the required scope or access to the target resource |
| `404 Not Found` | Resource not found |
| `413 Payload Too Large` | Request body or message text too large |
| `429 Too Many Requests` | Rate limited (see [Rate Limiting](#rate-limiting)) |
| `500 Internal Server Error` | Internal error |

Full envelope and `missing_scope` details: [Responses and
Errors](/docs/guides/responses-and-errors). The complete code catalog is in the
[Error Codes guide](/docs/guides/error-codes).

## Addressing

The API uses two addressing concepts:

- **Chat** (`chatId`): A conversation (DM, multi-DM, or group chat). Use for retrieving history or posting to an existing conversation.
- **Address** (`userId`, `groupId`): A destination for new messages. Use `userId` to DM a user or `groupId` to post to a group chat.

When 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).

## Replies & Threads

Messages can be replies to other messages. The API distinguishes between two types:

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

- **DM replies** (`replyTimestamp`): In DMs, replies reference the specific message being replied to via `replyTimestamp`. This is a direct reply rather than a thread.

When receiving webhook events, check for `threadTimestamp` to identify thread replies. When posting a reply, include `threadTimestamp` to reply within an existing thread.

## Endpoints Overview

### Chat & Messaging
| Endpoint | Method | Description |
|----------|--------|-------------|
| [`/chat.list`](/docs/api/chat-list) | GET | List all accessible chats (DMs, MultiDMs, Group chats) |
| [`/chat.post`](/docs/api/chat-post) | POST | Send or schedule a message (text, Block Kit, or poll) to any chat, group, or user |
| [`/chat.sendMessage`](/docs/api/chat-send-message) | POST | **Legacy** — prefer `/chat.post` |
| [`/chat.postEphemeral`](/docs/api/chat-post-ephemeral) | POST | Post a private "only you can see this" message to one member of a chat |
| [`/chat.scheduled.list`](/docs/api/chat-scheduled-list) | GET | List pending scheduled messages created via `chat.post`'s `sendAt` |
| [`/chat.scheduled.cancel`](/docs/api/chat-scheduled-cancel) | POST | Cancel a pending scheduled message before it sends |
| [`/chat.startStream`](/docs/api/chat-start-stream) | POST | Start a streaming message; returns a stream ID |
| [`/chat.appendStream`](/docs/api/chat-append-stream) | POST | Append a text chunk to an open stream |
| [`/chat.stopStream`](/docs/api/chat-stop-stream) | POST | Finalize a stream into a single persisted message |
| [`/chat.update`](/docs/api/chat-update) | POST | Edit a previously posted bot message |
| [`/chat.delete`](/docs/api/chat-delete) | POST | Delete a previously posted bot message |
| [`/chat.typing`](/docs/api/chat-typing) | POST | Show typing indicator to other participants |
| [`/chat.history`](/docs/api/chat-history) | GET | Retrieve message history for a chat |
| [`/chat.search`](/docs/api/chat-search) | POST | Full-text search over the user's accessible messages |
| [`/chat.link.resolve`](/docs/api/chat-link-resolve) | POST | Resolve a Roam chat deep link to a message reference |
| [`/chat.link.create`](/docs/api/chat-link-create) | POST | Create a shareable Roam link to a chat message |
| [`/chat.unfurl`](/docs/api/chat-unfurl) | POST | Attach rich previews to links in an existing message |
| [`/reaction.add`](/docs/api/reaction-add) | POST | Add emoji reaction to a message |
| [`/reaction.remove`](/docs/api/reaction-remove) | POST | Remove emoji reaction from a message |
| [`/reaction.list`](/docs/api/reaction-list) | GET | List all reactions on a message |
| [`/asset.create`](/docs/api/asset-create) | POST | Create a file upload (JSON/MCP-friendly); attach via `assetIds`, or use `purpose: "story"` for story media |
| [`/item.upload`](/docs/api/item-upload) | POST | Upload a file (raw bytes) to attach to a message |
| [`/story.post`](/docs/api/story-post) | POST | Post a story as the authenticated user (Personal tokens only) |

### Groups & Group Chats
| Endpoint | Method | Description |
|----------|--------|-------------|
| [`/group.list`](/docs/api/group-list) | GET | List accessible groups |
| [`/groups.list`](/docs/api/groups-list) | GET | **Legacy** — prefer `/group.list` (raw array response) |
| [`/group.info`](/docs/api/group-info) | GET | Get group details by ID or name |
| [`/group.create`](/docs/api/group-create) | POST | Create a new group chat |
| [`/group.rename`](/docs/api/group-rename) | POST | Rename an existing group |
| [`/group.archive`](/docs/api/group-archive) | POST | Archive a group |
| [`/group.members`](/docs/api/group-members) | GET | List members in a group with roles |
| [`/group.add`](/docs/api/group-add) | POST | Add members or admins to a group |
| [`/group.join`](/docs/api/group-join) | POST | Join a public group as the calling identity |
| [`/group.remove`](/docs/api/group-remove) | POST | Remove members from a group |

### Users
| Endpoint | Method | Description |
|----------|--------|-------------|
| [`/user.list`](/docs/api/user-list) | GET | List members or hydrate explicit principal IDs |
| [`/user.info`](/docs/api/user-info) | GET | Resolve a principal by ID or a member by email |
| [`/userauditlog.list`](/docs/api/userauditlog-list) | GET | List user audit log entries |
| [`/messageevent.export`](/docs/api/messageevent-export) | POST | Export daily message archives (JSON Lines) |

### Meetings
| Endpoint | Method | Description |
|----------|--------|-------------|
| [`/conversation.list`](/docs/api/conversation-list) | GET | List conversations (meetings) with participants |
| [`/meeting.list`](/docs/api/meeting-list) | GET | List meetings |
| [`/recording.list`](/docs/api/recording-list) | GET | **Legacy** — prefer `/meeting.list` / `/meeting.info` |
| [`/meeting.info`](/docs/api/meeting-info) | GET | Get meeting details with summary, action items, and chapters |
| [`/meeting.participants`](/docs/api/meeting-participants) | GET | Paginate through meeting participants |
| [`/meeting.transcript`](/docs/api/meeting-transcript) | GET | Get meeting transcript (JSON or WebVTT) |
| [`/meeting.search`](/docs/api/meeting-search) | GET | AI-powered meeting search (Personal access only) |
| [`/meeting.prompt`](/docs/api/meeting-prompt) | POST | Ask AI questions about a meeting transcript |
| [`/meeting.shareLink`](/docs/api/meeting-share-link) | POST | Get (or create) a shareable link for a meeting |
| [`/meeting.link.create`](/docs/api/meeting-link-create) | POST | Create a meeting link |
| [`/meeting.link.info`](/docs/api/meeting-link-info) | POST | Get details for a meeting link |
| [`/meeting.link.update`](/docs/api/meeting-link-update) | POST | Update a meeting link |
| [`/calendar.event.create`](/docs/api/calendar-event-create) | POST | Create a calendar event with a Roam meeting link |
| [`/calendar.list`](/docs/api/calendar-list) | GET | List events from the user's connected calendars |
| [`/lobby.list`](/docs/api/lobby-list) | GET | List active lobbies |
| [`/lobby.booking.list`](/docs/api/lobby-booking-list) | GET | List bookings for a lobby |

### Magicasts
| Endpoint | Method | Description |
|----------|--------|-------------|
| [`/magicast.list`](/docs/api/magicast-list) | GET | List magicasts |
| [`/magicast.info`](/docs/api/magicast-info) | GET | Get magicast details, transcript, and video |
| [`/magicast.shareLink`](/docs/api/magicast-share-link) | POST | Get (or create) a shareable player link |

### App Management
| Endpoint | Method | Description |
|----------|--------|-------------|
| [`/token.info`](/docs/api/token-info) | GET | Get info about the current access token |
| [`/token.revoke`](/docs/api/token-revoke) | POST | Revoke access token |
| [`/webhook.list`](/docs/webhooks/webhook-list) | GET | List webhook subscriptions |
| [`/webhook.subscribe`](/docs/webhooks/webhook-subscribe) | POST | Create or update a webhook subscription (dotted event names) |
| [`/webhook.unsubscribe`](/docs/webhooks/webhook-unsubscribe) | POST | Remove a webhook subscription |
| [`/webhook.deliveries`](/docs/webhooks/webhook-deliveries) | GET | List failed deliveries |

## Common Use Cases

### Build a Chat Bot
Create an OAuth app with `chat:read` and `chat:send_message` scopes, configure a webhook URL to receive messages, then respond programmatically.
- Subscribe to [`chat.message`](/docs/webhooks/chat-message) events
- Use [`/reaction.add`](/docs/api/reaction-add) to acknowledge receipt
- Use [`/chat.typing`](/docs/api/chat-typing) to show typing indicator
- Use [`/chat.post`](/docs/api/chat-post) to send a response

#### Responding privately
To reply so that only one person sees it — validation errors, permission
warnings, "here's how to use this command" help — use
[`/chat.postEphemeral`](/docs/api/chat-post-ephemeral) with the `chatId` and
the `userId` of the person to address. The recipient sees the message in the
shared chat under an "Only you can see this" header; nobody else sees
anything, and nothing is stored in history. Ephemeral messages are
best-effort and desktop/web-only — anything the recipient must durably
receive belongs in a DM (`/chat.post` with `userIds`) instead.

---
Have questions? Contact us via [Team Roam Support Chat](https://ro.am/support/contact-us).


## Base URL

```
https://api.ro.am/v1
```

## Authentication

```
Authorization: Bearer YOUR_API_KEY
```

## All Endpoints

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | [`/chat.list`](/docs/api/chat-list) | List chats |
| POST | [`/chat.post`](/docs/api/chat-post) | Send a message |
| POST | [`/chat.sendMessage`](/docs/api/chat-send-message) | Send a message (Legacy) |
| POST | [`/chat.postEphemeral`](/docs/api/chat-post-ephemeral) | Post an ephemeral message |
| GET | [`/chat.scheduled.list`](/docs/api/chat-scheduled-list) | List scheduled messages |
| POST | [`/chat.scheduled.cancel`](/docs/api/chat-scheduled-cancel) | Cancel a scheduled message |
| POST | [`/chat.startStream`](/docs/api/chat-start-stream) | Start a streaming message |
| POST | [`/chat.appendStream`](/docs/api/chat-append-stream) | Append to a streaming message |
| POST | [`/chat.stopStream`](/docs/api/chat-stop-stream) | Stop a streaming message |
| POST | [`/chat.update`](/docs/api/chat-update) | Update a message |
| POST | [`/chat.delete`](/docs/api/chat-delete) | Delete a message |
| POST | [`/chat.typing`](/docs/api/chat-typing) | Send a typing indicator |
| GET | [`/chat.history`](/docs/api/chat-history) | Get chat messages |
| POST | [`/chat.search`](/docs/api/chat-search) | Search chat messages |
| POST | [`/chat.link.resolve`](/docs/api/chat-link-resolve) | Resolve a chat link |
| POST | [`/chat.link.create`](/docs/api/chat-link-create) | Create a chat message link |
| POST | [`/chat.unfurl`](/docs/api/chat-unfurl) | Unfurl a link |
| POST | [`/reaction.add`](/docs/api/reaction-add) | Add reaction to message |
| POST | [`/reaction.remove`](/docs/api/reaction-remove) | Remove reaction from message |
| GET | [`/reaction.list`](/docs/api/reaction-list) | List reactions on a message |
| POST | [`/asset.create`](/docs/api/asset-create) | Create a file upload |
| POST | [`/item.upload`](/docs/api/item-upload) | Upload an item |
| POST | [`/story.post`](/docs/api/story-post) | Post story |
| GET | [`/user.list`](/docs/api/user-list) | List users |
| GET | [`/user.info`](/docs/api/user-info) | Get user info |
| GET | [`/userauditlog.list`](/docs/api/userauditlog-list) | User Audit Log |
| POST | [`/messageevent.export`](/docs/api/messageevent-export) | Export message events |
| GET | [`/conversation.list`](/docs/api/conversation-list) | List conversations |
| GET | [`/meeting.list`](/docs/api/meeting-list) | List meetings |
| GET | [`/recording.list`](/docs/api/recording-list) | List meeting recordings (Legacy) |
| GET | [`/meeting.info`](/docs/api/meeting-info) | Get meeting info |
| GET | [`/meeting.participants`](/docs/api/meeting-participants) | List meeting participants |
| GET | [`/meeting.transcript`](/docs/api/meeting-transcript) | Get meeting transcript |
| GET | [`/meeting.search`](/docs/api/meeting-search) | Search meetings |
| POST | [`/meeting.prompt`](/docs/api/meeting-prompt) | Prompt about a meeting |
| POST | [`/meeting.shareLink`](/docs/api/meeting-share-link) | Get a shareable meeting link |
| POST | [`/meeting.link.create`](/docs/api/meeting-link-create) | Create a meeting link |
| POST | [`/meeting.link.info`](/docs/api/meeting-link-info) | Get a meeting link |
| POST | [`/meeting.link.update`](/docs/api/meeting-link-update) | Update a meeting link |
| POST | [`/calendar.event.create`](/docs/api/calendar-event-create) | Create a calendar event |
| GET | [`/calendar.list`](/docs/api/calendar-list) | List calendar events |
| GET | [`/lobby.list`](/docs/api/lobby-list) | List lobbies |
| GET | [`/lobby.booking.list`](/docs/api/lobby-booking-list) | List lobby bookings |
| GET | [`/magicast.list`](/docs/api/magicast-list) | List magicasts |
| GET | [`/magicast.info`](/docs/api/magicast-info) | Get magicast info |
| POST | [`/magicast.shareLink`](/docs/api/magicast-share-link) | Get a shareable Magicast link |
| GET | [`/group.list`](/docs/api/group-list) | List groups |
| GET | [`/groups.list`](/docs/api/groups-list) | List groups (Legacy) |
| GET | [`/group.info`](/docs/api/group-info) | Get group info |
| POST | [`/group.create`](/docs/api/group-create) | Create a group |
| POST | [`/group.rename`](/docs/api/group-rename) | Rename a group |
| POST | [`/group.archive`](/docs/api/group-archive) | Archive a group |
| GET | [`/group.members`](/docs/api/group-members) | List group members |
| POST | [`/group.add`](/docs/api/group-add) | Add group members |
| POST | [`/group.join`](/docs/api/group-join) | Join a group |
| POST | [`/group.remove`](/docs/api/group-remove) | Remove group members |
| GET | [`/token.info`](/docs/api/token-info) | Access token info |
| POST | [`/token.revoke`](/docs/api/token-revoke) | Revoke access token |

## Contact

- Website: https://ro.am/support/contact-us

---

*Machine-readable API documentation.*
*Full documentation: https://developer.ro.am/docs/api*
