Skip to main content

Migration Guide: v0 to v1

This guide covers the changes between the Roam API v0 (Alpha) and v1, and how to update your integration.

Overview

v1 consolidates conventions across endpoints and simplifies several aspects in response to developer feedback:

  • Simplified IDs: v0 used tagged IDs (e.g. U-…, G-…) so the prefix encoded the resource type. v1 uses plain UUIDs and separate properties instead — e.g. userIds vs groupId on chat.post, or an explicit type on list items — so you never parse a tag off the ID.
  • Conversations vs meetings: v0's general meeting listing (meeting.list) becomes conversation.list — every conversation that occurred in the Roam. Separately, v0's transcript and recording surfaces (transcript.*, recording.list) consolidate under v1 meeting.*: a v1 "meeting" is a conversation that has Magic Minutes and/or a video recording. Use meeting.list / meeting.info for that subset (summary, action items, chapters, hasVideo / videoStatus), and meeting.transcript for transcript content.
  • Webhooks (see Webhook Changes below):
    • Common event envelope — every delivery has top-level type, eventId, timestamp, apiVersion, and data, so one HTTPS URL can accept many event types and route on type
    • Dot event nameschat.message instead of colon forms like chat:message:dm
    • Message event consolidation — v0's chat:message:dm / channel / mention are one chat.message event with filters
  • Convention consistency: All endpoints follow singular noun.verb naming. Updates use POST with JSON body, reads use GET with query params.

v0 endpoints and webhooks will continue to be supported indefinitely, but new functionality will only be added to v1.

ID Format Changes

v0 used tagged UUIDs with prefixes like U-, G-, D-. The tag both namespaced the ID and encoded the resource kind. v1 uses plain UUIDs and named fields (or explicit type enums) so callers do not branch on a prefix:

v0: U-709b8a57-70bc-427a-b6f0-b16ba5297f8c
v1: 709b8a57-70bc-427a-b6f0-b16ba5297f8c

Migration: Strip the prefix and hyphen from stored IDs:

const v1Id = v0Id.replace(/^[UGDCB]-/, "");

Where v0 packed type into the ID, v1 uses separate properties, for example:

v0 (tag or mixed field)v1
Destination inferred from U-… vs G-… / tagged chat iduserIds or groupId or chatId on chat.post
Chat kind encoded in a tagged idtype: "dm" | "group" on chat.list items (same vocabulary as the chat.message chatType filter)
Nested user: { id: "U-…" }userId + userType (user | bot) on messages and webhooks
Mention tokens <@U-…> / <@B-…> / <@C-…> / <@S-…> / <@all> in message textSlack-syntax tokens: <@id> for principals (users and bots), <!subteam^id> for groups and channels, <!channel> for the broadcast (legacy <@all> accepted on write)

Field Naming Convention

v1 follows a consistent convention:

  • id for the primary identifier of the resource being returned
  • thingId (e.g., userId, chatId, groupId) for references in filters, params, and nested objects
v0 Fieldv1 FieldContext
chatchatIdRequest params and response fields
groupidIn group responses; groupId in filters
addressIdidIn list responses
user (object)userIdWebhook payloads (plain UUID instead of embedded object)
userTypePrincipal type beside message/webhook userId (user or bot)

Endpoint Changes

v0 Endpointv1 EndpointNotes
/app.uninstall/token.revokeRenamed. Still only revokes the presented grant (access + refresh); does not uninstall the app. Fires token.revoked, not app.uninstalled.
/groups.list (Legacy)/group.listRenamed
/addr.info/user.info or /group.infoSplit by type: user.info for acting principals (users and bots), group.info for groups
/user.lookup/user.info with email paramConsolidated
v0 /meeting.list/conversation.listRenamed. Lists conversations that occurred (not limited to Magic Minutes / recordings).
/transcript.list/meeting.listMeetings with Magic Minutes and/or video (see also /meeting.info, /meeting.transcript). Field shape differs.
/transcript.info/meeting.transcriptTranscript content; summary / action items / chapters on /meeting.info
/transcript.search/meeting.searchSearch over Magic Minutes meetings
/transcript.prompt/meeting.promptPrompt over a meeting transcript
/recording.listPrefer /meeting.list / /meeting.info; legacy /recording.list still registeredUse hasVideo / videoStatus + meeting.ended. Raw videoUrl remains on the legacy org-mode recording list only.
/meetinglink.create/meeting.link.createRenamed
/meetinglink.info/meeting.link.infoRenamed
/meetinglink.update/meeting.link.updateRenamed
/lobbyBooking.list/lobby.booking.listRenamed
/chat.post (v0)/chat.postSame name; v1 uses UUID chatId / userId / groupId (no tagged prefixes), ok-envelope, and richer body (Block Kit, polls, schedule, streams)
/chat.sendMessage (Legacy)/chat.postLegacy. chat.sendMessage remains registered for existing callers; new integrations must use chat.post
/reaction.removeNew
/reaction.listNew
/token.revoked / /app.uninstalledNew lifecycle webhook events (subscribe with webhook:write)

