Identity & Principals
Every v1 acting userId emitted by chat history/search or the chat.message and chat.reaction
webhooks resolves through user.info under the same credentials, and any
accompanying userType or messageAuthorType equals user.info.type.
Roam uses one principal ID namespace for people and automated actors. A principal always has one of two public types:
| Principal | type | Additional signal |
|---|---|---|
| Workspace member | user | Member fields such as isAdmin are present |
| Guest (a person without membership in your account) | user | isGuest: true |
| Classic bot, agent, assistant, or coworker | bot | botCode and integrationId appear when that actor has them |
Automated implementation details intentionally collapse to bot. Do not branch on display names or
assume that every bot has botCode or integrationId.
Directory vs. hydration
An unfiltered user.list is the workspace member directory. It lists active
members only; it never enumerates guests or bots.
Use user.list?ids=<id1,id2,...> to hydrate IDs you already hold. This mode accepts up to 100
comma-separated bare or tagged IDs, deduplicates them in first-seen order, and returns resolved
principals in that order. It has no cursor and cannot be combined with q, limit, or cursor.
Unknown IDs, groups, and principals outside your authorization boundary are silently omitted, so an
empty result does not reveal whether an ID exists.
Explicit ID lookup is historical as well as current: user.info?id=... and user.list?ids=... can
resolve archived or deactivated principals. This lets stored messages remain attributable after
offboarding. Email lookup is different: it is member-only and requires user:read.email.
For user targets, a known UUID plus user:read is sufficient; prior shared-chat provenance is not
required. Guests expose public display identity and isGuest: true, with email only when the
credential also has user:read.email. Bots and agents are limited to the caller's Roam, assistants
to the caller's account, and coworkers to the account of their owning principal. Singular
unauthorized lookups return user_not_found; batch hydration silently omits them.
Avoid bot loops
History/search messages and chat.message webhooks include required userType. Reaction webhooks
include both actor userType and messageAuthorType. Use these fields before doing any extra
lookup:
if (event.data.userType === "bot") return;
When a surface supplies only an ID—such as group.members, reaction.list, poll voters, chat
previews, or meeting participants—hydrate it with user.info or batch IDs through user.list?ids.
Address sidecars
Principals are only one kind of chat address. Mentions may also refer to groups or the literal
all — the mention token's sigil tells you which: <@ID> is a principal (resolve with
user.info), <!subteam^ID> is a group or channel (resolve with
group.info).
For chat payloads, request expand=addresses on chat.list,
chat.history, or chat.search to receive an
address map keyed by UUID. chat.link.resolve does not support this expansion.
Slack capability mapping
| Slack concept | Roam v1 equivalent | Important difference |
|---|---|---|
users.list | Unfiltered user.list | Roam keeps this strictly as the active workspace member directory |
users.info | user.info | Roam also hydrates guests and bot-like actors from the same userId namespace |
users.info called repeatedly | user.list?ids= | Roam provides ordered bulk hydration for up to 100 IDs |
is_restricted / guest users | type: "user", isGuest: true | Roam's guest signal means no membership in the caller's account |
is_bot, bot_id, or app_id | type: "bot" plus optional botCode / integrationId | Agents, assistants, and coworkers intentionally share the bot vocabulary |
| Message bot subtype checks | userType / messageAuthorType | Roam puts principal type directly beside actor IDs |
<@U…> user mentions | <@uuid> | Same syntax; a |label suffix is accepted and ignored on write, reads are always bare |
<!subteam^S…> group mentions | <!subteam^uuid> | Same syntax and semantics (notifies members); Roam also uses it for channel mentions, which Slack lacks |
<!channel> broadcast | <!channel> | Same syntax and semantics; the legacy <@all> is accepted on write |
<#C…> channel links, <!here>, <!everyone> | Reserved — not tokens | Roam has no channel links or presence/workspace-scoped notify yet; these stay literal text |
The model is Slack-capable without copying Slack's split identity namespaces: one Roam userId is
sufficient for attribution, loop prevention, and later hydration.