Response Format Changes

group.members

v0 response:

{
"members": ["U-709b8a57-...", "U-af6663d5-..."]
}

v1 response:

{
"members": [
{ "userId": "709b8a57-...", "role": "member" },
{ "userId": "af6663d5-...", "role": "admin" }
]
}

token.info

v0 response:

{
"addr": "B-b893c426-6d54-4d9a-8e71-6bd53b26124e",
"scopes": ["chat:read", "chat:send_message"],
"roam": { ... }
}

v1 response:

{
"user": {
"id": "b893c426-6d54-4d9a-8e71-6bd53b26124e",
"name": "My Bot",
"imageUrl": "https://..."
},
"scopes": ["chat:read", "chat:send_message"],
"roam": { ... }
}

Webhook Changes

Payload Envelope (important)

Every v1 webhook body is a common envelope. The event-specific payload — what v0 delivered as the whole body — is nested under data. The envelope's type is the full event name (e.g. chat.message), so one HTTPS endpoint can accept many event types and route without guessing from payload shape:

// One URL for chat.message, meeting.ended, lobby.booked, …
app.post("/webhooks/roam", (req, res) => {
const { type, eventId, data } = req.body;
// de-dupe on eventId, then dispatch:
switch (type) {
case "chat.message":
return handleChatMessage(data);
case "meeting.ended":
return handleMeetingEnded(data);
default:
// ignore or log unknown types — additive events should not break you
return;
}
});
// v0: the event object IS the body (no shared envelope)
{
"type": "message",
"chat": "D-3f9a1b2c-...",
"text": "Deploying v2.3.0 now"
}

// v1: self-describing envelope; business fields live under data
{
"type": "chat.message",
"eventId": "0197f9a1-7d2e-7cc3-9f6a-8b1c2d3e4f5a",
"timestamp": "2026-07-07T18:23:45.123456Z",
"apiVersion": "2026-07-07",
"data": {
"version": 1,
"contentType": "text",
"chatId": "3f9a1b2c-...",
"userId": "b7c34e90-...",
"text": "Deploying v2.3.0 now"
}
}
FieldRole
typeFull event name (chat.message), same as the subscription and the Roam-Event-Type header. v0's per-event inner discriminators ("message", "reaction") are gone.
eventIdStable id for this occurrence — identical across retries (and equals webhook-id). Use it to de-duplicate.
timestampWhen the event occurred (RFC3339 UTC).
apiVersionDate version that shaped this body. Envelope exists from 2026-07-07 onward.
dataEvent-specific payload (what used to be the whole v0 body).

You can also point several webhook.subscribe registrations at the same URL — one for chat.message, one for meeting.ended, and so on — and still share a single handler that switches on type.

See the event envelope section of the Webhooks overview for the full field reference.

A single event can currently produce two POSTs to the same URL with the same webhook-id: one legacy tagged-id body (type: "message", sender: "B-…", chat: "D-…") and one v1 body (untagged userId / chatId, or the envelope above when the subscription is pinned to 2026-07-07 or later). De-duplicate on webhook-id and prefer the v1 body. See Dual bodies for one event.

Event Names

v0 uses colons, v1 uses dots to match API naming. /v1/webhook.subscribe rejects colon namesevent: "chat:message:dm" (or lobby:booked, etc.) returns 400 / Unrecognized event. Use the v1 name in the table below.

v0 Eventv1 Event
chat:message:dmchat.message
chat:message:channelchat.message
chat:message:mentionchat.message
chat:message:reactionchat.reaction
transcript:startedmeeting.started
transcript:savedmeeting.ended
user:status:updateuser.status.update
lobby:bookedlobby.booked
recording:savedNo v1 successor — keep a v0 recording:saved subscription if you still need it (v0 webhooks remain supported). For most apps, meeting.ended + the meeting APIs are enough — but note hasVideo only means the meeting was recorded, so poll /meeting.info until videoStatus is available to match recording:saved's "ready" signal.
token.revoked / app.uninstalled
chat.link.shared

Event Consolidation

v0's three chat message events are consolidated into one chat.message event with filter support:

v0 Eventv1 Filter
chat:message:dm{"chatType": "dm"}
chat:message:channel{"chatType": "group"}
chat:message:mention{"mention": true}

This consolidation allows adding new filters in the future without creating new event types.

Filter examples:

// DM messages only
{"chatType": "dm"}

// Group messages only
{"chatType": "group"}

// @mentions only
{"mention": true}

// DM mentions (combined)
{"mention": true, "chatType": "dm"}

// Group mentions (combined)
{"mention": true, "chatType": "group"}

Static Webhook Configuration

In addition to the dynamic /webhook.subscribe endpoint, you can now configure webhooks directly in Roam Administration > Developer > API Client:

  • Configure multiple webhook URLs per app (previously limited to one)
  • Set up event filters without writing code
  • Test webhook delivery with sample payloads

Payload Changes

All v1 payloads below live under the envelope's data key (see Payload Envelope above).

chat.message

v0v1 (inside data)
"type": "message"— (the envelope's type is chat.message)
"chat": "D-abc123""chatId": "abc123"
"user": {"id": "U-xyz", "name": "..."}"userId": "xyz"
"userType": "user" or "bot"; equals user.info.type
"contentType": "text" (new field)
"version": 1 (new field; edits and deletes also fire — see chat.message)

chat.reaction

v1 delivers one event per reaction change (Slack reaction_added / reaction_removed parity): action is "added" or "removed", name is the single reaction that changed, and userId is the user who changed it. v0 instead delivers the message's full reaction snapshot, coalesced and debounced (~10s) per message — which cannot say who changed what, and can skip changes entirely when an add and a remove land in the same window. See chat.reaction.

v0v1 (inside data)
Full "reactions": […] snapshot, debounced per messageOne event per change: "action", "name", "emojiText"
Reactors as tagged IDs in "reactions[].reactors""userId" = the user whose reaction changed
"userType" = actor principal type
"messageSender" (tagged ID)"messageAuthorId" = author of the reacted message
"messageAuthorType" = author principal type
"chat": "D-abc123" (tagged ID)"chatId": "abc123", plus "messageId" and "messageTimestamp"
the envelope's eventId deduplicates at-least-once deliveries; its timestamp is when the change happened
Filter key "codes"Filter key "names", matched against the changed reaction only

To track a message's current reaction set, apply the deltas to state fetched from /reaction.list — or simply re-fetch on each event.

New Headers

v1 adds delivery headers for event identification and versioning:

HeaderDescription
Roam-Event-TypeEvent name (e.g., chat.message); same value as the body's type
Roam-VersionThe API version the body is rendered as; same value as the body's apiVersion
webhook-idThe event's eventId; identical across retries — de-duplicate on it

Subscribe and unsubscribe request shapes

v0 (/v0/webhook.*)v1 (/v1/webhook.*)
Event nameColon (chat:message:dm)Dotted (chat.message)
Subscribe bodyJSON {url, event, filter?}JSON {url, event, filter?, apiVersion?}
Unsubscribe bodyapplication/x-www-form-urlencoded id=<uuid>JSON {"id": "<uuid>"}

Sending JSON {id} to /v0/webhook.unsubscribe returns id parameter required. Sending a colon event name to /v1/webhook.subscribe returns Unrecognized event.

OAuth Scope Changes

Renamed Scopes

v0 Scopev1 Scope
groups:readgroup:read
meeting:readmeetings:read

Old scope names continue to work for existing integrations.

v1 meeting endpoints require meetings:read. Tokens that only hold legacy transcript:read / recordings:read must re-authorize (or expand PAT groups) before calling /meeting.*. See the Scopes catalog.

meetings:read is not roam-wide. Org-wide meetings, recordings, transcripts, and those webhooks require admin:meetings:read (API keys auto-check it; OAuth defaults it off). Personal tokens never hold it. See Meeting width.

Timezone Handling

v1 endpoints respect the authenticated user's timezone for date-only inputs and human-readable response timestamps:

  • Date-only query params (after, before, startDate, endDate) are parsed at local midnight in the user's timezone, not UTC. For a user in America/Los_Angeles, after=2026-04-15 means 2026-04-15T00:00:00-07:00.
  • Response timestamps carrying calendar/event data (e.g. created, lastMessageTime, startTime, endTime, dateCreated) are emitted as RFC3339 with the user's offset.
  • Cursors and webhook payloads stay in UTC. Treat them as opaque server-to-server values.

This prevents off-by-a-day results for users west of UTC. v0 endpoints continue to use UTC.

Base URL

VersionBase URL
v0https://api.ro.am/v0
v1https://api.ro.am/v1

Migration Checklist

  1. Update base URL from /v0 to /v1
  2. Strip ID prefixes from all stored IDs
  3. Update field names in request/response handling:
    • chatchatId
    • groupid (in responses)
  4. Update endpoint paths for renamed endpoints
  5. Update webhook handlers:
    • Update event names (colons → dots). v1 subscribe rejects colon names.
    • Parse the payload envelope: route on type, read the event payload from data
    • De-duplicate deliveries on eventId / webhook-id (a single event can currently arrive twice, once in each body shape)
    • Handle consolidated chat.message event
    • Update field names in payloads
    • Switch unsubscribe from form-urlencoded to JSON {id}
  6. Update OAuth scopes if re-authorizing:
    • groups:readgroup:read
  7. Update group.members handling to expect { userId, role } objects
  8. Update actor handling to read userType / messageAuthorType and hydrate IDs through user.info or user.list?ids

Questions?

Contact us via Roam Support Chat or email developer@ro.am.